Browse Source

Merge remote-tracking branch 'origin/develop' into fix_rmg_teams

# Conflicts:
#	lib/rmg/CMapGenOptions.cpp
#	lib/rmg/CMapGenOptions.h
Tomasz Zieliński 1 year ago
parent
commit
c909bd766e
100 changed files with 1100 additions and 1096 deletions
  1. 13 12
      AI/Nullkiller/AIGateway.cpp
  2. 1 1
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  3. 1 1
      AI/Nullkiller/Behaviors/StartupBehavior.cpp
  4. 2 5
      AI/Nullkiller/Engine/FuzzyHelper.cpp
  5. 21 10
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  6. 0 1
      AI/VCAI/AIUtility.h
  7. 1 1
      AI/VCAI/FuzzyEngines.cpp
  8. 2 9
      AI/VCAI/FuzzyHelper.cpp
  9. 2 2
      AI/VCAI/Goals/CollectRes.cpp
  10. 1 1
      AI/VCAI/Goals/GatherTroops.cpp
  11. 1 1
      AI/VCAI/Pathfinding/PathfindingManager.cpp
  12. 1 13
      AI/VCAI/ResourceManager.cpp
  13. 14 13
      AI/VCAI/VCAI.cpp
  14. 1 1
      AI/VCAI/VCAI.h
  15. 1 1
      CCallback.cpp
  16. 10 16
      client/CPlayerInterface.cpp
  17. 1 1
      client/HeroMovementController.cpp
  18. 11 11
      client/NetPacksClient.cpp
  19. 17 13
      client/adventureMap/CInfoBar.cpp
  20. 1 1
      client/battle/BattleActionsController.cpp
  21. 10 2
      client/battle/BattleObstacleController.cpp
  22. 1 1
      client/lobby/CBonusSelection.cpp
  23. 4 2
      client/lobby/CSelectionBase.cpp
  24. 1 1
      client/lobby/OptionsTab.cpp
  25. 2 3
      client/mainmenu/CHighScoreScreen.cpp
  26. 1 5
      client/mapView/MapRendererContext.cpp
  27. 11 3
      client/render/CAnimation.cpp
  28. 10 10
      client/widgets/CArtifactHolder.cpp
  29. 2 2
      client/widgets/CArtifactsOfHeroAltar.cpp
  30. 2 2
      client/widgets/CArtifactsOfHeroBackpack.cpp
  31. 3 3
      client/widgets/CArtifactsOfHeroBase.cpp
  32. 3 2
      client/widgets/CArtifactsOfHeroKingdom.cpp
  33. 3 2
      client/widgets/CArtifactsOfHeroMain.cpp
  34. 183 133
      client/widgets/CComponent.cpp
  35. 11 20
      client/widgets/CComponent.h
  36. 8 6
      client/widgets/CGarrisonInt.cpp
  37. 6 6
      client/widgets/CWindowWithArtifacts.cpp
  38. 19 25
      client/widgets/MiscWidgets.cpp
  39. 5 5
      client/widgets/MiscWidgets.h
  40. 26 17
      client/windows/CCastleInterface.cpp
  41. 8 7
      client/windows/CCreatureWindow.cpp
  42. 14 22
      client/windows/CHeroWindow.cpp
  43. 2 2
      client/windows/CKingdomInterface.cpp
  44. 2 2
      client/windows/CSpellWindow.cpp
  45. 4 4
      client/windows/CTradeWindow.cpp
  46. 4 2
      client/windows/CWindowObject.cpp
  47. 2 1
      client/windows/CWindowObject.h
  48. 6 11
      client/windows/GUIClasses.cpp
  49. 1 1
      client/windows/GUIClasses.h
  50. 2 2
      cmake_modules/VCMI_lib.cmake
  51. 14 0
      cmake_modules/create_link.cmake
  52. 4 0
      docs/players/Installation_iOS.md
  53. 3 4
      launcher/CMakeLists.txt
  54. 2 0
      launcher/settingsView/csettingsview_moc.cpp
  55. 44 1
      lib/ArtifactUtils.cpp
  56. 2 0
      lib/ArtifactUtils.h
  57. 7 7
      lib/CArtHandler.cpp
  58. 9 9
      lib/CArtifactInstance.cpp
  59. 5 4
      lib/CArtifactInstance.h
  60. 6 6
      lib/CBuildingHandler.cpp
  61. 1 1
      lib/CBuildingHandler.h
  62. 16 27
      lib/CCreatureHandler.cpp
  63. 7 2
      lib/CCreatureSet.cpp
  64. 2 1
      lib/CCreatureSet.h
  65. 17 0
      lib/CGameInfoCallback.cpp
  66. 3 0
      lib/CGameInfoCallback.h
  67. 10 10
      lib/CHeroHandler.cpp
  68. 1 7
      lib/CHeroHandler.h
  69. 2 21
      lib/CSkillHandler.cpp
  70. 0 5
      lib/CSkillHandler.h
  71. 1 1
      lib/CTownHandler.cpp
  72. 3 3
      lib/IGameCallback.cpp
  73. 3 3
      lib/JsonRandom.cpp
  74. 6 6
      lib/MetaString.cpp
  75. 0 13
      lib/ResourceSet.cpp
  76. 2 3
      lib/ResourceSet.h
  77. 2 2
      lib/battle/CBattleInfoCallback.cpp
  78. 1 1
      lib/battle/SideInBattle.cpp
  79. 1 1
      lib/battle/Unit.cpp
  80. 3 3
      lib/bonuses/Bonus.cpp
  81. 3 3
      lib/bonuses/Limiters.cpp
  82. 9 10
      lib/campaign/CampaignState.cpp
  83. 43 6
      lib/constants/EntityIdentifiers.cpp
  84. 51 17
      lib/constants/EntityIdentifiers.h
  85. 1 1
      lib/constants/VariantIdentifier.h
  86. 50 285
      lib/gameState/CGameState.cpp
  87. 2 3
      lib/gameState/CGameState.h
  88. 14 14
      lib/gameState/CGameStateCampaign.cpp
  89. 3 3
      lib/gameState/TavernHeroesPool.cpp
  90. 11 14
      lib/mapObjectConstructors/CObjectClassesHandler.cpp
  91. 9 9
      lib/mapObjectConstructors/CObjectClassesHandler.h
  92. 2 2
      lib/mapObjects/CArmedInstance.cpp
  93. 18 22
      lib/mapObjects/CBank.cpp
  94. 55 18
      lib/mapObjects/CGCreature.cpp
  95. 2 0
      lib/mapObjects/CGCreature.h
  96. 127 65
      lib/mapObjects/CGDwelling.cpp
  97. 11 34
      lib/mapObjects/CGDwelling.h
  98. 50 29
      lib/mapObjects/CGHeroInstance.cpp
  99. 3 1
      lib/mapObjects/CGHeroInstance.h
  100. 1 1
      lib/mapObjects/CGMarket.cpp

+ 13 - 12
AI/Nullkiller/AIGateway.cpp

@@ -9,6 +9,7 @@
  */
 #include "StdInc.h"
 
+#include "../../lib/ArtifactUtils.h"
 #include "../../lib/UnlockGuard.h"
 #include "../../lib/mapObjects/MapObjects.h"
 #include "../../lib/mapObjects/ObjectTemplate.h"
@@ -663,7 +664,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 		// TODO: Find better way to understand it is Chest of Treasures
 		if(hero.validAndSet()
 			&& components.size() == 2
-			&& components.front().id == Component::EComponentType::RESOURCE
+			&& components.front().type == ComponentType::RESOURCE
 			&& (nullkiller->heroManager->getHeroRole(hero) != HeroRole::MAIN
 			|| nullkiller->buildAnalyzer->getGoldPreasure() > MAX_GOLD_PEASURE))
 		{
@@ -995,21 +996,21 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
 				for(auto p : h->artifactsWorn)
 				{
 					if(p.second.artifact)
-						allArtifacts.push_back(ArtifactLocation(h, p.first));
+						allArtifacts.push_back(ArtifactLocation(h->id, p.first));
 				}
 			}
 			for(auto slot : h->artifactsInBackpack)
-				allArtifacts.push_back(ArtifactLocation(h, h->getArtPos(slot.artifact)));
+				allArtifacts.push_back(ArtifactLocation(h->id, h->getArtPos(slot.artifact)));
 
 			if(otherh)
 			{
 				for(auto p : otherh->artifactsWorn)
 				{
 					if(p.second.artifact)
-						allArtifacts.push_back(ArtifactLocation(otherh, p.first));
+						allArtifacts.push_back(ArtifactLocation(otherh->id, p.first));
 				}
 				for(auto slot : otherh->artifactsInBackpack)
-					allArtifacts.push_back(ArtifactLocation(otherh, otherh->getArtPos(slot.artifact)));
+					allArtifacts.push_back(ArtifactLocation(otherh->id, otherh->getArtPos(slot.artifact)));
 			}
 			//we give stuff to one hero or another, depending on giveStuffToFirstHero
 
@@ -1021,13 +1022,13 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
 
 			for(auto location : allArtifacts)
 			{
-				if(location.relatedObj() == target && location.slot < ArtifactPosition::AFTER_LAST)
+				if(location.artHolder == target->id && ArtifactUtils::isSlotEquipment(location.slot))
 					continue; //don't reequip artifact we already wear
 
 				if(location.slot == ArtifactPosition::MACH4) // don't attempt to move catapult
 					continue;
 
-				auto s = location.getSlot();
+				auto s = cb->getHero(location.artHolder)->getSlot(location.slot);
 				if(!s || s->locked) //we can't move locks
 					continue;
 				auto artifact = s->artifact;
@@ -1038,9 +1039,9 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
 				bool emptySlotFound = false;
 				for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
 				{
-					ArtifactLocation destLocation(target, slot);
-					if(target->isPositionFree(slot) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move
+					if(target->isPositionFree(slot) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move
 					{
+						ArtifactLocation destLocation(target->id, slot);
 						cb->swapArtifacts(location, destLocation); //just put into empty slot
 						emptySlotFound = true;
 						changeMade = true;
@@ -1054,11 +1055,11 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
 						auto otherSlot = target->getSlot(slot);
 						if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one
 						{
-							ArtifactLocation destLocation(target, slot);
 							//if that artifact is better than what we have, pick it
-							if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move
+							if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move
 							{
-								cb->swapArtifacts(location, ArtifactLocation(target, target->getArtPos(otherSlot->artifact)));
+								ArtifactLocation destLocation(target->id, slot);
+								cb->swapArtifacts(location, ArtifactLocation(target->id, target->getArtPos(otherSlot->artifact)));
 								changeMade = true;
 								break;
 							}

+ 1 - 1
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

@@ -318,7 +318,7 @@ bool BuildAnalyzer::hasAnyBuilding(int32_t alignment, BuildingID bid) const
 {
 	for(auto tdi : developmentInfos)
 	{
-		if(tdi.town->subID == alignment && tdi.town->hasBuilt(bid))
+		if(tdi.town->getFaction() == alignment && tdi.town->hasBuilt(bid))
 			return true;
 	}
 

+ 1 - 1
AI/Nullkiller/Behaviors/StartupBehavior.cpp

@@ -71,7 +71,7 @@ bool needToRecruitHero(const CGTownInstance * startupTown)
 
 	for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects())
 	{
-		if((obj->ID == Obj::RESOURCE && obj->subID == GameResID(EGameResID::GOLD))
+		if((obj->ID == Obj::RESOURCE && dynamic_cast<const CGResource *>(obj)->resourceID() == EGameResID::GOLD)
 			|| obj->ID == Obj::TREASURE_CHEST
 			|| obj->ID == Obj::CAMPFIRE
 			|| obj->ID == Obj::WATER_WHEEL)

+ 2 - 5
AI/Nullkiller/Engine/FuzzyHelper.cpp

@@ -24,7 +24,7 @@ ui64 FuzzyHelper::estimateBankDanger(const CBank * bank)
 {
 	//this one is not fuzzy anymore, just calculate weighted average
 
-	auto objectInfo = VLC->objtypeh->getHandlerFor(bank->ID, bank->subID)->getObjectInfo(bank->appearance);
+	auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance);
 
 	CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
 
@@ -161,10 +161,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
 	}
 	case Obj::PYRAMID:
 	{
-		if(obj->subID == 0)
-			return estimateBankDanger(dynamic_cast<const CBank *>(obj));
-		else
-			return 0;
+		return estimateBankDanger(dynamic_cast<const CBank *>(obj));
 	}
 	default:
 		return 0;

+ 21 - 10
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -122,7 +122,7 @@ TResources getCreatureBankResources(const CGObjectInstance * target, const CGHer
 {
 	//Fixme: unused variable hero
 
-	auto objectInfo = VLC->objtypeh->getHandlerFor(target->ID, target->subID)->getObjectInfo(target->appearance);
+	auto objectInfo = target->getObjectHandler()->getObjectInfo(target->appearance);
 	CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
 	auto resources = bankInfo->getPossibleResourcesReward();
 	TResources result = TResources();
@@ -139,7 +139,7 @@ TResources getCreatureBankResources(const CGObjectInstance * target, const CGHer
 
 uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero)
 {
-	auto objectInfo = VLC->objtypeh->getHandlerFor(target->ID, target->subID)->getObjectInfo(target->appearance);
+	auto objectInfo = target->getObjectHandler()->getObjectInfo(target->appearance);
 	CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
 	auto creatures = bankInfo->getPossibleCreaturesReward();
 	uint64_t result = 0;
@@ -467,14 +467,20 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
 	switch(target->ID)
 	{
 	case Obj::MINE:
-		return target->subID == GameResID(EGameResID::GOLD)
+	{
+		auto mine = dynamic_cast<const CGMine *>(target);
+		return mine->producedResource == EGameResID::GOLD
 			? 0.5f 
-			: 0.4f * getTotalResourceRequirementStrength(target->subID) + 0.1f * getResourceRequirementStrength(target->subID);
+			: 0.4f * getTotalResourceRequirementStrength(mine->producedResource) + 0.1f * getResourceRequirementStrength(mine->producedResource);
+	}
 
 	case Obj::RESOURCE:
-		return target->subID == GameResID(EGameResID::GOLD)
+	{
+		auto resource = dynamic_cast<const CGResource *>(target);
+		return resource->resourceID() == EGameResID::GOLD
 			? 0
-			: 0.2f * getTotalResourceRequirementStrength(target->subID) + 0.4f * getResourceRequirementStrength(target->subID);
+			: 0.2f * getTotalResourceRequirementStrength(resource->resourceID()) + 0.4f * getResourceRequirementStrength(resource->resourceID());
+	}
 
 	case Obj::CREATURE_BANK:
 	{
@@ -626,12 +632,14 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
 	const int dailyIncomeMultiplier = 5;
 	const float enemyArmyEliminationGoldRewardRatio = 0.2f;
 	const int32_t heroEliminationBonus = GameConstants::HERO_GOLD_COST / 2;
-	auto isGold = target->subID == GameResID(EGameResID::GOLD); // TODO: other resorces could be sold but need to evaluate market power
 
 	switch(target->ID)
 	{
 	case Obj::RESOURCE:
-		return isGold ? 600 : 100;
+	{
+		auto * res = dynamic_cast<const CGResource*>(target);
+		return res->resourceID() == GameResID::GOLD ? 600 : 100;
+	}
 	case Obj::TREASURE_CHEST:
 		return 1500;
 	case Obj::WATER_WHEEL:
@@ -640,7 +648,10 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
 		return dailyIncomeMultiplier * estimateTownIncome(ai->cb.get(), target, hero);
 	case Obj::MINE:
 	case Obj::ABANDONED_MINE:
-		return dailyIncomeMultiplier * (isGold ? 1000 : 75);
+	{
+		auto * mine = dynamic_cast<const CGMine*>(target);
+		return dailyIncomeMultiplier * (mine->producedResource == GameResID::GOLD ? 1000 : 75);
+	}
 	case Obj::MYSTICAL_GARDEN:
 	case Obj::WINDMILL:
 		return 100;
@@ -1005,7 +1016,7 @@ public:
 
 uint64_t RewardEvaluator::getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const
 {
-	if(ai->buildAnalyzer->hasAnyBuilding(town->subID, bi.id))
+	if(ai->buildAnalyzer->hasAnyBuilding(town->getFaction(), bi.id))
 		return 0;
 
 	auto creaturesToUpgrade = ai->armyManager->getTotalCreaturesAvailable(bi.baseCreatureID);

+ 0 - 1
AI/VCAI/AIUtility.h

@@ -26,7 +26,6 @@ using crint3 = const int3 &;
 using crstring = const std::string &;
 using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
 
-const int GOLD_MINE_PRODUCTION = 1000, WOOD_ORE_MINE_PRODUCTION = 2, RESOURCE_MINE_PRODUCTION = 1;
 const int ACTUAL_RESOURCE_COUNT = 7;
 const int ALLOWED_ROAMING_HEROES = 8;
 

+ 1 - 1
AI/VCAI/FuzzyEngines.cpp

@@ -406,7 +406,7 @@ float VisitObjEngine::evaluate(Goals::VisitObj & goal)
 	else
 	{
 		MapObjectsEvaluator::getInstance().addObjectData(obj->ID, obj->subID, 0);
-		logGlobal->error("AI met object type it doesn't know - ID: " + std::to_string(obj->ID) + ", subID: " + std::to_string(obj->subID) + " - adding to database with value " + std::to_string(objValue));
+		logGlobal->error("AI met object type it doesn't know - ID: %d, subID: %d - adding to database with value %d ", obj->ID, obj->subID, objValue);
 	}
 
 	setSharedFuzzyVariables(goal);

+ 2 - 9
AI/VCAI/FuzzyHelper.cpp

@@ -66,7 +66,7 @@ ui64 FuzzyHelper::estimateBankDanger(const CBank * bank)
 {
 	//this one is not fuzzy anymore, just calculate weighted average
 
-	auto objectInfo = VLC->objtypeh->getHandlerFor(bank->ID, bank->subID)->getObjectInfo(bank->appearance);
+	auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance);
 
 	CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
 
@@ -324,15 +324,8 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai)
 	case Obj::DRAGON_UTOPIA:
 	case Obj::SHIPWRECK: //shipwreck
 	case Obj::DERELICT_SHIP: //derelict ship
-							 //	case Obj::PYRAMID:
-		return estimateBankDanger(dynamic_cast<const CBank *>(obj));
 	case Obj::PYRAMID:
-	{
-		if(obj->subID == 0)
-			return estimateBankDanger(dynamic_cast<const CBank *>(obj));
-		else
-			return 0;
-	}
+		return estimateBankDanger(dynamic_cast<const CBank *>(obj));
 	default:
 		return 0;
 	}

+ 2 - 2
AI/VCAI/Goals/CollectRes.cpp

@@ -43,10 +43,10 @@ TGoalVec CollectRes::getAllPossibleSubgoals()
 			return resID == GameResID(EGameResID::GOLD);
 			break;
 		case Obj::RESOURCE:
-			return obj->subID == resID;
+			return dynamic_cast<const CGResource*>(obj)->resourceID() == GameResID(resID);
 			break;
 		case Obj::MINE:
-			return (obj->subID == resID &&
+			return (dynamic_cast<const CGMine*>(obj)->producedResource == GameResID(resID) &&
 				(cb->getPlayerRelations(obj->tempOwner, ai->playerID) == PlayerRelations::ENEMIES)); //don't capture our mines
 			break;
 		case Obj::CAMPFIRE:

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

@@ -88,7 +88,7 @@ TGoalVec GatherTroops::getAllPossibleSubgoals()
 		}
 
 		auto creature = VLC->creatures()->getByIndex(objid);
-		if(t->subID == creature->getFaction()) //TODO: how to force AI to build unupgraded creatures? :O
+		if(t->getFaction() == creature->getFaction()) //TODO: how to force AI to build unupgraded creatures? :O
 		{
 			auto tryFindCreature = [&]() -> std::optional<std::vector<CreatureID>>
 			{

+ 1 - 1
AI/VCAI/Pathfinding/PathfindingManager.cpp

@@ -190,7 +190,7 @@ Goals::TSubgoal PathfindingManager::clearWayTo(HeroPtr hero, int3 firstTileToGet
 	if(isBlockedBorderGate(firstTileToGet))
 	{
 		//FIXME: this way we'll not visit gate and activate quest :?
-		return sptr(Goals::FindObj(Obj::KEYMASTER, cb->getTile(firstTileToGet)->visitableObjects.back()->subID));
+		return sptr(Goals::FindObj(Obj::KEYMASTER, cb->getTile(firstTileToGet)->visitableObjects.back()->getObjTypeIndex()));
 	}
 
 	auto topObj = cb->getTopObj(firstTileToGet);

+ 1 - 13
AI/VCAI/ResourceManager.cpp

@@ -59,19 +59,7 @@ TResources ResourceManager::estimateIncome() const
 		if (obj->ID == Obj::MINE)
 		{
 			auto mine = dynamic_cast<const CGMine*>(obj);
-			switch (mine->producedResource.toEnum())
-			{
-			case EGameResID::WOOD:
-			case EGameResID::ORE:
-				ret[obj->subID] += WOOD_ORE_MINE_PRODUCTION;
-				break;
-			case EGameResID::GOLD:
-				ret[EGameResID::GOLD] += GOLD_MINE_PRODUCTION;
-				break;
-			default:
-				ret[obj->subID] += RESOURCE_MINE_PRODUCTION;
-				break;
-			}
+			ret += mine->dailyIncome();
 		}
 	}
 

+ 14 - 13
AI/VCAI/VCAI.cpp

@@ -14,6 +14,7 @@
 #include "BuildingManager.h"
 #include "Goals/Goals.h"
 
+#include "../../lib/ArtifactUtils.h"
 #include "../../lib/UnlockGuard.h"
 #include "../../lib/mapObjects/MapObjects.h"
 #include "../../lib/mapObjects/ObjectTemplate.h"
@@ -1166,21 +1167,21 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot
 				for(auto p : h->artifactsWorn)
 				{
 					if(p.second.artifact)
-						allArtifacts.push_back(ArtifactLocation(h, p.first));
+						allArtifacts.push_back(ArtifactLocation(h->id, p.first));
 				}
 			}
 			for(auto slot : h->artifactsInBackpack)
-				allArtifacts.push_back(ArtifactLocation(h, h->getArtPos(slot.artifact)));
+				allArtifacts.push_back(ArtifactLocation(h->id, h->getArtPos(slot.artifact)));
 
 			if(otherh)
 			{
 				for(auto p : otherh->artifactsWorn)
 				{
 					if(p.second.artifact)
-						allArtifacts.push_back(ArtifactLocation(otherh, p.first));
+						allArtifacts.push_back(ArtifactLocation(otherh->id, p.first));
 				}
 				for(auto slot : otherh->artifactsInBackpack)
-					allArtifacts.push_back(ArtifactLocation(otherh, otherh->getArtPos(slot.artifact)));
+					allArtifacts.push_back(ArtifactLocation(otherh->id, otherh->getArtPos(slot.artifact)));
 			}
 			//we give stuff to one hero or another, depending on giveStuffToFirstHero
 
@@ -1195,10 +1196,10 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot
 				if(location.slot == ArtifactPosition::MACH4 || location.slot == ArtifactPosition::SPELLBOOK)
 					continue; // don't attempt to move catapult and spellbook
 
-				if(location.relatedObj() == target && location.slot < ArtifactPosition::AFTER_LAST)
+				if(location.artHolder == target->id && ArtifactUtils::isSlotEquipment(location.slot))
 					continue; //don't reequip artifact we already wear
 
-				auto s = location.getSlot();
+				auto s = cb->getHero(location.artHolder)->getSlot(location.slot);
 				if(!s || s->locked) //we can't move locks
 					continue;
 				auto artifact = s->artifact;
@@ -1209,9 +1210,9 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot
 				bool emptySlotFound = false;
 				for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
 				{
-					ArtifactLocation destLocation(target, slot);
-					if(target->isPositionFree(slot) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move
+					if(target->isPositionFree(slot) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move
 					{
+						ArtifactLocation destLocation(target->id, slot);
 						cb->swapArtifacts(location, destLocation); //just put into empty slot
 						emptySlotFound = true;
 						changeMade = true;
@@ -1225,11 +1226,11 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot
 						auto otherSlot = target->getSlot(slot);
 						if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one
 						{
-							ArtifactLocation destLocation(target, slot);
 							//if that artifact is better than what we have, pick it
-							if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move
+							if(compareArtifacts(artifact, otherSlot->artifact) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move
 							{
-								cb->swapArtifacts(location, ArtifactLocation(target, target->getArtPos(otherSlot->artifact)));
+								ArtifactLocation destLocation(target->id, slot);
+								cb->swapArtifacts(location, ArtifactLocation(target->id, target->getArtPos(otherSlot->artifact)));
 								changeMade = true;
 								break;
 							}
@@ -1759,11 +1760,11 @@ void VCAI::addVisitableObj(const CGObjectInstance * obj)
 		CGTeleport::addToChannel(knownTeleportChannels, teleportObj);
 }
 
-const CGObjectInstance * VCAI::lookForArt(int aid) const
+const CGObjectInstance * VCAI::lookForArt(ArtifactID aid) const
 {
 	for(const CGObjectInstance * obj : ai->visitableObjs)
 	{
-		if(obj->ID == Obj::ARTIFACT && obj->subID == aid)
+		if(obj->ID == Obj::ARTIFACT && dynamic_cast<const CGArtifact *>(obj)->getArtifact()  == aid)
 			return obj;
 	}
 

+ 1 - 1
AI/VCAI/VCAI.h

@@ -251,7 +251,7 @@ public:
 	void retrieveVisitableObjs();
 	virtual std::vector<const CGObjectInstance *> getFlaggedObjects() const;
 
-	const CGObjectInstance * lookForArt(int aid) const;
+	const CGObjectInstance * lookForArt(ArtifactID aid) const;
 	bool isAccessible(const int3 & pos) const;
 	HeroPtr getHeroWithGrail() const;
 

+ 1 - 1
CCallback.cpp

@@ -265,7 +265,7 @@ void CCallback::recruitHero(const CGObjectInstance *townOrTavern, const CGHeroIn
 	assert(townOrTavern);
 	assert(hero);
 
-	HireHero pack(HeroTypeID(hero->subID), townOrTavern->id);
+	HireHero pack(hero->getHeroType(), townOrTavern->id);
 	pack.player = *player;
 	sendRequest(&pack);
 }

+ 10 - 16
client/CPlayerInterface.cpp

@@ -293,7 +293,7 @@ void CPlayerInterface::yourTurn(QueryID queryID)
 			std::string msg = CGI->generaltexth->allTexts[13];
 			boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name);
 			std::vector<std::shared_ptr<CComponent>> cmp;
-			cmp.push_back(std::make_shared<CComponent>(CComponent::flag, playerID.getNum(), 0));
+			cmp.push_back(std::make_shared<CComponent>(ComponentType::FLAG, playerID));
 			showInfoDialog(msg, cmp);
 		}
 		else
@@ -326,7 +326,7 @@ void CPlayerInterface::acceptTurn(QueryID queryID)
 		auto playerColor = *cb->getPlayerID();
 
 		std::vector<Component> components;
-		components.emplace_back(Component::EComponentType::FLAG, playerColor.getNum(), 0, 0);
+		components.emplace_back(ComponentType::FLAG, playerColor);
 		MetaString text;
 
 		const auto & optDaysWithoutCastle = cb->getPlayerState(playerColor)->daysWithoutCastle;
@@ -1228,7 +1228,7 @@ void CPlayerInterface::showArtifactAssemblyDialog(const Artifact * artifact, con
 		text += boost::str(boost::format(CGI->generaltexth->allTexts[732]) % assembledArtifact->getNameTranslated());
 
 		// Picture of assembled artifact at bottom.
-		auto sc = std::make_shared<CComponent>(CComponent::artifact, assembledArtifact->getIndex(), 0);
+		auto sc = std::make_shared<CComponent>(ComponentType::ARTIFACT, assembledArtifact->getId());
 		scs.push_back(sc);
 	}
 	else
@@ -1441,7 +1441,7 @@ void CPlayerInterface::playerBlocked(int reason, bool start)
 			std::string msg = CGI->generaltexth->translate("vcmi.adventureMap.playerAttacked");
 			boost::replace_first(msg, "%s", cb->getStartInfo()->playerInfos.find(playerID)->second.name);
 			std::vector<std::shared_ptr<CComponent>> cmp;
-			cmp.push_back(std::make_shared<CComponent>(CComponent::flag, playerID.getNum(), 0));
+			cmp.push_back(std::make_shared<CComponent>(ComponentType::FLAG, playerID));
 			makingTurn = true; //workaround for stiff showInfoDialog implementation
 			showInfoDialog(msg, cmp);
 			makingTurn = false;
@@ -1753,8 +1753,7 @@ void CPlayerInterface::requestReturningToMainMenu(bool won)
 
 void CPlayerInterface::askToAssembleArtifact(const ArtifactLocation &al)
 {
-	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
-	if(hero)
+	if(auto hero = cb->getHero(al.artHolder))
 	{
 		auto art = hero->getArt(al.slot);
 		if(art == nullptr)
@@ -1770,15 +1769,13 @@ void CPlayerInterface::askToAssembleArtifact(const ArtifactLocation &al)
 void CPlayerInterface::artifactPut(const ArtifactLocation &al)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
-	adventureInt->onHeroChanged(hero);
+	adventureInt->onHeroChanged(cb->getHero(al.artHolder));
 }
 
 void CPlayerInterface::artifactRemoved(const ArtifactLocation &al)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
-	adventureInt->onHeroChanged(hero);
+	adventureInt->onHeroChanged(cb->getHero(al.artHolder));
 
 	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
 		artWin->artifactRemoved(al);
@@ -1789,8 +1786,7 @@ void CPlayerInterface::artifactRemoved(const ArtifactLocation &al)
 void CPlayerInterface::artifactMoved(const ArtifactLocation &src, const ArtifactLocation &dst)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	auto hero = std::visit(HeroObjectRetriever(), dst.artHolder);
-	adventureInt->onHeroChanged(hero);
+	adventureInt->onHeroChanged(cb->getHero(dst.artHolder));
 
 	bool redraw = true;
 	// If a bulk transfer has arrived, then redrawing only the last art movement.
@@ -1815,8 +1811,7 @@ void CPlayerInterface::bulkArtMovementStart(size_t numOfArts)
 void CPlayerInterface::artifactAssembled(const ArtifactLocation &al)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
-	adventureInt->onHeroChanged(hero);
+	adventureInt->onHeroChanged(cb->getHero(al.artHolder));
 
 	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
 		artWin->artifactAssembled(al);
@@ -1825,8 +1820,7 @@ void CPlayerInterface::artifactAssembled(const ArtifactLocation &al)
 void CPlayerInterface::artifactDisassembled(const ArtifactLocation &al)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	auto hero = std::visit(HeroObjectRetriever(), al.artHolder);
-	adventureInt->onHeroChanged(hero);
+	adventureInt->onHeroChanged(cb->getHero(al.artHolder));
 
 	for(auto artWin : GH.windows().findWindows<CArtifactHolder>())
 		artWin->artifactDisassembled(al);

+ 1 - 1
client/HeroMovementController.cpp

@@ -359,7 +359,7 @@ void HeroMovementController::moveOnce(const CGHeroInstance * h, const CGPath & p
 	{
 		stopMovementSound();
 		logGlobal->trace("Requesting hero teleportation to %s", nextNode.coord.toString());
-		LOCPLINT->cb->moveHero(h, nextCoord, false);
+		LOCPLINT->cb->moveHero(h, h->pos, false);
 		return;
 	}
 	else

+ 11 - 11
client/NetPacksClient.cpp

@@ -267,14 +267,14 @@ void ApplyClientNetPackVisitor::visitBulkSmartRebalanceStacks(BulkSmartRebalance
 
 void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack)
 {
-	callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactPut, pack.al);
+	callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactPut, pack.al);
 	if(pack.askAssemble)
-		callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::askToAssembleArtifact, pack.al);
+		callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::askToAssembleArtifact, pack.al);
 }
 
 void ApplyClientNetPackVisitor::visitEraseArtifact(EraseArtifact & pack)
 {
-	callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactRemoved, pack.al);
+	callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactRemoved, pack.al);
 }
 
 void ApplyClientNetPackVisitor::visitMoveArtifact(MoveArtifact & pack)
@@ -286,9 +286,9 @@ void ApplyClientNetPackVisitor::visitMoveArtifact(MoveArtifact & pack)
 			callInterfaceIfPresent(cl, player, &IGameEventsReceiver::askToAssembleArtifact, pack.dst);
 	};
 
-	moveArtifact(pack.src.owningPlayer());
-	if(pack.src.owningPlayer() != pack.dst.owningPlayer())
-		moveArtifact(pack.dst.owningPlayer());
+	moveArtifact(cl.getOwner(pack.src.artHolder));
+	if(cl.getOwner(pack.src.artHolder) != cl.getOwner(pack.dst.artHolder))
+		moveArtifact(cl.getOwner(pack.dst.artHolder));
 
 	cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings
 }
@@ -306,8 +306,8 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack)
 		}
 	};
 
-	auto srcOwner = std::get<ConstTransitivePtr<CGHeroInstance>>(pack.srcArtHolder)->tempOwner;
-	auto dstOwner = std::get<ConstTransitivePtr<CGHeroInstance>>(pack.dstArtHolder)->tempOwner;
+	auto srcOwner = cl.getOwner(pack.srcArtHolder);
+	auto dstOwner = cl.getOwner(pack.dstArtHolder);
 
 	// Begin a session of bulk movement of arts. It is not necessary but useful for the client optimization.
 	callInterfaceIfPresent(cl, srcOwner, &IGameEventsReceiver::bulkArtMovementStart, pack.artsPack0.size() + pack.artsPack1.size());
@@ -321,14 +321,14 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack)
 
 void ApplyClientNetPackVisitor::visitAssembledArtifact(AssembledArtifact & pack)
 {
-	callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactAssembled, pack.al);
+	callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactAssembled, pack.al);
 
 	cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings
 }
 
 void ApplyClientNetPackVisitor::visitDisassembledArtifact(DisassembledArtifact & pack)
 {
-	callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactDisassembled, pack.al);
+	callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactDisassembled, pack.al);
 
 	cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings
 }
@@ -604,7 +604,7 @@ void ApplyClientNetPackVisitor::visitSetHeroesInTown(SetHeroesInTown & pack)
 void ApplyClientNetPackVisitor::visitHeroRecruited(HeroRecruited & pack)
 {
 	CGHeroInstance *h = gs.map->heroesOnMap.back();
-	if(h->subID != pack.hid)
+	if(h->getHeroType() != pack.hid)
 	{
 		logNetwork->error("Something wrong with hero recruited!");
 	}

+ 17 - 13
client/adventureMap/CInfoBar.cpp

@@ -376,47 +376,51 @@ void CInfoBar::pushComponents(const std::vector<Component> & components, std::st
 		std::array<std::pair<std::vector<Component>, int>, 10> reward_map;
 		for(const auto & c : components)
 		{
-			switch(c.id)
+			switch(c.type)
 			{
-				case Component::EComponentType::PRIM_SKILL:
-				case Component::EComponentType::EXPERIENCE: 
+				case ComponentType::PRIM_SKILL:
+				case ComponentType::EXPERIENCE:
+				case ComponentType::LEVEL:
+				case ComponentType::MANA:
 					reward_map.at(0).first.push_back(c);
 					reward_map.at(0).second = 8; //At most 8, cannot be more
 					break;
-				case Component::EComponentType::SEC_SKILL:
+				case ComponentType::SEC_SKILL:
 					reward_map.at(1).first.push_back(c);
 					reward_map.at(1).second = 4; //At most 4
 					break;
-				case Component::EComponentType::SPELL: 
+				case ComponentType::SPELL:
 					reward_map.at(2).first.push_back(c);
 					reward_map.at(2).second = 4; //At most 4
 					break;
-				case Component::EComponentType::ARTIFACT:
+				case ComponentType::ARTIFACT:
+				case ComponentType::SPELL_SCROLL:
 					reward_map.at(3).first.push_back(c);
 					reward_map.at(3).second = 4; //At most 4, too long names
 					break;
-				case Component::EComponentType::CREATURE:
+				case ComponentType::CREATURE:
 					reward_map.at(4).first.push_back(c);
 					reward_map.at(4).second = 4; //At most 4, too long names
 					break;
-				case Component::EComponentType::RESOURCE:
+				case ComponentType::RESOURCE:
+				case ComponentType::RESOURCE_PER_DAY:
 					reward_map.at(5).first.push_back(c);
 					reward_map.at(5).second = 7; //At most 7
 					break;
-				case Component::EComponentType::MORALE: 
-				case Component::EComponentType::LUCK:
+				case ComponentType::MORALE:
+				case ComponentType::LUCK:
 					reward_map.at(6).first.push_back(c);
 					reward_map.at(6).second = 2; //At most 2 - 1 for morale + 1 for luck
 					break;
-				case Component::EComponentType::BUILDING:
+				case ComponentType::BUILDING:
 					reward_map.at(7).first.push_back(c);
 					reward_map.at(7).second = 1; //At most 1 - only large icons available AFAIK
 					break;
-				case Component::EComponentType::HERO_PORTRAIT:
+				case ComponentType::HERO_PORTRAIT:
 					reward_map.at(8).first.push_back(c);
 					reward_map.at(8).second = 1; //I do not think than we even can get more than 1 hero
 					break;
-				case Component::EComponentType::FLAG:
+				case ComponentType::FLAG:
 					reward_map.at(9).first.push_back(c);
 					reward_map.at(9).second = 1; //I do not think than we even can get more than 1 player in notification
 					break;

+ 1 - 1
client/battle/BattleActionsController.cpp

@@ -905,7 +905,7 @@ void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterS
 const spells::Caster * BattleActionsController::getCurrentSpellcaster() const
 {
 	if (heroSpellToCast)
-		return owner.getActiveHero();
+		return owner.currentHero();
 	else
 		return owner.stacksController->getActiveStack();
 }

+ 10 - 2
client/battle/BattleObstacleController.cpp

@@ -27,6 +27,7 @@
 #include "../../CCallback.h"
 #include "../../lib/battle/CObstacleInstance.h"
 #include "../../lib/ObstacleHandler.h"
+#include "../../lib/serializer/JsonDeserializer.h"
 
 BattleObstacleController::BattleObstacleController(BattleInterface & owner):
 	owner(owner),
@@ -77,7 +78,14 @@ void BattleObstacleController::obstacleRemoved(const std::vector<ObstacleChanges
 			continue;
 		}
 
-		auto animation = GH.renderHandler().loadAnimation(AnimationPath::fromJson(obstacle["appearAnimation"]));
+		AnimationPath animationPath;
+		JsonDeserializer ser(nullptr, obstacle);
+		ser.serializeStruct("appearAnimation", animationPath);
+
+		if(animationPath.empty())
+			continue;
+
+		auto animation = GH.renderHandler().loadAnimation(animationPath);
 		animation->preload();
 
 		auto first = animation->getImage(0, 0);
@@ -88,7 +96,7 @@ void BattleObstacleController::obstacleRemoved(const std::vector<ObstacleChanges
 		// -> if we know how to blit obstacle, let's blit the effect in the same place
 		Point whereTo = getObstaclePosition(first, obstacle);
 		//AFAIK, in H3 there is no sound of obstacle removal
-		owner.stacksController->addNewAnim(new EffectAnimation(owner, AnimationPath::fromJson(obstacle["appearAnimation"]), whereTo, obstacle["position"].Integer(), 0, true));
+		owner.stacksController->addNewAnim(new EffectAnimation(owner, animationPath, whereTo, obstacle["position"].Integer(), 0, true));
 
 		obstacleAnimations.erase(oi.id);
 		//so when multiple obstacles are removed, they show up one after another

+ 1 - 1
client/lobby/CBonusSelection.cpp

@@ -161,7 +161,7 @@ void CBonusSelection::createBonusesIcons()
 			break;
 		case CampaignBonusType::BUILDING:
 		{
-			int faction = -1;
+			FactionID faction;
 			for(auto & elem : CSH->si->playerInfos)
 			{
 				if(elem.second.isControlledByHuman())

+ 4 - 2
client/lobby/CSelectionBase.cpp

@@ -209,7 +209,6 @@ void InfoCard::changeSelection()
 		return;
 
 	labelSaveDate->setText(mapInfo->date);
-	labelMapSize->setText(std::to_string(mapInfo->mapHeader->width) + "x" + std::to_string(mapInfo->mapHeader->height));
 	mapName->setText(mapInfo->getNameTranslated());
 	mapDescription->setText(mapInfo->getDescriptionTranslated());
 
@@ -220,8 +219,11 @@ void InfoCard::changeSelection()
 	if(SEL->screenType == ESelectionScreen::campaignList)
 		return;
 
-	iconsMapSizes->setFrame(mapInfo->getMapSizeIconId());
 	const CMapHeader * header = mapInfo->mapHeader.get();
+
+	labelMapSize->setText(std::to_string(header->width) + "x" + std::to_string(header->height));
+	iconsMapSizes->setFrame(mapInfo->getMapSizeIconId());
+
 	iconsVictoryCondition->setFrame(header->victoryIconIndex);
 	labelVictoryConditionText->setText(header->victoryMessage.toString());
 	iconsLossCondition->setFrame(header->defeatIconIndex);

+ 1 - 1
client/lobby/OptionsTab.cpp

@@ -571,7 +571,7 @@ void OptionsTab::CPlayerOptionTooltipBox::genTownWindow()
 	for(auto & elem : town->creatures)
 	{
 		if(!elem.empty())
-			components.push_back(std::make_shared<CComponent>(CComponent::creature, elem.front(), 0, CComponent::tiny));
+			components.push_back(std::make_shared<CComponent>(ComponentType::CREATURE, elem.front(), 0, CComponent::tiny));
 	}
 	boxAssociatedCreatures = std::make_shared<CComponentBox>(components, Rect(10, 140, pos.w - 20, 140));
 }

+ 2 - 3
client/mainmenu/CHighScoreScreen.cpp

@@ -252,7 +252,7 @@ int CHighScoreInputScreen::addEntry(std::string text) {
 	auto sortFunctor = [](const JsonNode & left, const JsonNode & right)
 	{
 		if(left["points"].Integer() == right["points"].Integer())
-			return left["posFlag"].Integer() > right["posFlag"].Integer();
+			return left["posFlag"].Bool() > right["posFlag"].Bool();
 		return left["points"].Integer() > right["points"].Integer();
 	};
 
@@ -325,7 +325,6 @@ void CHighScoreInputScreen::deactivate()
 {
 	CCS->videoh->close();
 	CCS->soundh->stopSound(videoSoundHandle);
-	CIntObject::deactivate();
 }
 
 void CHighScoreInputScreen::clickPressed(const Point & cursorPosition)
@@ -361,7 +360,7 @@ void CHighScoreInputScreen::keyPressed(EShortcut key)
 }
 
 CHighScoreInput::CHighScoreInput(std::string playerName, std::function<void(std::string text)> readyCB)
-	: CWindowObject(0, ImagePath::builtin("HIGHNAME")), ready(readyCB)
+	: CWindowObject(NEEDS_ANIMATED_BACKGROUND, ImagePath::builtin("HIGHNAME")), ready(readyCB)
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 

+ 1 - 5
client/mapView/MapRendererContext.cpp

@@ -427,11 +427,7 @@ size_t MapRendererWorldViewContext::overlayImageIndex(const int3 & coordinates)
 		if(!object->visitableAt(coordinates.x, coordinates.y))
 			continue;
 
-		ObjectPosInfo info;
-		info.pos = coordinates;
-		info.id = object->ID;
-		info.subId = object->subID;
-		info.owner = object->tempOwner;
+		ObjectPosInfo info(object);
 
 		size_t iconIndex = selectOverlayImageForObject(info);
 

+ 11 - 3
client/render/CAnimation.cpp

@@ -204,11 +204,19 @@ void CAnimation::printError(size_t frame, size_t group, std::string type) const
 
 CAnimation::CAnimation(const AnimationPath & Name):
 	name(boost::starts_with(Name.getName(), "SPRITES") ? Name : Name.addPrefix("SPRITES/")),
-	preloaded(false),
-	defFile()
+	preloaded(false)
 {
 	if(CResourceHandler::get()->existsResource(name))
-		defFile = std::make_shared<CDefFile>(name);
+	{
+		try
+		{
+			defFile = std::make_shared<CDefFile>(name);
+		}
+		catch ( const std::runtime_error & e)
+		{
+			logAnim->error("Def file %s failed to load! Reason: %s", Name.getOriginalName(), e.what());
+		}
+	}
 
 	init();
 

+ 10 - 10
client/widgets/CArtifactHolder.cpp

@@ -49,7 +49,7 @@ void CArtPlace::setInternals(const CArtifactInstance * artInst)
 		if(settings["general"]["enableUiEnhancements"].Bool())
 		{
 			imageIndex = spellID.num;
-			if(baseType != CComponent::spell)
+			if(component.type != ComponentType::SPELL_SCROLL)
 			{
 				image->setScale(Point(pos.w, 34));
 				image->setAnimationPath(AnimationPath::builtin("spellscr"), imageIndex);
@@ -57,21 +57,20 @@ void CArtPlace::setInternals(const CArtifactInstance * artInst)
 			}
 		}
 		// Add spell component info (used to provide a pic in r-click popup)
-		baseType = CComponent::spell;
-		type = spellID;
+		component.type = ComponentType::SPELL_SCROLL;
+		component.subType = spellID;
 	}
 	else
 	{
-		if(settings["general"]["enableUiEnhancements"].Bool() && baseType != CComponent::artifact)
+		if(settings["general"]["enableUiEnhancements"].Bool() && component.type != ComponentType::ARTIFACT)
 		{
 			image->setScale(Point());
 			image->setAnimationPath(AnimationPath::builtin("artifact"), imageIndex);
 			image->moveTo(Point(pos.x, pos.y));
 		}
-		baseType = CComponent::artifact;
-		type = artInst->getTypeId();
+		component.type = ComponentType::ARTIFACT;
+		component.subType = artInst->getTypeId();
 	}
-	bonusValue = 0;
 	image->enable();
 	text = artInst->getDescription();
 }
@@ -121,10 +120,11 @@ void CCommanderArtPlace::returnArtToHeroCallback()
 	}
 	else
 	{
-		ArtifactLocation src(commanderOwner->commander.get(), artifactPos);
-		ArtifactLocation dst(commanderOwner, freeSlot);
+		ArtifactLocation src(commanderOwner->id, artifactPos);
+		src.creature = SlotID::COMMANDER_SLOT_PLACEHOLDER;
+		ArtifactLocation dst(commanderOwner->id, freeSlot);
 
-		if(ourArt->canBePutAt(dst, true))
+		if(ourArt->canBePutAt(commanderOwner, freeSlot, true))
 		{
 			LOCPLINT->cb->swapArtifacts(src, dst);
 			setArtifact(nullptr);

+ 2 - 2
client/widgets/CArtifactsOfHeroAltar.cpp

@@ -72,7 +72,7 @@ void CArtifactsOfHeroAltar::pickUpArtifact(CHeroArtPlace & artPlace)
 		if(ArtifactUtils::isSlotBackpack(pickedArtFromSlot))
 			pickedArtFromSlot = curHero->getSlotByInstance(art);
 		assert(pickedArtFromSlot != ArtifactPosition::PRE_FIRST);
-		LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, pickedArtFromSlot), ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS));
+		LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, pickedArtFromSlot), ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS));
 	}
 }
 
@@ -89,7 +89,7 @@ void CArtifactsOfHeroAltar::pickedArtMoveToAltar(const ArtifactPosition & slot)
 	if(ArtifactUtils::isSlotBackpack(slot) || ArtifactUtils::isSlotEquipment(slot) || slot == ArtifactPosition::TRANSITION_POS)
 	{
 		assert(curHero->getSlot(slot)->getArt());
-		LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, slot), ArtifactLocation(curHero, pickedArtFromSlot));
+		LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, slot), ArtifactLocation(curHero->id, pickedArtFromSlot));
 		pickedArtFromSlot = ArtifactPosition::PRE_FIRST;
 	}
 }

+ 2 - 2
client/widgets/CArtifactsOfHeroBackpack.cpp

@@ -80,8 +80,8 @@ void CArtifactsOfHeroBackpack::swapArtifacts(const ArtifactLocation & srcLoc, co
 
 void CArtifactsOfHeroBackpack::pickUpArtifact(CHeroArtPlace & artPlace)
 {
-	LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, artPlace.slot),
-		ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS));
+	LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, artPlace.slot),
+		ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS));
 }
 
 void CArtifactsOfHeroBackpack::scrollBackpack(int offset)

+ 3 - 3
client/widgets/CArtifactsOfHeroBase.cpp

@@ -39,11 +39,11 @@ void CArtifactsOfHeroBase::putBackPickedArtifact()
 		auto slot = ArtifactUtils::getArtAnyPosition(curHero, curHero->artifactsTransitionPos.begin()->artifact->getTypeId());
 		if(slot == ArtifactPosition::PRE_FIRST)
 		{
-			LOCPLINT->cb->eraseArtifactByClient(ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS));
+			LOCPLINT->cb->eraseArtifactByClient(ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS));
 		}
 		else
 		{
-			LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS), ArtifactLocation(curHero, slot));
+			LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS), ArtifactLocation(curHero->id, slot));
 		}
 	}
 	if(putBackPickedArtCallback)
@@ -178,7 +178,7 @@ void CArtifactsOfHeroBase::scrollBackpackForArtSet(int offset, const CArtifactSe
 void CArtifactsOfHeroBase::markPossibleSlots(const CArtifactInstance * art, bool assumeDestRemoved)
 {
 	for(auto artPlace : artWorn)
-		artPlace.second->selectSlot(art->artType->canBePutAt(curHero, artPlace.second->slot, assumeDestRemoved));
+		artPlace.second->selectSlot(art->canBePutAt(curHero, artPlace.second->slot, assumeDestRemoved));
 }
 
 void CArtifactsOfHeroBase::unmarkSlots()

+ 3 - 2
client/widgets/CArtifactsOfHeroKingdom.cpp

@@ -13,6 +13,7 @@
 #include "Buttons.h"
 
 #include "../CPlayerInterface.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
 
 #include "../../CCallback.h"
 #include "../../lib/networkPacks/ArtifactLocation.h"
@@ -56,7 +57,7 @@ void CArtifactsOfHeroKingdom::swapArtifacts(const ArtifactLocation & srcLoc, con
 
 void CArtifactsOfHeroKingdom::pickUpArtifact(CHeroArtPlace & artPlace)
 {
-	LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, artPlace.slot),
-		ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS));
+	LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, artPlace.slot),
+		ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS));
 }
 

+ 3 - 2
client/widgets/CArtifactsOfHeroMain.cpp

@@ -11,6 +11,7 @@
 #include "CArtifactsOfHeroMain.h"
 
 #include "../CPlayerInterface.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
 
 #include "../../CCallback.h"
 #include "../../lib/networkPacks/ArtifactLocation.h"
@@ -36,6 +37,6 @@ void CArtifactsOfHeroMain::swapArtifacts(const ArtifactLocation & srcLoc, const
 
 void CArtifactsOfHeroMain::pickUpArtifact(CHeroArtPlace & artPlace)
 {
-	LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero, artPlace.slot),
-		ArtifactLocation(curHero, ArtifactPosition::TRANSITION_POS));
+	LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, artPlace.slot),
+		ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS));
 }

+ 183 - 133
client/widgets/CComponent.cpp

@@ -39,41 +39,35 @@
 #include "../../lib/CArtHandler.h"
 #include "../../lib/CArtifactInstance.h"
 
-CComponent::CComponent(Etype Type, int Subtype, int Val, ESize imageSize, EFonts font):
-	perDay(false)
+CComponent::CComponent(ComponentType Type, ComponentSubType Subtype, std::optional<int32_t> Val, ESize imageSize, EFonts font)
 {
 	init(Type, Subtype, Val, imageSize, font, "");
 }
 
-CComponent::CComponent(Etype Type, int Subtype, std::string Val, ESize imageSize, EFonts font):
-	perDay(false)
+CComponent::CComponent(ComponentType Type, ComponentSubType Subtype, const std::string & Val, ESize imageSize, EFonts font)
 {
-	init(Type, Subtype, 0, imageSize, font, Val);
+	init(Type, Subtype, std::nullopt, imageSize, font, Val);
 }
 
 CComponent::CComponent(const Component & c, ESize imageSize, EFonts font)
-	: perDay(false)
 {
-	if(c.id == Component::EComponentType::RESOURCE && c.when==-1)
-		perDay = true;
-
-	init((Etype)c.id, c.subtype, c.val, imageSize, font);
+	init(c.type, c.subType, c.value, imageSize, font, "");
 }
 
-void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts fnt, std::string ValText)
+void CComponent::init(ComponentType Type, ComponentSubType Subtype, std::optional<int32_t> Val, ESize imageSize, EFonts fnt, const std::string & ValText)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 
 	addUsedEvents(SHOW_POPUP);
 
-	compType = Type;
-	subtype = Subtype;
-	val = Val;
-	valText = ValText;
+	data.type = Type;
+	data.subType = Subtype;
+	data.value = Val;
+
+	customSubtitle = ValText;
 	size = imageSize;
 	font = fnt;
 
-	assert(compType < typeInvalid);
 	assert(size < sizeInvalid);
 
 	setSurface(getFileName()[size], (int)getIndex());
@@ -94,7 +88,7 @@ void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts
 	if (size < small)
 		max = 30;
 
-	if(Type == Etype::resource && !valText.empty())
+	if(Type == ComponentType::RESOURCE && !ValText.empty())
 		max = 80;
 
 	std::vector<std::string> textLines = CMessage::breakText(getSubtitle(), std::max<int>(max, pos.w), font);
@@ -113,153 +107,209 @@ void CComponent::init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts
 	}
 }
 
-std::vector<AnimationPath> CComponent::getFileName()
+std::vector<AnimationPath> CComponent::getFileName() const
 {
-	static const std::string  primSkillsArr [] = {"PSKIL32",        "PSKIL32",        "PSKIL42",        "PSKILL"};
-	static const std::string  secSkillsArr [] =  {"SECSK32",        "SECSK32",        "SECSKILL",       "SECSK82"};
-	static const std::string  resourceArr [] =   {"SMALRES",        "RESOURCE",       "RESOURCE",       "RESOUR82"};
-	static const std::string  creatureArr [] =   {"CPRSMALL",       "CPRSMALL",       "CPRSMALL",       "TWCRPORT"};
-	static const std::string  artifactArr[]  =   {"Artifact",       "Artifact",       "Artifact",       "Artifact"};
-	static const std::string  spellsArr [] =     {"SpellInt",       "SpellInt",       "SpellInt",       "SPELLSCR"};
-	static const std::string  moraleArr [] =     {"IMRL22",         "IMRL30",         "IMRL42",         "imrl82"};
-	static const std::string  luckArr [] =       {"ILCK22",         "ILCK30",         "ILCK42",         "ilck82"};
-	static const std::string  heroArr [] =       {"PortraitsSmall", "PortraitsSmall", "PortraitsSmall", "PortraitsLarge"};
-	static const std::string  flagArr [] =       {"CREST58",        "CREST58",        "CREST58",        "CREST58"};
-
-	auto gen = [](const std::string * arr) -> std::vector<AnimationPath>
+	static const std::array<std::string, 4>  primSkillsArr = {"PSKIL32",        "PSKIL32",        "PSKIL42",        "PSKILL"};
+	static const std::array<std::string, 4>  secSkillsArr =  {"SECSK32",        "SECSK32",        "SECSKILL",       "SECSK82"};
+	static const std::array<std::string, 4>  resourceArr =   {"SMALRES",        "RESOURCE",       "RESOURCE",       "RESOUR82"};
+	static const std::array<std::string, 4>  creatureArr =   {"CPRSMALL",       "CPRSMALL",       "CPRSMALL",       "TWCRPORT"};
+	static const std::array<std::string, 4>  artifactArr =   {"Artifact",       "Artifact",       "Artifact",       "Artifact"};
+	static const std::array<std::string, 4>  spellsArr =     {"SpellInt",       "SpellInt",       "SpellInt",       "SPELLSCR"};
+	static const std::array<std::string, 4>  moraleArr =     {"IMRL22",         "IMRL30",         "IMRL42",         "imrl82"};
+	static const std::array<std::string, 4>  luckArr =       {"ILCK22",         "ILCK30",         "ILCK42",         "ilck82"};
+	static const std::array<std::string, 4>  heroArr =       {"PortraitsSmall", "PortraitsSmall", "PortraitsSmall", "PortraitsLarge"};
+	static const std::array<std::string, 4>  flagArr =       {"CREST58",        "CREST58",        "CREST58",        "CREST58"};
+
+	auto gen = [](const std::array<std::string, 4> & arr) -> std::vector<AnimationPath>
 	{
 		return { AnimationPath::builtin(arr[0]), AnimationPath::builtin(arr[1]), AnimationPath::builtin(arr[2]), AnimationPath::builtin(arr[3]) };
 	};
 
-	switch(compType)
+	switch(data.type)
 	{
-	case primskill:  return gen(primSkillsArr);
-	case secskill:   return gen(secSkillsArr);
-	case resource:   return gen(resourceArr);
-	case creature:   return gen(creatureArr);
-	case artifact:   return gen(artifactArr);
-	case experience: return gen(primSkillsArr);
-	case spell:      return gen(spellsArr);
-	case morale:     return gen(moraleArr);
-	case luck:       return gen(luckArr);
-	case building:   return std::vector<AnimationPath>(4, (*CGI->townh)[subtype]->town->clientInfo.buildingsIcons);
-	case hero:       return gen(heroArr);
-	case flag:       return gen(flagArr);
+		case ComponentType::PRIM_SKILL:
+		case ComponentType::EXPERIENCE:
+		case ComponentType::MANA:
+		case ComponentType::LEVEL:
+			return gen(primSkillsArr);
+		case ComponentType::SEC_SKILL:
+			return gen(secSkillsArr);
+		case ComponentType::RESOURCE:
+		case ComponentType::RESOURCE_PER_DAY:
+			return gen(resourceArr);
+		case ComponentType::CREATURE:
+			return gen(creatureArr);
+		case ComponentType::ARTIFACT:
+			return gen(artifactArr);
+		case ComponentType::SPELL_SCROLL:
+		case ComponentType::SPELL:
+			return gen(spellsArr);
+		case ComponentType::MORALE:
+			return gen(moraleArr);
+		case ComponentType::LUCK:
+			return gen(luckArr);
+		case ComponentType::BUILDING:
+			return std::vector<AnimationPath>(4, (*CGI->townh)[data.subType.as<BuildingTypeUniqueID>().getFaction()]->town->clientInfo.buildingsIcons);
+		case ComponentType::HERO_PORTRAIT:
+			return gen(heroArr);
+		case ComponentType::FLAG:
+			return gen(flagArr);
+		default:
+			assert(0);
+			return {};
 	}
-	assert(0);
-	return {};
 }
 
-size_t CComponent::getIndex()
+size_t CComponent::getIndex() const
 {
-	switch(compType)
+	switch(data.type)
 	{
-	case primskill:  return subtype;
-	case secskill:   return subtype*3 + 3 + val - 1;
-	case resource:   return subtype;
-	case creature:   return CGI->creatures()->getByIndex(subtype)->getIconIndex();
-	case artifact:   return CGI->artifacts()->getByIndex(subtype)->getIconIndex();
-	case experience: return 4;
-	case spell:      return (size < large) ? subtype + 1 : subtype;
-	case morale:     return val+3;
-	case luck:       return val+3;
-	case building:   return val;
-	case hero:       return CGI->heroTypes()->getByIndex(subtype)->getIconIndex();
-	case flag:       return subtype;
+		case ComponentType::PRIM_SKILL:
+			return data.subType.getNum();
+		case ComponentType::EXPERIENCE:
+		case ComponentType::LEVEL:
+			return 4; // for whatever reason, in H3 experience icon is located in primary skills icons
+		case ComponentType::MANA:
+			return 5; // for whatever reason, in H3 mana points icon is located in primary skills icons
+		case ComponentType::SEC_SKILL:
+			return data.subType.getNum() * 3 + 3 + data.value.value_or(0) - 1;
+		case ComponentType::RESOURCE:
+		case ComponentType::RESOURCE_PER_DAY:
+			return data.subType.getNum();
+		case ComponentType::CREATURE:
+			return CGI->creatures()->getById(data.subType.as<CreatureID>())->getIconIndex();
+		case ComponentType::ARTIFACT:
+			return CGI->artifacts()->getById(data.subType.as<ArtifactID>())->getIconIndex();
+		case ComponentType::SPELL_SCROLL:
+		case ComponentType::SPELL:
+			return (size < large) ? data.subType.getNum() + 1 : data.subType.getNum();
+		case ComponentType::MORALE:
+			return data.value.value_or(0) + 3;
+		case ComponentType::LUCK:
+			return data.value.value_or(0) + 3;
+		case ComponentType::BUILDING:
+			return data.subType.as<BuildingTypeUniqueID>().getBuilding();
+		case ComponentType::HERO_PORTRAIT:
+			return CGI->heroTypes()->getById(data.subType.as<HeroTypeID>())->getIconIndex();
+		case ComponentType::FLAG:
+			return data.subType.getNum();
+		default:
+			assert(0);
+			return 0;
 	}
-	assert(0);
-	return 0;
 }
 
-std::string CComponent::getDescription()
+std::string CComponent::getDescription() const
 {
-	switch(compType)
-	{
-	case primskill:  return (subtype < 4)? CGI->generaltexth->arraytxt[2+subtype] //Primary skill
-										 : CGI->generaltexth->allTexts[149]; //mana
-	case secskill:   return CGI->skillh->getByIndex(subtype)->getDescriptionTranslated(val);
-	case resource:   return CGI->generaltexth->allTexts[242];
-	case creature:   return "";
-	case artifact:
+	switch(data.type)
 	{
-		auto artID = ArtifactID(subtype);
-		auto description = VLC->arth->objects[artID]->getDescriptionTranslated();
-		if(artID == ArtifactID::SPELL_SCROLL)
+		case ComponentType::PRIM_SKILL:
+			return CGI->generaltexth->arraytxt[2+data.subType.getNum()];
+		case ComponentType::EXPERIENCE:
+		case ComponentType::LEVEL:
+			return CGI->generaltexth->allTexts[241];
+		case ComponentType::MANA:
+			return CGI->generaltexth->allTexts[149];
+		case ComponentType::SEC_SKILL:
+			return CGI->skillh->getByIndex(data.subType.getNum())->getDescriptionTranslated(data.value.value_or(0));
+		case ComponentType::RESOURCE:
+		case ComponentType::RESOURCE_PER_DAY:
+			return CGI->generaltexth->allTexts[242];
+		case ComponentType::CREATURE:
+			return "";
+		case ComponentType::ARTIFACT:
+			return VLC->artifacts()->getById(data.subType.as<ArtifactID>())->getDescriptionTranslated();
+		case ComponentType::SPELL_SCROLL:
 		{
-			ArtifactUtils::insertScrrollSpellName(description, SpellID(val));
+			auto description = VLC->arth->objects[ArtifactID::SPELL_SCROLL]->getDescriptionTranslated();
+			ArtifactUtils::insertScrrollSpellName(description, data.subType.as<SpellID>());
+			return description;
 		}
-		return description;
-	}
-	case experience: return CGI->generaltexth->allTexts[241];
-	case spell:      return (*CGI->spellh)[subtype]->getDescriptionTranslated(val);
-	case morale:     return CGI->generaltexth->heroscrn[ 4 - (val>0) + (val<0)];
-	case luck:       return CGI->generaltexth->heroscrn[ 7 - (val>0) + (val<0)];
-	case building:   return (*CGI->townh)[subtype]->town->buildings[BuildingID(val)]->getDescriptionTranslated();
-	case hero:       return "";
-	case flag:       return "";
+		case ComponentType::SPELL:
+			return VLC->spells()->getById(data.subType.as<SpellID>())->getDescriptionTranslated(data.value.value_or(0));
+		case ComponentType::MORALE:
+			return CGI->generaltexth->heroscrn[ 4 - (data.value.value_or(0)>0) + (data.value.value_or(0)<0)];
+		case ComponentType::LUCK:
+			return CGI->generaltexth->heroscrn[ 7 - (data.value.value_or(0)>0) + (data.value.value_or(0)<0)];
+		case ComponentType::BUILDING:
+		{
+			auto index = data.subType.as<BuildingTypeUniqueID>();
+			return (*CGI->townh)[index.getFaction()]->town->buildings[index.getBuilding()]->getDescriptionTranslated();
+		}
+		case ComponentType::HERO_PORTRAIT:
+			return "";
+		case ComponentType::FLAG:
+			return "";
+		default:
+			assert(0);
+			return "";
 	}
-	assert(0);
-	return "";
 }
 
-std::string CComponent::getSubtitle()
+std::string CComponent::getSubtitle() const
 {
-	if(!perDay)
-		return getSubtitleInternal();
-
-	std::string ret = CGI->generaltexth->allTexts[3];
-	boost::replace_first(ret, "%d", getSubtitleInternal());
-	return ret;
-}
+	if (!customSubtitle.empty())
+		return customSubtitle;
 
-std::string CComponent::getSubtitleInternal()
-{
-	//FIXME: some of these are horrible (e.g creature)
-	switch(compType)
+	switch(data.type)
 	{
-	case primskill:  return boost::str(boost::format("%+d %s") % val % (subtype < 4 ? CGI->generaltexth->primarySkillNames[subtype] : CGI->generaltexth->allTexts[387]));
-	case secskill:   return CGI->generaltexth->levels[val-1] + "\n" + CGI->skillh->getByIndex(subtype)->getNameTranslated();
-	case resource:   return valText.empty() ? std::to_string(val) : valText;
-	case creature:
-		{
-			auto creature = CGI->creh->getByIndex(subtype);
-			if ( val )
-				return std::to_string(val) + " " + (val > 1 ? creature->getNamePluralTranslated() : creature->getNameSingularTranslated());
+		case ComponentType::PRIM_SKILL:
+			if (data.value)
+				return boost::str(boost::format("%+d %s") % data.value.value_or(0) % CGI->generaltexth->primarySkillNames[data.subType.getNum()]);
 			else
-				return val > 1 ? creature->getNamePluralTranslated() : creature->getNameSingularTranslated();
+				return CGI->generaltexth->primarySkillNames[data.subType.getNum()];
+		case ComponentType::EXPERIENCE:
+			return std::to_string(data.value.value_or(0));
+		case ComponentType::LEVEL:
+		{
+			std::string level = CGI->generaltexth->allTexts[442];
+			boost::replace_first(level, "1", std::to_string(data.value.value_or(0)));
+			return level;
 		}
-	case artifact:   return CGI->artifacts()->getByIndex(subtype)->getNameTranslated();
-	case experience:
+		case ComponentType::MANA:
+			return boost::str(boost::format("%+d %s") % data.value.value_or(0) % CGI->generaltexth->allTexts[387]);
+		case ComponentType::SEC_SKILL:
+			return CGI->generaltexth->levels[data.value.value_or(0)-1] + "\n" + CGI->skillh->getById(data.subType.as<SecondarySkill>())->getNameTranslated();
+		case ComponentType::RESOURCE:
+			return std::to_string(data.value.value_or(0));
+		case ComponentType::RESOURCE_PER_DAY:
+			return boost::str(boost::format(CGI->generaltexth->allTexts[3]) % data.value.value_or(0));
+		case ComponentType::CREATURE:
 		{
-			if(subtype == 1) //+1 level - tree of knowledge
-			{
-				std::string level = CGI->generaltexth->allTexts[442];
-				boost::replace_first(level, "1", std::to_string(val));
-				return level;
-			}
+			auto creature = CGI->creh->getById(data.subType.as<CreatureID>());
+			if(data.value)
+				return std::to_string(*data.value) + " " + (*data.value > 1 ? creature->getNamePluralTranslated() : creature->getNameSingularTranslated());
 			else
-			{
-				return std::to_string(val); //amount of experience OR level required for seer hut;
-			}
+				return creature->getNamePluralTranslated();
 		}
-	case spell:      return CGI->spells()->getByIndex(subtype)->getNameTranslated();
-	case morale:     return "";
-	case luck:       return "";
-	case building:
-		{
-			auto building = (*CGI->townh)[subtype]->town->buildings[BuildingID(val)];
-			if(!building)
+		case ComponentType::ARTIFACT:
+			return CGI->artifacts()->getById(data.subType.as<ArtifactID>())->getNameTranslated();
+		case ComponentType::SPELL_SCROLL:
+		case ComponentType::SPELL:
+			return CGI->spells()->getById(data.subType.as<SpellID>())->getNameTranslated();
+		case ComponentType::MORALE:
+			return "";
+		case ComponentType::LUCK:
+			return "";
+		case ComponentType::BUILDING:
 			{
-				logGlobal->error("Town of faction %s has no building #%d", (*CGI->townh)[subtype]->town->faction->getNameTranslated(), val);
-				return (boost::format("Missing building #%d") % val).str();
+				auto index = data.subType.as<BuildingTypeUniqueID>();
+				auto building = (*CGI->townh)[index.getFaction()]->town->buildings[index.getBuilding()];
+				if(!building)
+				{
+					logGlobal->error("Town of faction %s has no building #%d", (*CGI->townh)[index.getFaction()]->town->faction->getNameTranslated(), index.getBuilding().getNum());
+					return (boost::format("Missing building #%d") % index.getBuilding().getNum()).str();
+				}
+				return building->getNameTranslated();
 			}
-			return building->getNameTranslated();
-		}
-	case hero:       return "";
-	case flag:       return CGI->generaltexth->capColors[subtype];
+		case ComponentType::HERO_PORTRAIT:
+			return "";
+		case ComponentType::FLAG:
+			return CGI->generaltexth->capColors[data.subType.as<PlayerColor>().getNum()];
+		default:
+			assert(0);
+			return "";
 	}
-	logGlobal->error("Invalid CComponent type: %d", (int)compType);
-	return "";
 }
 
 void CComponent::setSurface(const AnimationPath & defName, int imgPos)
@@ -299,7 +349,7 @@ CSelectableComponent::CSelectableComponent(const Component &c, std::function<voi
 	init();
 }
 
-CSelectableComponent::CSelectableComponent(Etype Type, int Sub, int Val, ESize imageSize, std::function<void()> OnSelect):
+CSelectableComponent::CSelectableComponent(ComponentType Type, ComponentSubType Sub, int Val, ESize imageSize, std::function<void()> OnSelect):
 	CComponent(Type,Sub,Val, imageSize),onSelect(OnSelect)
 {
 	setRedrawParent(true);

+ 11 - 20
client/widgets/CComponent.h

@@ -12,6 +12,7 @@
 #include "../gui/CIntObject.h"
 #include "../render/EFont.h"
 #include "../../lib/filesystem/ResourcePath.h"
+#include "../../lib/networkPacks/Component.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -26,11 +27,6 @@ class CLabel;
 class CComponent : public virtual CIntObject
 {
 public:
-	enum Etype
-	{
-		primskill, secskill, resource, creature, artifact, experience, spell, morale, luck, building, hero, flag, typeInvalid
-	};
-
 	//NOTE: not all types have exact these sizes or have less than 4 of them. In such cases closest one will be used
 	enum ESize
 	{
@@ -44,29 +40,24 @@ public:
 private:
 	std::vector<std::shared_ptr<CLabel>> lines;
 
-	size_t getIndex();
-	std::vector<AnimationPath> getFileName();
+	size_t getIndex() const;
+	std::vector<AnimationPath> getFileName() const;
 	void setSurface(const AnimationPath & defName, int imgPos);
-	std::string getSubtitleInternal();
 
-	void init(Etype Type, int Subtype, int Val, ESize imageSize, EFonts font = FONT_SMALL, std::string ValText="");
+	void init(ComponentType Type, ComponentSubType Subtype, std::optional<int32_t> Val, ESize imageSize, EFonts font, const std::string & ValText);
 
 public:
 	std::shared_ptr<CAnimImage> image;
-
-	Etype compType; //component type
+	Component data;
+	std::string customSubtitle;
 	ESize size; //component size.
 	EFonts font; //Font size of label
-	int subtype; //type-dependant subtype. See getSomething methods for details
-	int val; // value \ strength \ amount of component. See getSomething methods for details
-	std::string valText; // value instead of amount; currently only for resource
-	bool perDay; // add "per day" text to subtitle
 
-	std::string getDescription();
-	std::string getSubtitle();
+	std::string getDescription() const;
+	std::string getSubtitle() const;
 
-	CComponent(Etype Type, int Subtype, int Val = 0, ESize imageSize=large, EFonts font = FONT_SMALL);
-	CComponent(Etype Type, int Subtype, std::string Val, ESize imageSize=large, EFonts font = FONT_SMALL);
+	CComponent(ComponentType Type, ComponentSubType Subtype, std::optional<int32_t> Val = std::nullopt, ESize imageSize=large, EFonts font = FONT_SMALL);
+	CComponent(ComponentType Type, ComponentSubType Subtype, const std::string & Val, ESize imageSize=large, EFonts font = FONT_SMALL);
 	CComponent(const Component &c, ESize imageSize=large, EFonts font = FONT_SMALL);
 
 	void showPopupWindow(const Point & cursorPosition) override; //call-in
@@ -86,7 +77,7 @@ public:
 
 	void clickPressed(const Point & cursorPosition) override; //call-in
 	void clickDouble(const Point & cursorPosition) override; //call-in
-	CSelectableComponent(Etype Type, int Sub, int Val, ESize imageSize=large, std::function<void()> OnSelect = nullptr);
+	CSelectableComponent(ComponentType Type, ComponentSubType Sub, int Val, ESize imageSize=large, std::function<void()> OnSelect = nullptr);
 	CSelectableComponent(const Component & c, std::function<void()> OnSelect = nullptr);
 };
 

+ 8 - 6
client/widgets/CGarrisonInt.cpp

@@ -196,16 +196,18 @@ bool CGarrisonSlot::highlightOrDropArtifact()
 			artSelected = true;
 			if (myStack) // try dropping the artifact only if the slot isn't empty
 			{
-				ArtifactLocation src(srcHero, ArtifactPosition::TRANSITION_POS);
-				ArtifactLocation dst(myStack, ArtifactPosition::CREATURE_SLOT);
-				if(pickedArtInst->canBePutAt(dst, true))
+				ArtifactLocation src(srcHero->id, ArtifactPosition::TRANSITION_POS);
+				ArtifactLocation dst(getObj()->id, ArtifactPosition::CREATURE_SLOT);
+				dst.creature = getSlot();
+
+				if(pickedArtInst->canBePutAt(myStack, ArtifactPosition::CREATURE_SLOT, true))
 				{	//equip clicked stack
-					if(dst.getArt())
+					if(auto dstArt = myStack->getArt(ArtifactPosition::CREATURE_SLOT))
 					{
 						//creature can wear only one active artifact
 						//if we are placing a new one, the old one will be returned to the hero's backpack
-						LOCPLINT->cb->swapArtifacts(dst, ArtifactLocation(srcHero,
-							ArtifactUtils::getArtBackpackPosition(srcHero, dst.getArt()->getTypeId())));
+						LOCPLINT->cb->swapArtifacts(dst, ArtifactLocation(srcHero->id,
+							ArtifactUtils::getArtBackpackPosition(srcHero, dstArt->getTypeId())));
 					}
 					LOCPLINT->cb->swapArtifacts(src, dst);
 				}

+ 6 - 6
client/widgets/CWindowWithArtifacts.cpp

@@ -95,7 +95,7 @@ void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst
 		if(artPlace.getArt()->getTypeId() == ArtifactID::CATAPULT)
 		{
 			// The Catapult must be equipped
-			std::vector<std::shared_ptr<CComponent>> catapult(1, std::make_shared<CComponent>(CComponent::artifact, 3, 0));
+			std::vector<std::shared_ptr<CComponent>> catapult(1, std::make_shared<CComponent>(ComponentType::ARTIFACT, ArtifactID(ArtifactID::CATAPULT)));
 			LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[312], catapult);
 			return false;
 		}
@@ -122,8 +122,8 @@ void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst
 
 				if(pickedArtInst)
 				{
-					auto srcLoc = ArtifactLocation(heroPickedArt, ArtifactPosition::TRANSITION_POS);
-					auto dstLoc = ArtifactLocation(hero, artPlace.slot);
+					auto srcLoc = ArtifactLocation(heroPickedArt->id, ArtifactPosition::TRANSITION_POS);
+					auto dstLoc = ArtifactLocation(hero->id, artPlace.slot);
 
 					if(ArtifactUtils::isSlotBackpack(artPlace.slot))
 					{
@@ -141,7 +141,7 @@ void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst
 						}
 					}
 					// Check if artifact transfer is possible
-					else if(pickedArtInst->canBePutAt(dstLoc, true) && (!artPlace.getArt() || hero->tempOwner == LOCPLINT->playerID))
+					else if(pickedArtInst->canBePutAt(hero, artPlace.slot, true) && (!artPlace.getArt() || hero->tempOwner == LOCPLINT->playerID))
 					{
 						isTransferAllowed = true;
 					}
@@ -270,7 +270,7 @@ void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const
 	// we have a different artifact may look surprising... but it's valid.
 
 	auto pickedArtInst = std::get<const CArtifactInstance*>(curState.value());
-	assert(!pickedArtInst || destLoc.isHolder(std::get<const CGHeroInstance*>(curState.value())));
+	assert(!pickedArtInst || destLoc.artHolder == std::get<const CGHeroInstance*>(curState.value())->id);
 
 	auto artifactMovedBody = [this, withRedraw, &destLoc, &pickedArtInst](auto artSetWeak) -> void
 	{
@@ -316,7 +316,7 @@ void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const
 			}
 
 			// Make sure the status bar is updated so it does not display old text
-			if(destLoc.getHolderArtSet() == hero)
+			if(destLoc.artHolder == hero->id)
 			{
 				if(auto artPlace = artSetPtr->getArtPlace(destLoc.slot))
 					artPlace->hover(true);

+ 19 - 25
client/widgets/MiscWidgets.cpp

@@ -95,16 +95,16 @@ void LRClickableAreaWTextComp::clickPressed(const Point & cursorPosition)
 	LOCPLINT->showInfoDialog(text, comp);
 }
 
-LRClickableAreaWTextComp::LRClickableAreaWTextComp(const Rect &Pos, int BaseType)
-	: LRClickableAreaWText(Pos), baseType(BaseType), bonusValue(-1)
+LRClickableAreaWTextComp::LRClickableAreaWTextComp(const Rect &Pos, ComponentType BaseType)
+	: LRClickableAreaWText(Pos)
 {
-	type = -1;
+	component.type = BaseType;
 }
 
 std::shared_ptr<CComponent> LRClickableAreaWTextComp::createComponent() const
 {
-	if(baseType >= 0)
-		return std::make_shared<CComponent>(CComponent::Etype(baseType), type, bonusValue);
+	if(component.type != ComponentType::NONE)
+		return std::make_shared<CComponent>(component);
 	else
 		return std::shared_ptr<CComponent>();
 }
@@ -164,17 +164,11 @@ void CHeroArea::hover(bool on)
 void LRClickableAreaOpenTown::clickPressed(const Point & cursorPosition)
 {
 	if(town)
-	{
 		LOCPLINT->openTownWindow(town);
-		if ( type == 2 )
-			LOCPLINT->castleInt->builds->buildingClicked(BuildingID::VILLAGE_HALL);
-		else if ( type == 3 && town->fortLevel() )
-			LOCPLINT->castleInt->builds->buildingClicked(BuildingID::FORT);
-	}
 }
 
 LRClickableAreaOpenTown::LRClickableAreaOpenTown(const Rect & Pos, const CGTownInstance * Town)
-	: LRClickableAreaWTextComp(Pos, -1), town(Town)
+	: LRClickableAreaWTextComp(Pos), town(Town)
 {
 }
 
@@ -542,20 +536,21 @@ void MoraleLuckBox::set(const AFactionMember * node)
 {
 	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE);
 
-	const int textId[] = {62, 88}; //eg %s \n\n\n {Current Luck Modifiers:}
+	const std::array textId = {62, 88}; //eg %s \n\n\n {Current Luck Modifiers:}
 	const int noneTxtId = 108; //Russian version uses same text for neutral morale\luck
-	const int neutralDescr[] = {60, 86}; //eg {Neutral Morale} \n\n Neutral morale means your armies will neither be blessed with extra attacks or freeze in combat.
-	const int componentType[] = {CComponent::luck, CComponent::morale};
-	const int hoverTextBase[] = {7, 4};
+	const std::array neutralDescr = {60, 86}; //eg {Neutral Morale} \n\n Neutral morale means your armies will neither be blessed with extra attacks or freeze in combat.
+	const std::array componentType = {ComponentType::LUCK, ComponentType::MORALE};
+	const std::array hoverTextBase = {7, 4};
 	TConstBonusListPtr modifierList = std::make_shared<const BonusList>();
-	bonusValue = 0;
+
+	component.value = 0;
 
 	if(node)
-		bonusValue = morale ? node->moraleValAndBonusList(modifierList) : node->luckValAndBonusList(modifierList);
+		component.value = morale ? node->moraleValAndBonusList(modifierList) : node->luckValAndBonusList(modifierList);
 
-	int mrlt = (bonusValue>0)-(bonusValue<0); //signum: -1 - bad luck / morale, 0 - neutral, 1 - good
+	int mrlt = (component.value>0)-(component.value<0); //signum: -1 - bad luck / morale, 0 - neutral, 1 - good
 	hoverText = CGI->generaltexth->heroscrn[hoverTextBase[morale] - mrlt];
-	baseType = componentType[morale];
+	component.type = componentType[morale];
 	text = CGI->generaltexth->arraytxt[textId[morale]];
 	boost::algorithm::replace_first(text,"%s",CGI->generaltexth->arraytxt[neutralDescr[morale]-mrlt]);
 
@@ -563,19 +558,19 @@ void MoraleLuckBox::set(const AFactionMember * node)
 			|| node->getBonusBearer()->hasBonusOfType(BonusType::NON_LIVING)))
 	{
 		text += CGI->generaltexth->arraytxt[113]; //unaffected by morale
-		bonusValue = 0;
+		component.value = 0;
 	}
 	else if(morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_MORALE))
 	{
 		auto noMorale = node->getBonusBearer()->getBonus(Selector::type()(BonusType::NO_MORALE));
 		text += "\n" + noMorale->Description();
-		bonusValue = 0;
+		component.value = 0;
 	}
 	else if (!morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_LUCK))
 	{
 		auto noLuck = node->getBonusBearer()->getBonus(Selector::type()(BonusType::NO_LUCK));
 		text += "\n" + noLuck->Description();
-		bonusValue = 0;
+		component.value = 0;
 	}
 	else
 	{
@@ -595,7 +590,7 @@ void MoraleLuckBox::set(const AFactionMember * node)
 	else
 		imageName = morale ? "IMRL42" : "ILCK42";
 
-	image = std::make_shared<CAnimImage>(AnimationPath::builtin(imageName), bonusValue + 3);
+	image = std::make_shared<CAnimImage>(AnimationPath::builtin(imageName), *component.value + 3);
 	image->moveBy(Point(pos.w/2 - image->pos.w/2, pos.h/2 - image->pos.h/2));//center icon
 }
 
@@ -603,7 +598,6 @@ MoraleLuckBox::MoraleLuckBox(bool Morale, const Rect &r, bool Small)
 	: morale(Morale),
 	small(Small)
 {
-	bonusValue = 0;
 	pos = r + pos.topLeft();
 	defActions = 255-DISPOSE;
 }

+ 5 - 5
client/widgets/MiscWidgets.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "../gui/CIntObject.h"
+#include "../../lib/networkPacks/Component.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -202,13 +203,12 @@ private:
 class LRClickableAreaWTextComp: public LRClickableAreaWText
 {
 public:
-	int type;
-	int baseType;
-	int bonusValue;
+	Component component;
+
 	void clickPressed(const Point & cursorPosition) override;
 	void showPopupWindow(const Point & cursorPosition) override;
 
-	LRClickableAreaWTextComp(const Rect &Pos = Rect(0,0,0,0), int BaseType = -1);
+	LRClickableAreaWTextComp(const Rect &Pos = Rect(0,0,0,0), ComponentType baseType = ComponentType::NONE);
 	std::shared_ptr<CComponent> createComponent() const;
 };
 
@@ -263,4 +263,4 @@ class SimpleLine : public CIntObject
 public:
     SimpleLine(Point pos1, Point pos2, ColorRGBA color);
     void showAll(Canvas & to) override;
-};
+};

+ 26 - 17
client/windows/CCastleInterface.cpp

@@ -157,7 +157,7 @@ void CBuildingRect::showPopupWindow(const Point & cursorPosition)
 	if (bid < BuildingID::DWELL_FIRST)
 	{
 		CRClickPopup::createAndPush(CInfoWindow::genText(bld->getNameTranslated(), bld->getDescriptionTranslated()),
-									std::make_shared<CComponent>(CComponent::building, bld->town->faction->getIndex(), bld->bid));
+									std::make_shared<CComponent>(ComponentType::BUILDING, BuildingTypeUniqueID(bld->town->faction->getId(), bld->bid)));
 	}
 	else
 	{
@@ -860,7 +860,7 @@ void CCastleBuildings::enterBlacksmith(ArtifactID artifactID)
 
 void CCastleBuildings::enterBuilding(BuildingID building)
 {
-	std::vector<std::shared_ptr<CComponent>> comps(1, std::make_shared<CComponent>(CComponent::building, town->subID, building));
+	std::vector<std::shared_ptr<CComponent>> comps(1, std::make_shared<CComponent>(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFaction(), building)));
 	LOCPLINT->showInfoDialog( town->town->buildings.find(building)->second->getDescriptionTranslated(), comps);
 }
 
@@ -920,7 +920,7 @@ void CCastleBuildings::enterToTheQuickRecruitmentWindow()
 
 void CCastleBuildings::enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades)
 {
-	std::vector<std::shared_ptr<CComponent>> comps(1, std::make_shared<CComponent>(CComponent::building,town->subID,building));
+	std::vector<std::shared_ptr<CComponent>> comps(1, std::make_shared<CComponent>(ComponentType::BUILDING, BuildingTypeUniqueID(town->getFaction(), building)));
 	std::string descr = town->town->buildings.find(building)->second->getDescriptionTranslated();
 	std::string hasNotProduced;
 	std::string hasProduced;
@@ -969,9 +969,9 @@ void CCastleBuildings::enterMagesGuild()
 	{
 		const StartInfo *si = LOCPLINT->cb->getStartInfo();
 		// it would be nice to find a way to move this hack to config/mapOverrides.json
-		if(si && si->campState &&                				// We're in campaign,
+		if(si && si->campState &&                                   // We're in campaign,
 			(si->campState->getFilename() == "DATA/YOG.H3C") && // which is "Birth of a Barbarian",
-			(hero->subID == 45))                                // and the hero is Yog (based on Solmyr)
+			(hero->getHeroType() == 45))                        // and the hero is Yog (based on Solmyr)
 		{
 			// "Yog has given up magic in all its forms..."
 			LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[736]);
@@ -986,7 +986,7 @@ void CCastleBuildings::enterMagesGuild()
 			CFunctionList<void()> onYes = [this](){ openMagesGuild(); };
 			CFunctionList<void()> onNo = onYes;
 			onYes += [hero](){ LOCPLINT->cb->buyArtifact(hero, ArtifactID::SPELLBOOK); };
-			std::vector<std::shared_ptr<CComponent>> components(1, std::make_shared<CComponent>(CComponent::artifact,ArtifactID::SPELLBOOK,0));
+			std::vector<std::shared_ptr<CComponent>> components(1, std::make_shared<CComponent>(ComponentType::ARTIFACT, ArtifactID(ArtifactID::SPELLBOOK)));
 
 			LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[214], onYes, onNo, components);
 		}
@@ -1129,7 +1129,7 @@ void CCreaInfo::showPopupWindow(const Point & cursorPosition)
 	if (showAvailable)
 		GH.windows().createAndPushWindow<CDwellingInfoBox>(GH.screenDimensions().x / 2, GH.screenDimensions().y / 2, town, level);
 	else
-		CRClickPopup::createAndPush(genGrowthText(), std::make_shared<CComponent>(CComponent::creature, creature->getId()));
+		CRClickPopup::createAndPush(genGrowthText(), std::make_shared<CComponent>(ComponentType::CREATURE, creature->getId()));
 }
 
 bool CCreaInfo::getShowAvailable()
@@ -1180,7 +1180,7 @@ void CTownInfo::showPopupWindow(const Point & cursorPosition)
 {
 	if(building)
 	{
-		auto c =  std::make_shared<CComponent>(CComponent::building, building->town->faction->getIndex(), building->bid);
+		auto c =  std::make_shared<CComponent>(ComponentType::BUILDING, BuildingTypeUniqueID(building->town->faction->getId(), building->bid));
 		CRClickPopup::createAndPush(CInfoWindow::genText(building->getNameTranslated(), building->getDescriptionTranslated()), c);
 	}
 }
@@ -1526,15 +1526,24 @@ CBuildWindow::CBuildWindow(const CGTownInstance *Town, const CBuilding * Buildin
 	//Create components for all required resources
 	std::vector<std::shared_ptr<CComponent>> components;
 
-	for(int i = 0; i<GameConstants::RESOURCE_QUANTITY; i++)
+	for(GameResID i : GameResID::ALL_RESOURCES())
 	{
 		if(building->resources[i])
 		{
-			std::string text = std::to_string(building->resources[i]);
-			int resAfterBuy = LOCPLINT->cb->getResourceAmount(GameResID(i)) - building->resources[i];
-			if(resAfterBuy < 0 && state != EBuildingState::ALREADY_PRESENT && settings["general"]["enableUiEnhancements"].Bool())
-				text = "{H3Red|" + std::to_string(LOCPLINT->cb->getResourceAmount(GameResID(i))) + "}" + " / " + text;
-			components.push_back(std::make_shared<CComponent>(CComponent::resource, i, text, CComponent::small));
+			MetaString message;
+			int resourceAmount = LOCPLINT->cb->getResourceAmount(i);
+			bool canAfford = resourceAmount >= building->resources[i];
+
+			if(!canAfford && state != EBuildingState::ALREADY_PRESENT && settings["general"]["enableUiEnhancements"].Bool())
+			{
+				message.appendRawString("{H3Red|%d}/%d");
+				message.replaceNumber(resourceAmount);
+			}
+			else
+				message.appendRawString("%d");
+
+			message.replaceNumber(building->resources[i]);
+			components.push_back(std::make_shared<CComponent>(ComponentType::RESOURCE, i, message.toString(), CComponent::small));
 		}
 	}
 
@@ -1889,12 +1898,12 @@ CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell)
 
 void CMageGuildScreen::Scroll::clickPressed(const Point & cursorPosition)
 {
-	LOCPLINT->showInfoDialog(spell->getDescriptionTranslated(0), std::make_shared<CComponent>(CComponent::spell, spell->id));
+	LOCPLINT->showInfoDialog(spell->getDescriptionTranslated(0), std::make_shared<CComponent>(ComponentType::SPELL, spell->id));
 }
 
 void CMageGuildScreen::Scroll::showPopupWindow(const Point & cursorPosition)
 {
-	CRClickPopup::createAndPush(spell->getDescriptionTranslated(0), std::make_shared<CComponent>(CComponent::spell, spell->id));
+	CRClickPopup::createAndPush(spell->getDescriptionTranslated(0), std::make_shared<CComponent>(ComponentType::SPELL, spell->id));
 }
 
 void CMageGuildScreen::Scroll::hover(bool on)
@@ -1935,7 +1944,7 @@ CBlacksmithDialog::CBlacksmithDialog(bool possible, CreatureID creMachineID, Art
 	cancelText.appendTextID("core.genrltxt.596");
 	cancelText.replaceTextID(creature->getNameSingularTextID());
 
-	std::string costString = std::to_string(aid.toArtifact(CGI->artifacts())->getPrice());
+	std::string costString = std::to_string(aid.toEntity(CGI)->getPrice());
 
 	title = std::make_shared<CLabel>(165, 28, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, titleString.toString());
 	costText = std::make_shared<CLabel>(165, 218, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->jktexts[43]);

+ 8 - 7
client/windows/CCreatureWindow.cpp

@@ -325,7 +325,7 @@ CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset)
 				std::vector<std::shared_ptr<CComponent>> resComps;
 				for(TResources::nziterator i(totalCost); i.valid(); i++)
 				{
-					resComps.push_back(std::make_shared<CComponent>(CComponent::resource, i->resType, (int)i->resVal));
+					resComps.push_back(std::make_shared<CComponent>(ComponentType::RESOURCE, i->resType, i->resVal));
 				}
 
 				if(LOCPLINT->cb->getResourceAmount().canAfford(totalCost))
@@ -582,10 +582,10 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s
 			const CCommanderInstance * commander = parent->info->commander;
 			expRankIcon = std::make_shared<CAnimImage>(AnimationPath::builtin("PSKIL42"), 4, 0, pos.x, pos.y);
 
-			auto area = std::make_shared<LRClickableAreaWTextComp>(Rect(pos.x, pos.y, 44, 44), CComponent::experience);
+			auto area = std::make_shared<LRClickableAreaWTextComp>(Rect(pos.x, pos.y, 44, 44), ComponentType::EXPERIENCE);
 			expArea = area;
 			area->text = CGI->generaltexth->allTexts[2];
-			area->bonusValue =	commander->getExpRank();
+			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(CGI->heroh->reqExp(commander->getExpRank() + 1)));
 			boost::replace_first(area->text, "%d", std::to_string(commander->experience));
@@ -611,8 +611,8 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s
 		if(art)
 		{
 			parent->stackArtifactIcon = std::make_shared<CAnimImage>(AnimationPath::builtin("ARTIFACT"), art->artType->getIconIndex(), 0, pos.x, pos.y);
-			parent->stackArtifactHelp = std::make_shared<LRClickableAreaWTextComp>(Rect(pos, Point(44, 44)), CComponent::artifact);
-			parent->stackArtifactHelp->type = art->artType->getId();
+			parent->stackArtifactHelp = std::make_shared<LRClickableAreaWTextComp>(Rect(pos, Point(44, 44)), ComponentType::ARTIFACT);
+			parent->stackArtifactHelp->component.subType = art->artType->getId();
 
 			if(parent->info->owner)
 			{
@@ -974,11 +974,12 @@ void CStackWindow::removeStackArtifact(ArtifactPosition pos)
 	const auto slot = ArtifactUtils::getArtBackpackPosition(info->owner, art->getTypeId());
 	if(slot != ArtifactPosition::PRE_FIRST)
 	{
-		LOCPLINT->cb->swapArtifacts(ArtifactLocation(info->stackNode, pos), ArtifactLocation(info->owner, slot));
+		auto artLoc = ArtifactLocation(info->owner->id, pos);
+		artLoc.creature = info->stackNode->armyObj->findStack(info->stackNode);
+		LOCPLINT->cb->swapArtifacts(artLoc, ArtifactLocation(info->owner->id, slot));
 		stackArtifactButton.reset();
 		stackArtifactHelp.reset();
 		stackArtifactIcon.reset();
 		redraw();
 	}
 }
-

+ 14 - 22
client/windows/CHeroWindow.cpp

@@ -119,9 +119,9 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero)
 
 	for(int v = 0; v < GameConstants::PRIMARY_SKILLS; ++v)
 	{
-		auto area = std::make_shared<LRClickableAreaWTextComp>(Rect(30 + 70 * v, 109, 42, 64), CComponent::primskill);
+		auto area = std::make_shared<LRClickableAreaWTextComp>(Rect(30 + 70 * v, 109, 42, 64), ComponentType::PRIM_SKILL);
 		area->text = CGI->generaltexth->arraytxt[2+v];
-		area->type = v;
+		area->component.subType = PrimarySkill(v);
 		area->hoverText = boost::str(boost::format(CGI->generaltexth->heroscrn[1]) % CGI->generaltexth->primarySkillNames[v]);
 		primSkillAreas.push_back(area);
 
@@ -154,7 +154,7 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero)
 	for(int i = 0; i < std::min<size_t>(hero->secSkills.size(), 8u); ++i)
 	{
 		Rect r = Rect(i%2 == 0  ?  18  :  162,  276 + 48 * (i/2),  136,  42);
-		secSkillAreas.push_back(std::make_shared<LRClickableAreaWTextComp>(r, CComponent::secskill));
+		secSkillAreas.push_back(std::make_shared<LRClickableAreaWTextComp>(r, ComponentType::SEC_SKILL));
 		secSkillImages.push_back(std::make_shared<CAnimImage>(secSkills, 0, 0, r.x, r.y));
 
 		int x = (i % 2) ? 212 : 68;
@@ -232,20 +232,21 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded)
 	//primary skills support
 	for(size_t g=0; g<primSkillAreas.size(); ++g)
 	{
-		primSkillAreas[g]->bonusValue = curHero->getPrimSkillLevel(static_cast<PrimarySkill>(g));
-		primSkillValues[g]->setText(std::to_string(primSkillAreas[g]->bonusValue));
+		int value = curHero->getPrimSkillLevel(static_cast<PrimarySkill>(g));
+		primSkillAreas[g]->component.value = value;
+		primSkillValues[g]->setText(std::to_string(value));
 	}
 
 	//secondary skills support
 	for(size_t g=0; g< secSkillAreas.size(); ++g)
 	{
-		int skill = curHero->secSkills[g].first;
-		int	level = curHero->getSecSkillLevel(SecondarySkill(curHero->secSkills[g].first));
+		SecondarySkill skill = curHero->secSkills[g].first;
+		int	level = curHero->getSecSkillLevel(skill);
 		std::string skillName = CGI->skillh->getByIndex(skill)->getNameTranslated();
 		std::string skillValue = CGI->generaltexth->levels[level-1];
 
-		secSkillAreas[g]->type = skill;
-		secSkillAreas[g]->bonusValue = level;
+		secSkillAreas[g]->component.subType = skill;
+		secSkillAreas[g]->component.value = level;
 		secSkillAreas[g]->text = CGI->skillh->getByIndex(skill)->getDescriptionTranslated(level);
 		secSkillAreas[g]->hoverText = boost::str(boost::format(heroscrn[21]) % skillValue % skillName);
 		secSkillImages[g]->setFrame(skill*3 + level + 2);
@@ -334,20 +335,11 @@ void CHeroWindow::commanderWindow()
 	if(pickedArtInst)
 	{
 		const auto freeSlot = ArtifactUtils::getArtAnyPosition(curHero->commander, pickedArtInst->getTypeId());
-		if(freeSlot < ArtifactPosition::COMMANDER_AFTER_LAST) //we don't want to put it in commander's backpack!
+		if(vstd::contains(ArtifactUtils::commanderSlots(), freeSlot)) // We don't want to put it in commander's backpack!
 		{
-			ArtifactLocation src(hero, ArtifactPosition::TRANSITION_POS);
-			ArtifactLocation dst(curHero->commander.get(), freeSlot);
-
-			if(pickedArtInst->canBePutAt(dst, true))
-			{	//equip clicked stack
-				if(dst.getArt())
-				{
-					LOCPLINT->cb->swapArtifacts(dst, ArtifactLocation(hero,
-						ArtifactUtils::getArtBackpackPosition(hero, pickedArtInst->getTypeId())));
-				}
-				LOCPLINT->cb->swapArtifacts(src, dst);
-			}
+			ArtifactLocation dst(curHero->id, freeSlot);
+			dst.creature = SlotID::COMMANDER_SLOT_PLACEHOLDER;
+			LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS), dst);
 		}
 	}
 	else

+ 2 - 2
client/windows/CKingdomInterface.cpp

@@ -256,7 +256,7 @@ void InfoBoxAbstractHeroData::prepareMessage(std::string & text, std::shared_ptr
 		break;
 	case HERO_PRIMARY_SKILL:
 		text = CGI->generaltexth->arraytxt[2+getSubID()];
-		comp = std::make_shared<CComponent>(CComponent::primskill, getSubID(), (int)getValue());
+		comp = std::make_shared<CComponent>(ComponentType::PRIM_SKILL, PrimarySkill(getSubID()), getValue());
 		break;
 	case HERO_MANA:
 		text = CGI->generaltexth->allTexts[149];
@@ -271,7 +271,7 @@ void InfoBoxAbstractHeroData::prepareMessage(std::string & text, std::shared_ptr
 			if(value)
 			{
 				text = CGI->skillh->getByIndex(subID)->getDescriptionTranslated((int)value);
-				comp = std::make_shared<CComponent>(CComponent::secskill, subID, (int)value);
+				comp = std::make_shared<CComponent>(ComponentType::SEC_SKILL, SecondarySkill(subID), (int)value);
 			}
 			break;
 		}

+ 2 - 2
client/windows/CSpellWindow.cpp

@@ -558,7 +558,7 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition)
 		//battle spell on adv map or adventure map spell during combat => display infowindow, not cast
 		if((combatSpell ^ inCombat) || inCastle)
 		{
-			std::vector<std::shared_ptr<CComponent>> hlp(1, std::make_shared<CComponent>(CComponent::spell, mySpell->id, 0));
+			std::vector<std::shared_ptr<CComponent>> hlp(1, std::make_shared<CComponent>(ComponentType::SPELL, mySpell->id));
 			LOCPLINT->showInfoDialog(mySpell->getDescriptionTranslated(schoolLevel), hlp);
 		}
 		else if(combatSpell)
@@ -614,7 +614,7 @@ void CSpellWindow::SpellArea::showPopupWindow(const Point & cursorPosition)
 			boost::algorithm::replace_first(dmgInfo, "%d", std::to_string(causedDmg));
 		}
 
-		CRClickPopup::createAndPush(mySpell->getDescriptionTranslated(schoolLevel) + dmgInfo, std::make_shared<CComponent>(CComponent::spell, mySpell->id));
+		CRClickPopup::createAndPush(mySpell->getDescriptionTranslated(schoolLevel) + dmgInfo, std::make_shared<CComponent>(ComponentType::SPELL, mySpell->id));
 	}
 }
 

+ 4 - 4
client/windows/CTradeWindow.cpp

@@ -191,8 +191,8 @@ void CTradeWindow::CTradeableItem::clickPressed(const Point & cursorPosition)
 				const auto hero = aw->arts->getHero();
 				const auto slot = hero->getSlotByInstance(art);
 				assert(slot != ArtifactPosition::PRE_FIRST);
-				LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero, slot),
-					ArtifactLocation(hero, ArtifactPosition::TRANSITION_POS));
+				LOCPLINT->cb->swapArtifacts(ArtifactLocation(hero->id, slot),
+					ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS));
 				aw->arts->pickedArtFromSlot = slot;
 				aw->arts->artifactsOnAltar.erase(art);
 				setID(-1);
@@ -667,10 +667,10 @@ CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInsta
 			title = (*CGI->townh)[ETownType::STRONGHOLD]->town->buildings[BuildingID::FREELANCERS_GUILD]->getNameTranslated();
 			break;
 		case EMarketMode::RESOURCE_ARTIFACT:
-			title = (*CGI->townh)[o->subID]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated();
+			title = (*CGI->townh)[o->getFaction()]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated();
 			break;
 		case EMarketMode::ARTIFACT_RESOURCE:
-			title = (*CGI->townh)[o->subID]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated();
+			title = (*CGI->townh)[o->getFaction()]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated();
 
 			// create image that copies part of background containing slot MISC_1 into position of slot MISC_5
 			// this is workaround for bug in H3 files where this slot for ragdoll on this screen is missing

+ 4 - 2
client/windows/CWindowObject.cpp

@@ -39,7 +39,8 @@ CWindowObject::CWindowObject(int options_, const ImagePath & imageName, Point ce
 	options(options_),
 	background(createBg(imageName, options & PLAYER_COLORED))
 {
-	assert(parent == nullptr); //Safe to remove, but windows should not have parent
+	if(!(options & NEEDS_ANIMATED_BACKGROUND)) //currently workaround for highscores (currently uses window as normal control, because otherwise videos are not played in background yet)
+		assert(parent == nullptr); //Safe to remove, but windows should not have parent
 
 	defActions = 255-DISPOSE;
 
@@ -60,7 +61,8 @@ CWindowObject::CWindowObject(int options_, const ImagePath & imageName):
 	options(options_),
 	background(createBg(imageName, options_ & PLAYER_COLORED))
 {
-	assert(parent == nullptr); //Safe to remove, but windows should not have parent
+	if(!(options & NEEDS_ANIMATED_BACKGROUND)) //currently workaround for highscores (currently uses window as normal control, because otherwise videos are not played in background yet)
+		assert(parent == nullptr); //Safe to remove, but windows should not have parent
 
 	defActions = 255-DISPOSE;
 

+ 2 - 1
client/windows/CWindowObject.h

@@ -38,7 +38,8 @@ public:
 		PLAYER_COLORED=1, //background will be player-colored
 		RCLICK_POPUP=2, // window will behave as right-click popup
 		BORDERED=4, // window will have border if current resolution is bigger than size of window
-		SHADOW_DISABLED=8 //this window won't display any shadow
+		SHADOW_DISABLED=8, //this window won't display any shadow
+		NEEDS_ANIMATED_BACKGROUND=16 //there are videos in the background that have to be played
 	};
 
 	/*

+ 6 - 11
client/windows/GUIClasses.cpp

@@ -401,7 +401,7 @@ CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill pskill, std
 		std::vector<std::shared_ptr<CSelectableComponent>> comps;
 		for(auto & skill : skills)
 		{
-			auto comp = std::make_shared<CSelectableComponent>(CComponent::secskill, skill, hero->getSecSkillLevel(SecondarySkill(skill))+1, CComponent::medium);
+			auto comp = std::make_shared<CSelectableComponent>(ComponentType::SEC_SKILL, skill, hero->getSecSkillLevel(SecondarySkill(skill))+1, CComponent::medium);
 			comp->onChoose = std::bind(&CLevelWindow::close, this);
 			comps.push_back(comp);
 		}
@@ -693,9 +693,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 		else
 			primSkillAreas[g]->pos = Rect(Point(pos.x + 329, pos.y + 19 + 36 * g), Point(140, 32));
 		primSkillAreas[g]->text = CGI->generaltexth->arraytxt[2+g];
-		primSkillAreas[g]->type = g;
-		primSkillAreas[g]->bonusValue = 0;
-		primSkillAreas[g]->baseType = 0;
+		primSkillAreas[g]->component = Component( ComponentType::PRIM_SKILL, PrimarySkill(g));
 		primSkillAreas[g]->hoverText = CGI->generaltexth->heroscrn[1];
 		boost::replace_first(primSkillAreas[g]->hoverText, "%s", CGI->generaltexth->primarySkillNames[g]);
 	}
@@ -708,14 +706,11 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 		//secondary skill's clickable areas
 		for(int g=0; g<hero->secSkills.size(); ++g)
 		{
-			int skill = hero->secSkills[g].first,
-				level = hero->secSkills[g].second; // <1, 3>
+			SecondarySkill skill = hero->secSkills[g].first;
+			int level = hero->secSkills[g].second; // <1, 3>
 			secSkillAreas[b].push_back(std::make_shared<LRClickableAreaWTextComp>());
 			secSkillAreas[b][g]->pos = Rect(Point(pos.x + 32 + g * 36 + b * 454 , pos.y + (qeLayout ? 83 : 88)), Point(32, 32) );
-			secSkillAreas[b][g]->baseType = 1;
-
-			secSkillAreas[b][g]->type = skill;
-			secSkillAreas[b][g]->bonusValue = level;
+			secSkillAreas[b][g]->component = Component(ComponentType::SEC_SKILL, skill, level);
 			secSkillAreas[b][g]->text = CGI->skillh->getByIndex(skill)->getDescriptionTranslated(level);
 
 			secSkillAreas[b][g]->hoverText = CGI->generaltexth->heroscrn[21];
@@ -1082,7 +1077,7 @@ void CUniversityWindow::CItem::clickPressed(const Point & cursorPosition)
 
 void CUniversityWindow::CItem::showPopupWindow(const Point & cursorPosition)
 {
-	CRClickPopup::createAndPush(CGI->skillh->getByIndex(ID)->getDescriptionTranslated(1), std::make_shared<CComponent>(CComponent::secskill, ID, 1));
+	CRClickPopup::createAndPush(CGI->skillh->getByIndex(ID)->getDescriptionTranslated(1), std::make_shared<CComponent>(ComponentType::SEC_SKILL, ID, 1));
 }
 
 void CUniversityWindow::CItem::hover(bool on)

+ 1 - 1
client/windows/GUIClasses.h

@@ -375,7 +375,7 @@ class CUniversityWindow : public CStatusbarWindow
 		std::shared_ptr<CLabel> name;
 		std::shared_ptr<CLabel> level;
 	public:
-		int ID;//id of selected skill
+		SecondarySkill ID;//id of selected skill
 		CUniversityWindow * parent;
 
 		void showAll(Canvas & to) override;

+ 2 - 2
cmake_modules/VCMI_lib.cmake

@@ -686,8 +686,8 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		add_custom_command(TARGET ${TARGET_NAME} POST_BUILD
 			COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/config
 			COMMAND ${CMAKE_COMMAND} -E remove_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/Mods
-			COMMAND ${CMAKE_COMMAND} -E copy_directory ${MAIN_LIB_DIR}/../config ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/config
-			COMMAND ${CMAKE_COMMAND} -E copy_directory ${MAIN_LIB_DIR}/../Mods ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/Mods
+			COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${MAIN_LIB_DIR}/../config ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/config
+			COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${MAIN_LIB_DIR}/../Mods ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/Mods
 		)
 	endif()
 

+ 14 - 0
cmake_modules/create_link.cmake

@@ -0,0 +1,14 @@
+
+#message(${CMAKE_ARGV0}) # cmake.exe
+#message(${CMAKE_ARGV1}) # -P
+#message(${CMAKE_ARGV2}) # thisfilename
+#message(${CMAKE_ARGV3}) # existing
+#message(${CMAKE_ARGV4}) # linkname
+if (WIN32)
+	file(TO_NATIVE_PATH ${CMAKE_ARGV3} existing_native)
+	file(TO_NATIVE_PATH ${CMAKE_ARGV4} linkname_native)
+	execute_process(COMMAND cmd.exe /c RD /Q "${linkname_native}")
+	execute_process(COMMAND cmd.exe /c mklink /J "${linkname_native}" "${existing_native}")
+else()
+	execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_ARGV3} ${CMAKE_ARGV4})
+endif()

+ 4 - 0
docs/players/Installation_iOS.md

@@ -16,6 +16,10 @@ have the following options:
 - [Create signing assets on macOS from terminal](https://github.com/kambala-decapitator/xcode-auto-signing-assets). In the command replace `your.bundle.id` with something like `com.MY-NAME.vcmi`. After that use the above signer tool.
 - [Sign from any OS](https://github.com/indygreg/PyOxidizer/tree/main/tugger-code-signing). You'd still need to find a way to create signing assets (private key and provisioning profile) though.
 
+To install the signed ipa on your device, you can use Xcode or Apple Configurator (available on the Mac App Store for free). The latter also allows installing ipa from the command line, here's an example that assumes you have only 1 device connected to your Mac and the signed ipa is on your desktop:
+
+    /Applications/Apple\ Configurator.app/Contents/MacOS/cfgutil install-app ~/Desktop/vcmi.ipa
+
 ## Step 2: Installing Heroes III data files
 
 Note: if you don't need in-game videos, you can omit downloading/copying file VIDEO.VID from data folder - it will save your time and space. The same applies to the Mp3 directory.

+ 3 - 4
launcher/CMakeLists.txt

@@ -174,12 +174,11 @@ if(APPLE_IOS)
 else()
 	set(RESOURCES_DESTINATION ${DATA_DIR}/launcher)
 
-	# Copy to build directory for easier debugging
+	# Link to build directory for easier debugging
 	add_custom_command(TARGET vcmilauncher POST_BUILD
 		COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher
-		COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_SOURCE_DIR}/launcher/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/icons
-		COMMAND ${CMAKE_COMMAND} -E create_symlink ${CMAKE_CURRENT_BINARY_DIR}/translation ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/translation
-
+		COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_SOURCE_DIR}/launcher/icons ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/icons
+		COMMAND ${CMAKE_COMMAND} -P ${CMAKE_SOURCE_DIR}/cmake_modules/create_link.cmake ${CMAKE_CURRENT_BINARY_DIR}/translation ${CMAKE_BINARY_DIR}/bin/${CMAKE_CFG_INTDIR}/launcher/translation
 	)
 
 	install(TARGETS vcmilauncher DESTINATION ${BIN_DIR})

+ 2 - 0
launcher/settingsView/csettingsview_moc.cpp

@@ -90,7 +90,9 @@ void CSettingsView::loadSettings()
 	ui->comboBoxFriendlyAI->setCurrentText(QString::fromStdString(settings["server"]["friendlyAI"].String()));
 	ui->comboBoxNeutralAI->setCurrentText(QString::fromStdString(settings["server"]["neutralAI"].String()));
 	ui->comboBoxEnemyAI->setCurrentText(QString::fromStdString(settings["server"]["enemyAI"].String()));
+
 	ui->comboBoxEnemyPlayerAI->setCurrentText(QString::fromStdString(settings["server"]["playerAI"].String()));
+	ui->comboBoxAlliedPlayerAI->setCurrentText(QString::fromStdString(settings["server"]["alliedAI"].String()));
 
 	ui->spinBoxNetworkPort->setValue(settings["server"]["port"].Integer());
 

+ 44 - 1
lib/ArtifactUtils.cpp

@@ -74,6 +74,49 @@ DLL_LINKAGE const std::vector<ArtifactPosition> & ArtifactUtils::constituentWorn
 	return positions;
 }
 
+DLL_LINKAGE const std::vector<ArtifactPosition> & ArtifactUtils::allWornSlots()
+{
+	static const std::vector<ArtifactPosition> positions =
+	{
+		ArtifactPosition::HEAD,
+		ArtifactPosition::SHOULDERS,
+		ArtifactPosition::NECK,
+		ArtifactPosition::RIGHT_HAND,
+		ArtifactPosition::LEFT_HAND,
+		ArtifactPosition::TORSO,
+		ArtifactPosition::RIGHT_RING,
+		ArtifactPosition::LEFT_RING,
+		ArtifactPosition::FEET,
+		ArtifactPosition::MISC1,
+		ArtifactPosition::MISC2,
+		ArtifactPosition::MISC3,
+		ArtifactPosition::MISC4,
+		ArtifactPosition::MISC5,
+		ArtifactPosition::MACH1,
+		ArtifactPosition::MACH2,
+		ArtifactPosition::MACH3,
+		ArtifactPosition::MACH4,
+		ArtifactPosition::SPELLBOOK
+	};
+
+	return positions;
+}
+
+DLL_LINKAGE const std::vector<ArtifactPosition> & ArtifactUtils::commanderSlots()
+{
+	static const std::vector<ArtifactPosition> positions =
+	{
+		ArtifactPosition::COMMANDER1,
+		ArtifactPosition::COMMANDER2,
+		ArtifactPosition::COMMANDER3,
+		ArtifactPosition::COMMANDER4,
+		ArtifactPosition::COMMANDER5,
+		ArtifactPosition::COMMANDER6
+	};
+
+	return positions;
+}
+
 DLL_LINKAGE bool ArtifactUtils::isArtRemovable(const std::pair<ArtifactPosition, ArtSlotInfo> & slot)
 {
 	return slot.second.artifact
@@ -214,7 +257,7 @@ DLL_LINKAGE void ArtifactUtils::insertScrrollSpellName(std::string & description
 	if(sid.getNum() >= 0)
 	{
 		if(nameStart != std::string::npos && nameEnd != std::string::npos)
-			description = description.replace(nameStart, nameEnd - nameStart + 1, sid.toSpell(VLC->spells())->getNameTranslated());
+			description = description.replace(nameStart, nameEnd - nameStart + 1, sid.toEntity(VLC->spells())->getNameTranslated());
 	}
 }
 

+ 2 - 0
lib/ArtifactUtils.h

@@ -31,6 +31,8 @@ namespace ArtifactUtils
 	// TODO: Make this constexpr when the toolset is upgraded
 	DLL_LINKAGE const std::vector<ArtifactPosition> & unmovableSlots();
 	DLL_LINKAGE const std::vector<ArtifactPosition> & constituentWornSlots();
+	DLL_LINKAGE const std::vector<ArtifactPosition> & allWornSlots();
+	DLL_LINKAGE const std::vector<ArtifactPosition> & commanderSlots();
 	DLL_LINKAGE bool isArtRemovable(const std::pair<ArtifactPosition, ArtSlotInfo> & slot);
 	DLL_LINKAGE bool checkSpellbookIsNeeded(const CGHeroInstance * heroPtr, const ArtifactID & artID, const ArtifactPosition & slot);
 	DLL_LINKAGE bool isSlotBackpack(const ArtifactPosition & slot);

+ 7 - 7
lib/CArtHandler.cpp

@@ -167,7 +167,7 @@ bool CArtifact::isBig() const
 
 bool CArtifact::isTradable() const
 {
-	switch(id)
+	switch(id.toEnum())
 	{
 	case ArtifactID::SPELLBOOK:
 	case ArtifactID::GRAIL:
@@ -693,8 +693,8 @@ void CArtHandler::makeItCommanderArt(CArtifact * a, bool onlyCommander)
 		a->possibleSlots[ArtBearer::HERO].clear();
 		a->possibleSlots[ArtBearer::CREATURE].clear();
 	}
-	for (int i = ArtifactPosition::COMMANDER1; i <= ArtifactPosition::COMMANDER6; ++i)
-		a->possibleSlots[ArtBearer::COMMANDER].push_back(ArtifactPosition(i));
+	for(const auto & slot : ArtifactUtils::commanderSlots())
+		a->possibleSlots[ArtBearer::COMMANDER].push_back(ArtifactPosition(slot));
 }
 
 bool CArtHandler::legalArtifact(const ArtifactID & id)
@@ -975,9 +975,9 @@ const ArtSlotInfo * CArtifactSet::getSlot(const ArtifactPosition & pos) const
 	}
 	if(vstd::contains(artifactsWorn, pos))
 		return &artifactsWorn.at(pos);
-	if(pos >= ArtifactPosition::AFTER_LAST )
+	if(ArtifactUtils::isSlotBackpack(pos))
 	{
-		int backpackPos = (int)pos - ArtifactPosition::BACKPACK_START;
+		auto backpackPos = pos - ArtifactPosition::BACKPACK_START;
 		if(backpackPos < 0 || backpackPos >= artifactsInBackpack.size())
 			return nullptr;
 		else
@@ -1080,9 +1080,9 @@ void CArtifactSet::serializeJsonArtifacts(JsonSerializeFormat & handler, const s
 
 void CArtifactSet::serializeJsonHero(JsonSerializeFormat & handler, CMap * map)
 {
-	for(ArtifactPosition ap = ArtifactPosition::HEAD; ap < ArtifactPosition::AFTER_LAST; ap.advance(1))
+	for(const auto & slot : ArtifactUtils::allWornSlots())
 	{
-		serializeJsonSlot(handler, ap, map);
+		serializeJsonSlot(handler, slot, map);
 	}
 
 	std::vector<ArtifactID> backpackTemp;

+ 9 - 9
lib/CArtifactInstance.cpp

@@ -155,9 +155,9 @@ void CArtifactInstance::setId(ArtifactInstanceID id)
 	this->id = id;
 }
 
-bool CArtifactInstance::canBePutAt(const ArtifactLocation & al, bool assumeDestRemoved) const
+bool CArtifactInstance::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) const
 {
-	return artType->canBePutAt(al.getHolderArtSet(), al.slot, assumeDestRemoved);
+	return artType->canBePutAt(artSet, slot, assumeDestRemoved);
 }
 
 bool CArtifactInstance::isCombined() const
@@ -165,15 +165,15 @@ bool CArtifactInstance::isCombined() const
 	return artType->isCombined();
 }
 
-void CArtifactInstance::putAt(const ArtifactLocation & al)
+void CArtifactInstance::putAt(CArtifactSet & set, const ArtifactPosition slot)
 {
-	auto placementMap = al.getHolderArtSet()->putArtifact(al.slot, this);
+	auto placementMap = set.putArtifact(slot, this);
 	addPlacementMap(placementMap);
 }
 
-void CArtifactInstance::removeFrom(const ArtifactLocation & al)
+void CArtifactInstance::removeFrom(CArtifactSet & set, const ArtifactPosition slot)
 {
-	al.getHolderArtSet()->removeArtifact(al.slot);
+	set.removeArtifact(slot);
 	for(auto & part : partsInfo)
 	{
 		if(part.slot != ArtifactPosition::PRE_FIRST)
@@ -181,10 +181,10 @@ void CArtifactInstance::removeFrom(const ArtifactLocation & al)
 	}
 }
 
-void CArtifactInstance::move(const ArtifactLocation & src, const ArtifactLocation & dst)
+void CArtifactInstance::move(CArtifactSet & srcSet, const ArtifactPosition srcSlot, CArtifactSet & dstSet, const ArtifactPosition dstSlot)
 {
-	removeFrom(src);
-	putAt(dst);
+	removeFrom(srcSet, srcSlot);
+	putAt(dstSet, dstSlot);
 }
 
 void CArtifactInstance::deserializationFix()

+ 5 - 4
lib/CArtifactInstance.h

@@ -84,11 +84,12 @@ public:
 	ArtifactInstanceID getId() const;
 	void setId(ArtifactInstanceID id);
 
-	bool canBePutAt(const ArtifactLocation & al, bool assumeDestRemoved = false) const;
+	bool canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot = ArtifactPosition::FIRST_AVAILABLE,
+		bool assumeDestRemoved = false) const;
 	bool isCombined() const;
-	void putAt(const ArtifactLocation & al);
-	void removeFrom(const ArtifactLocation & al);
-	void move(const ArtifactLocation & src, const ArtifactLocation & dst);
+	void putAt(CArtifactSet & set, const ArtifactPosition slot);
+	void removeFrom(CArtifactSet & set, const ArtifactPosition slot);
+	void move(CArtifactSet & srcSet, const ArtifactPosition srcSlot, CArtifactSet & dstSet, const ArtifactPosition dstSlot);
 	
 	void deserializationFix();
 	template <typename Handler> void serialize(Handler & h, const int version)

+ 6 - 6
lib/CBuildingHandler.cpp

@@ -12,7 +12,7 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-BuildingID CBuildingHandler::campToERMU(int camp, int townType, const std::set<BuildingID> & builtBuildings)
+BuildingID CBuildingHandler::campToERMU(int camp, FactionID townType, const std::set<BuildingID> & builtBuildings)
 {
 	static const std::vector<BuildingID> campToERMU = 
 	{
@@ -47,13 +47,13 @@ BuildingID CBuildingHandler::campToERMU(int camp, int townType, const std::set<B
 
 		if (i < 5) // last two levels don't have reserved horde ID. Yet another H3C weirdeness
 		{
-			if (vstd::contains(hordeLvlsPerTType[townType], i))
+			if (vstd::contains(hordeLvlsPerTType[townType.getNum()], i))
 			{
 				if (camp == curPos)
 				{
-					if (hordeLvlsPerTType[townType][0] == i)
+					if (hordeLvlsPerTType[townType.getNum()][0] == i)
 					{
-						BuildingID dwellingID(BuildingID::DWELL_UP_FIRST + hordeLvlsPerTType[townType][0]);
+						BuildingID dwellingID(BuildingID::DWELL_UP_FIRST + hordeLvlsPerTType[townType.getNum()][0]);
 
 						if(vstd::contains(builtBuildings, dwellingID)) //if upgraded dwelling is built
 							return BuildingID::HORDE_1_UPGR;
@@ -62,9 +62,9 @@ BuildingID CBuildingHandler::campToERMU(int camp, int townType, const std::set<B
 					}
 					else
 					{
-						if(hordeLvlsPerTType[townType].size() > 1)
+						if(hordeLvlsPerTType[townType.getNum()].size() > 1)
 						{
-							BuildingID dwellingID(BuildingID::DWELL_UP_FIRST + hordeLvlsPerTType[townType][1]);
+							BuildingID dwellingID(BuildingID::DWELL_UP_FIRST + hordeLvlsPerTType[townType.getNum()][1]);
 
 							if(vstd::contains(builtBuildings, dwellingID)) //if upgraded dwelling is built
 								return BuildingID::HORDE_2_UPGR;

+ 1 - 1
lib/CBuildingHandler.h

@@ -16,7 +16,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 class DLL_LINKAGE CBuildingHandler
 {
 public:
-	static BuildingID campToERMU(int camp, int townType, const std::set<BuildingID> & builtBuildings);
+	static BuildingID campToERMU(int camp, FactionID townType, const std::set<BuildingID> & builtBuildings);
 };
 
 VCMI_LIB_NAMESPACE_END

+ 16 - 27
lib/CCreatureHandler.cpp

@@ -263,7 +263,7 @@ bool CCreature::isDoubleWide() const
  */
 bool CCreature::isGood () const
 {
-	return VLC->factions()->getByIndex(faction)->getAlignment() == EAlignment::GOOD;
+	return VLC->factions()->getById(faction)->getAlignment() == EAlignment::GOOD;
 }
 
 /**
@@ -272,7 +272,7 @@ bool CCreature::isGood () const
  */
 bool CCreature::isEvil () const
 {
-	return VLC->factions()->getByIndex(faction)->getAlignment() == EAlignment::EVIL;
+	return VLC->factions()->getById(faction)->getAlignment() == EAlignment::EVIL;
 }
 
 si32 CCreature::maxAmount(const TResources &res) const //how many creatures can be bought
@@ -388,7 +388,7 @@ void CCreature::serializeJson(JsonSerializeFormat & handler)
 	if(handler.updating)
 	{
 		cost.serializeJson(handler, "cost");
-		handler.serializeInt("faction", faction);//TODO: unify with deferred resolve
+		handler.serializeId("faction", faction);
 	}
 
 	handler.serializeInt("level", level);
@@ -1350,34 +1350,23 @@ CCreatureHandler::~CCreatureHandler()
 
 CreatureID CCreatureHandler::pickRandomMonster(CRandomGenerator & rand, int tier) const
 {
-	int r = 0;
-	if(tier == -1) //pick any allowed creature
+	std::vector<CreatureID> allowed;
+	for(const auto & creature : objects)
 	{
-		do
-		{
-			r = (*RandomGeneratorUtil::nextItem(objects, rand))->getId();
-		} while (objects[r] && objects[r]->special); // find first "not special" creature
-	}
-	else
-	{
-		assert(vstd::iswithin(tier, 1, 7));
-		std::vector<CreatureID> allowed;
-		for(const auto & creature : objects)
-		{
-			if(!creature->special && creature->level == tier)
-				allowed.push_back(creature->getId());
-		}
+		if(creature->special)
+			continue;
 
-		if(allowed.empty())
-		{
-			logGlobal->warn("Cannot pick a random creature of tier %d!", tier);
-			return CreatureID::NONE;
-		}
+		if (creature->level == tier || tier == -1)
+			allowed.push_back(creature->getId());
+	}
 
-		return *RandomGeneratorUtil::nextItem(allowed, rand);
+	if(allowed.empty())
+	{
+		logGlobal->warn("Cannot pick a random creature of tier %d!", tier);
+		return CreatureID::NONE;
 	}
-	assert (r >= 0); //should always be, but it crashed
-	return CreatureID(r);
+
+	return *RandomGeneratorUtil::nextItem(allowed, rand);
 }
 
 

+ 7 - 2
lib/CCreatureSet.cpp

@@ -458,7 +458,7 @@ const CStackInstance & CCreatureSet::getStack(const SlotID & slot) const
 	return *getStackPtr(slot);
 }
 
-const CStackInstance * CCreatureSet::getStackPtr(const SlotID & slot) const
+CStackInstance * CCreatureSet::getStackPtr(const SlotID & slot) const
 {
 	if(hasStackAtSlot(slot))
 		return stacks.find(slot)->second;
@@ -870,7 +870,7 @@ ArtBearer::ArtBearer CStackInstance::bearerType() const
 CStackInstance::ArtPlacementMap CStackInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art)
 {
 	assert(!getArt(pos));
-	assert(art->artType->canBePutAt(this, pos));
+	assert(art->canBePutAt(this, pos));
 
 	attachTo(*art);
 	return CArtifactSet::putArtifact(pos, art);
@@ -1017,6 +1017,11 @@ const Creature * CStackBasicDescriptor::getType() const
 	return type;
 }
 
+CreatureID CStackBasicDescriptor::getId() const
+{
+	return type->getId();
+}
+
 TQuantity CStackBasicDescriptor::getCount() const
 {
 	return count;

+ 2 - 1
lib/CCreatureSet.h

@@ -39,6 +39,7 @@ public:
 	virtual ~CStackBasicDescriptor() = default;
 
 	const Creature * getType() const;
+	CreatureID getId() const;
 	TQuantity getCount() const;
 
 	virtual void setType(const CCreature * c);
@@ -253,7 +254,7 @@ public:
 	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.
 
 	const CStackInstance & getStack(const SlotID & slot) const; //stack must exist
-	const CStackInstance * getStackPtr(const SlotID & slot) const; //if stack doesn't exist, returns nullptr
+	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;

+ 17 - 0
lib/CGameInfoCallback.cpp

@@ -16,6 +16,7 @@
 #include "gameState/TavernHeroesPool.h"
 #include "gameState/QuestInfo.h"
 #include "mapObjects/CGHeroInstance.h"
+#include "networkPacks/ArtifactLocation.h"
 #include "CGeneralTextHandler.h"
 #include "StartInfo.h" // for StartInfo
 #include "battle/BattleInfo.h" // for BattleInfo
@@ -966,6 +967,22 @@ const CGObjectInstance * CGameInfoCallback::getObjInstance( ObjectInstanceID oid
 	return gs->map->objects[oid.num];
 }
 
+CArtifactSet * CGameInfoCallback::getArtSet(const ArtifactLocation & loc) const
+{
+	auto hero = const_cast<CGHeroInstance*>(getHero(loc.artHolder));
+	if(loc.creature.has_value())
+	{
+		if(loc.creature.value() == SlotID::COMMANDER_SLOT_PLACEHOLDER)
+			return hero->commander;
+		else
+			return hero->getStackPtr(loc.creature.value());
+	}
+	else
+	{
+		return hero;
+	}
+}
+
 std::vector<ObjectInstanceID> CGameInfoCallback::getVisibleTeleportObjects(std::vector<ObjectInstanceID> ids, PlayerColor player) const
 {
 	vstd::erase_if(ids, [&](const ObjectInstanceID & id) -> bool

+ 3 - 0
lib/CGameInfoCallback.h

@@ -40,6 +40,8 @@ class CGameState;
 class PathfinderConfig;
 struct TurnTimerInfo;
 
+struct ArtifactLocation;
+class CArtifactSet;
 class CArmedInstance;
 class CGObjectInstance;
 class CGHeroInstance;
@@ -174,6 +176,7 @@ public:
 	virtual int64_t estimateSpellDamage(const CSpell * sp, const CGHeroInstance * hero) const; //estimates damage of given spell; returns 0 if spell causes no dmg
 	virtual const CArtifactInstance * getArtInstance(ArtifactInstanceID aid) const;
 	virtual const CGObjectInstance * getObjInstance(ObjectInstanceID oid) const;
+	virtual CArtifactSet * getArtSet(const ArtifactLocation & loc) const;
 	//virtual const CGObjectInstance * getArmyInstance(ObjectInstanceID oid) const;
 
 	//objects

+ 10 - 10
lib/CHeroHandler.cpp

@@ -125,15 +125,17 @@ SecondarySkill CHeroClass::chooseSecSkill(const std::set<SecondarySkill> & possi
 {
 	int totalProb = 0;
 	for(const auto & possible : possibles)
-	{
-		totalProb += secSkillProbability[possible];
-	}
+		if (secSkillProbability.count(possible) != 0)
+			totalProb += secSkillProbability.at(possible);
+
 	if (totalProb != 0) // may trigger if set contains only banned skills (0 probability)
 	{
 		auto ran = rand.nextInt(totalProb - 1);
 		for(const auto & possible : possibles)
 		{
-			ran -= secSkillProbability[possible];
+			if (secSkillProbability.count(possible) != 0)
+				ran -= secSkillProbability.at(possible);
+
 			if(ran < 0)
 			{
 				return possible;
@@ -151,7 +153,7 @@ bool CHeroClass::isMagicHero() const
 
 EAlignment CHeroClass::getAlignment() const
 {
-	return VLC->factions()->getByIndex(faction)->getAlignment();
+	return VLC->factions()->getById(faction)->getAlignment();
 }
 
 int32_t CHeroClass::getIndex() const
@@ -209,7 +211,7 @@ CHeroClass::CHeroClass():
 
 void CHeroClassHandler::fillPrimarySkillData(const JsonNode & node, CHeroClass * heroClass, PrimarySkill pSkill) const
 {
-	const auto & skillName = NPrimarySkill::names[static_cast<int>(pSkill)];
+	const auto & skillName = NPrimarySkill::names[pSkill.getNum()];
 	auto currentPrimarySkillValue = static_cast<int>(node["primarySkills"][skillName].Integer());
 	//minimal value is 0 for attack and defense and 1 for spell power and knowledge
 	auto primarySkillLegalMinimum = (pSkill == PrimarySkill::ATTACK || pSkill == PrimarySkill::DEFENSE) ? 0 : 1;
@@ -271,8 +273,6 @@ CHeroClass * CHeroClassHandler::loadFromJson(const std::string & scope, const Js
 		int probability = static_cast<int>(skillPair.second.Integer());
 		VLC->identifiers()->requestIdentifier(skillPair.second.meta, "skill", skillPair.first, [heroClass, probability](si32 skillID)
 		{
-			if(heroClass->secSkillProbability.size() <= skillID)
-				heroClass->secSkillProbability.resize(skillID + 1, -1); // -1 = override with default later
 			heroClass->secSkillProbability[skillID] = probability;
 		});
 	}
@@ -369,11 +369,11 @@ void CHeroClassHandler::afterLoadFinalization()
 			auto chance = static_cast<float>(heroClass->defaultTavernChance * faction->town->defaultTavernChance);
 			heroClass->selectionProbability[faction->getId()] = static_cast<int>(sqrt(chance) + 0.5); //FIXME: replace with std::round once MVS supports it
 		}
+
 		// set default probabilities for gaining secondary skills where not loaded previously
-		heroClass->secSkillProbability.resize(VLC->skillh->size(), -1);
 		for(int skillID = 0; skillID < VLC->skillh->size(); skillID++)
 		{
-			if(heroClass->secSkillProbability[skillID] < 0)
+			if(heroClass->secSkillProbability.count(skillID) == 0)
 			{
 				const CSkill * skill = (*VLC->skillh)[SecondarySkill(skillID)];
 				logMod->trace("%s: no probability for %s, using default", heroClass->identifier, skill->getJsonKey());

+ 1 - 7
lib/CHeroHandler.h

@@ -157,7 +157,7 @@ public:
 	std::vector<int> primarySkillLowLevel; // probability (%) of getting point of primary skill when getting level
 	std::vector<int> primarySkillHighLevel;// same for high levels (> 10)
 
-	std::vector<int> secSkillProbability; //probabilities of gaining secondary skills (out of 112), in id order
+	std::map<SecondarySkill, int> secSkillProbability; //probabilities of gaining secondary skills (out of 112), in id order
 
 	std::map<FactionID, int> selectionProbability; //probability of selection in towns
 
@@ -201,12 +201,6 @@ public:
 		h & imageBattleFemale;
 		h & imageMapMale;
 		h & imageMapFemale;
-
-		if(!h.saving)
-		{
-			for(int & i : secSkillProbability)
-				vstd::amax(i, 0);
-		}
 	}
 	EAlignment getAlignment() const;
 };

+ 2 - 21
lib/CSkillHandler.cpp

@@ -120,7 +120,7 @@ DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill::LevelInf
 
 DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill & skill)
 {
-	out << "Skill(" << (int)skill.id << "," << skill.identifier << "): [";
+	out << "Skill(" << skill.id.getNum() << "," << skill.identifier << "): [";
 	for(int i=0; i < skill.levels.size(); i++)
 		out << (i ? "," : "") << skill.levels[i];
 	return out << "]";
@@ -233,7 +233,7 @@ CSkill * CSkillHandler::loadFromJson(const std::string & scope, const JsonNode &
 		skillAtLevel.iconMedium = levelNode["images"]["medium"].String();
 		skillAtLevel.iconLarge = levelNode["images"]["large"].String();
 	}
-	logMod->debug("loaded secondary skill %s(%d)", identifier, (int)skill->id);
+	logMod->debug("loaded secondary skill %s(%d)", identifier, skill->id.getNum());
 
 	return skill;
 }
@@ -262,23 +262,4 @@ std::vector<bool> CSkillHandler::getDefaultAllowed() const
 	return allowedSkills;
 }
 
-si32 CSkillHandler::decodeSkill(const std::string & identifier)
-{
-	auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeMap(), "skill", identifier);
-	if(rawId)
-		return rawId.value();
-	else
-		return -1;
-}
-
-std::string CSkillHandler::encodeSkill(const si32 index)
-{
-	return (*VLC->skillh)[SecondarySkill(index)]->identifier;
-}
-
-std::string CSkillHandler::encodeSkillWithType(const si32 index)
-{
-	return ModUtility::makeFullIdentifier("", "skill", encodeSkill(index));
-}
-
 VCMI_LIB_NAMESPACE_END

+ 0 - 5
lib/CSkillHandler.h

@@ -111,11 +111,6 @@ public:
 
 	std::vector<bool> getDefaultAllowed() const override;
 
-	///json serialization helpers
-	static si32 decodeSkill(const std::string & identifier);
-	static std::string encodeSkill(const si32 index);
-	static std::string encodeSkillWithType(const si32 index);
-
 	template <typename Handler> void serialize(Handler & h, const int version)
 	{
 		h & objects;

+ 1 - 1
lib/CTownHandler.cpp

@@ -686,7 +686,7 @@ void CTownHandler::loadBuilding(CTown * town, const std::string & stringID, cons
 	//MODS COMPATIBILITY FOR 0.96
 	if(!ret->produce.nonZero())
 	{
-		switch (ret->bid) {
+		switch (ret->bid.toEnum()) {
 			break; case BuildingID::VILLAGE_HALL: ret->produce[EGameResID::GOLD] = 500;
 			break; case BuildingID::TOWN_HALL :   ret->produce[EGameResID::GOLD] = 1000;
 			break; case BuildingID::CITY_HALL :   ret->produce[EGameResID::GOLD] = 2000;

+ 3 - 3
lib/IGameCallback.cpp

@@ -148,11 +148,11 @@ void CPrivilegedInfoCallback::getAllTiles(std::unordered_set<int3> & tiles, std:
 void CPrivilegedInfoCallback::pickAllowedArtsSet(std::vector<const CArtifact *> & out, CRandomGenerator & rand) const
 {
 	for (int j = 0; j < 3 ; j++)
-		out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_TREASURE)]);
+		out.push_back(VLC->arth->pickRandomArtifact(rand, CArtifact::ART_TREASURE).toArtifact());
 	for (int j = 0; j < 3 ; j++)
-		out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MINOR)]);
+		out.push_back(VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MINOR).toArtifact());
 
-	out.push_back(VLC->arth->objects[VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MAJOR)]);
+	out.push_back(VLC->arth->pickRandomArtifact(rand, CArtifact::ART_MAJOR).toArtifact());
 }
 
 void CPrivilegedInfoCallback::getAllowedSpells(std::vector<SpellID> & out, std::optional<ui16> level)

+ 3 - 3
lib/JsonRandom.cpp

@@ -113,7 +113,7 @@ namespace JsonRandom
 		if (value.empty() || value[0] != '@')
 			return PrimarySkill(*VLC->identifiers()->getIdentifier(modScope, "primarySkill", value));
 		else
-			return PrimarySkill(loadVariable("primarySkill", value, variables, static_cast<int>(PrimarySkill::NONE)));
+			return PrimarySkill(loadVariable("primarySkill", value, variables, PrimarySkill::NONE.getNum()));
 	}
 
 	/// Method that allows type-specific object filtering
@@ -322,7 +322,7 @@ namespace JsonRandom
 			for(const auto & pair : value.Struct())
 			{
 				PrimarySkill id = decodeKey<PrimarySkill>(pair.second.meta, pair.first, variables);
-				ret[static_cast<int>(id)] += loadValue(pair.second, rng, variables);
+				ret[id.getNum()] += loadValue(pair.second, rng, variables);
 			}
 		}
 		if(value.isVector())
@@ -333,7 +333,7 @@ namespace JsonRandom
 				PrimarySkill skillID = *RandomGeneratorUtil::nextItem(potentialPicks, rng);
 
 				defaultSkills.erase(skillID);
-				ret[static_cast<int>(skillID)] += loadValue(element, rng, variables);
+				ret[skillID.getNum()] += loadValue(element, rng, variables);
 			}
 		}
 		return ret;

+ 6 - 6
lib/MetaString.cpp

@@ -114,35 +114,35 @@ std::string MetaString::getLocalString(const std::pair<EMetaText, ui32> & txt) c
 	{
 		case EMetaText::ART_NAMES:
 		{
-			const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts());
+			const auto * art = ArtifactID(ser).toEntity(VLC);
 			if(art)
 				return art->getNameTranslated();
 			return "#!#";
 		}
 		case EMetaText::ART_DESCR:
 		{
-			const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts());
+			const auto * art = ArtifactID(ser).toEntity(VLC);
 			if(art)
 				return art->getDescriptionTranslated();
 			return "#!#";
 		}
 		case EMetaText::ART_EVNTS:
 		{
-			const auto * art = ArtifactID(ser).toArtifact(VLC->artifacts());
+			const auto * art = ArtifactID(ser).toEntity(VLC);
 			if(art)
 				return art->getEventTranslated();
 			return "#!#";
 		}
 		case EMetaText::CRE_PL_NAMES:
 		{
-			const auto * cre = CreatureID(ser).toCreature(VLC->creatures());
+			const auto * cre = CreatureID(ser).toEntity(VLC);
 			if(cre)
 				return cre->getNamePluralTranslated();
 			return "#!#";
 		}
 		case EMetaText::CRE_SING_NAMES:
 		{
-			const auto * cre = CreatureID(ser).toCreature(VLC->creatures());
+			const auto * cre = CreatureID(ser).toEntity(VLC);
 			if(cre)
 				return cre->getNameSingularTranslated();
 			return "#!#";
@@ -157,7 +157,7 @@ std::string MetaString::getLocalString(const std::pair<EMetaText, ui32> & txt) c
 		}
 		case EMetaText::SPELL_NAME:
 		{
-			const auto * spell = SpellID(ser).toSpell(VLC->spells());
+			const auto * spell = SpellID(ser).toEntity(VLC);
 			if(spell)
 				return spell->getNameTranslated();
 			return "#!#";

+ 0 - 13
lib/ResourceSet.cpp

@@ -25,19 +25,6 @@ ResourceSet::ResourceSet(const JsonNode & node)
 		container[i] = static_cast<int>(node[GameConstants::RESOURCE_NAMES[i]].Float());
 }
 
-ResourceSet::ResourceSet(TResource wood, TResource mercury, TResource ore, TResource sulfur, TResource crystal,
-							TResource gems, TResource gold, TResource mithril)
-{
-	container[GameResID(EGameResID::WOOD)] = wood;
-	container[GameResID(EGameResID::MERCURY)] = mercury;
-	container[GameResID(EGameResID::ORE)] = ore;
-	container[GameResID(EGameResID::SULFUR)] = sulfur;
-	container[GameResID(EGameResID::CRYSTAL)] = crystal;
-	container[GameResID(EGameResID::GEMS)] = gems;
-	container[GameResID(EGameResID::GOLD)] = gold;
-	container[GameResID(EGameResID::MITHRIL)] = mithril;
-}
-
 void ResourceSet::serializeJson(JsonSerializeFormat & handler, const std::string & fieldName)
 {
 	if(handler.saving && !nonZero())

+ 2 - 3
lib/ResourceSet.h

@@ -26,12 +26,11 @@ class ResourceSet;
 class ResourceSet
 {
 private:
-	std::array<TResource, GameConstants::RESOURCE_QUANTITY> container;
+	std::array<TResource, GameConstants::RESOURCE_QUANTITY> container = {};
 public:
 	// read resources set from json. Format example: { "gold": 500, "wood":5 }
 	DLL_LINKAGE ResourceSet(const JsonNode & node);
-	DLL_LINKAGE ResourceSet(TResource wood = 0, TResource mercury = 0, TResource ore = 0, TResource sulfur = 0, TResource crystal = 0,
-							TResource gems = 0, TResource gold = 0, TResource mithril = 0);
+	DLL_LINKAGE ResourceSet() = default;
 
 
 #define scalarOperator(OPSIGN)									\

+ 2 - 2
lib/battle/CBattleInfoCallback.cpp

@@ -1155,7 +1155,7 @@ std::pair<const battle::Unit *, BattleHex> CBattleInfoCallback::getNearestStack(
 
 BattleHex CBattleInfoCallback::getAvaliableHex(const CreatureID & creID, ui8 side, int initialPos) const
 {
-	bool twoHex = VLC->creh->objects[creID]->isDoubleWide();
+	bool twoHex = VLC->creatures()->getById(creID)->isDoubleWide();
 
 	int pos;
 	if (initialPos > -1)
@@ -1663,7 +1663,7 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c
 		|| !(spellID.toSpell()->canBeCast(this, spells::Mode::CREATURE_ACTIVE, subject)))
 			continue;
 
-		switch (spellID)
+		switch (spellID.toEnum())
 		{
 		case SpellID::SHIELD:
 		case SpellID::FIRE_SHIELD: // not if all enemy units are shooters

+ 1 - 1
lib/battle/SideInBattle.cpp

@@ -18,7 +18,7 @@ void SideInBattle::init(const CGHeroInstance * Hero, const CArmedInstance * Army
 	hero = Hero;
 	armyObject = Army;
 
-	switch(armyObject->ID)
+	switch(armyObject->ID.toEnum())
 	{
 		case Obj::CREATURE_GENERATOR1:
 		case Obj::CREATURE_GENERATOR2:

+ 1 - 1
lib/battle/Unit.cpp

@@ -218,7 +218,7 @@ int Unit::getRawSurrenderCost() const
 void UnitInfo::serializeJson(JsonSerializeFormat & handler)
 {
 	handler.serializeInt("count", count);
-	handler.serializeId("type", type, CreatureID::NONE);
+	handler.serializeId("type", type, CreatureID(CreatureID::NONE));
 	handler.serializeInt("side", side);
 	handler.serializeInt("position", position);
 	handler.serializeBool("summoned", summoned);

+ 3 - 3
lib/bonuses/Bonus.cpp

@@ -100,13 +100,13 @@ std::string Bonus::Description(std::optional<si32> customValue) const
 			switch(source)
 			{
 			case BonusSource::ARTIFACT:
-				str << sid.as<ArtifactID>().toArtifact(VLC->artifacts())->getNameTranslated();
+				str << sid.as<ArtifactID>().toEntity(VLC)->getNameTranslated();
 				break;
 			case BonusSource::SPELL_EFFECT:
-				str << sid.as<SpellID>().toSpell(VLC->spells())->getNameTranslated();
+				str << sid.as<SpellID>().toEntity(VLC)->getNameTranslated();
 				break;
 			case BonusSource::CREATURE_ABILITY:
-				str << sid.as<CreatureID>().toCreature(VLC->creatures())->getNamePluralTranslated();
+				str << sid.as<CreatureID>().toEntity(VLC)->getNamePluralTranslated();
 				break;
 			case BonusSource::SECONDARY_SKILL:
 				str << VLC->skills()->getById(sid.as<SecondarySkill>())->getNameTranslated();

+ 3 - 3
lib/bonuses/Limiters.cpp

@@ -114,7 +114,7 @@ CCreatureTypeLimiter::CCreatureTypeLimiter(const CCreature & creature_, bool Inc
 
 void CCreatureTypeLimiter::setCreature(const CreatureID & id)
 {
-	creature = VLC->creh->objects[id];
+	creature = id.toCreature();
 }
 
 std::string CCreatureTypeLimiter::toString() const
@@ -317,7 +317,7 @@ ILimiter::EDecision FactionLimiter::limit(const BonusLimitationContext &context)
 std::string FactionLimiter::toString() const
 {
 	boost::format fmt("FactionLimiter(faction=%s)");
-	fmt % VLC->factions()->getByIndex(faction)->getJsonKey();
+	fmt % VLC->factions()->getById(faction)->getJsonKey();
 	return fmt.str();
 }
 
@@ -326,7 +326,7 @@ JsonNode FactionLimiter::toJsonNode() const
 	JsonNode root(JsonNode::JsonType::DATA_STRUCT);
 
 	root["type"].String() = "FACTION_LIMITER";
-	root["parameters"].Vector().push_back(JsonUtils::stringNode(VLC->factions()->getByIndex(faction)->getJsonKey()));
+	root["parameters"].Vector().push_back(JsonUtils::stringNode(VLC->factions()->getById(faction)->getJsonKey()));
 
 	return root;
 }

+ 9 - 10
lib/campaign/CampaignState.cpp

@@ -73,13 +73,13 @@ ImagePath CampaignRegions::getBackgroundName() const
 
 Point CampaignRegions::getPosition(CampaignScenarioID which) const
 {
-	auto const & region = regions[static_cast<int>(which)];
+	auto const & region = regions[which.getNum()];
 	return Point(region.xpos, region.ypos);
 }
 
 ImagePath CampaignRegions::getNameFor(CampaignScenarioID which, int colorIndex, std::string type) const
 {
-	auto const & region = regions[static_cast<int>(which)];
+	auto const & region = regions[which.getNum()];
 
 	static const std::string colors[2][8] =
 	{
@@ -267,24 +267,23 @@ void CampaignState::setCurrentMapAsConquered(std::vector<CGHeroInstance *> heroe
 		return a->getHeroStrength() > b->getHeroStrength();
 	});
 
-	logGlobal->info("Scenario %d of campaign %s (%s) has been completed", static_cast<int>(*currentMap), getFilename(), getNameTranslated());
+	logGlobal->info("Scenario %d of campaign %s (%s) has been completed", currentMap->getNum(), getFilename(), getNameTranslated());
 
 	mapsConquered.push_back(*currentMap);
 	auto reservedHeroes = getReservedHeroes();
 
 	for (auto * hero : heroes)
 	{
-		HeroTypeID heroType(hero->subID);
 		JsonNode node = CampaignState::crossoverSerialize(hero);
 
-		if (reservedHeroes.count(heroType))
+		if (reservedHeroes.count(hero->getHeroType()))
 		{
-			logGlobal->info("Hero crossover: %d (%s) exported to global pool", hero->subID, hero->getNameTranslated());
-			globalHeroPool[heroType] = node;
+			logGlobal->info("Hero crossover: %d (%s) exported to global pool", hero->getHeroType(), hero->getNameTranslated());
+			globalHeroPool[hero->getHeroType()] = node;
 		}
 		else
 		{
-			logGlobal->info("Hero crossover: %d (%s) exported to scenario pool", hero->subID, hero->getNameTranslated());
+			logGlobal->info("Hero crossover: %d (%s) exported to scenario pool", hero->getHeroType(), hero->getNameTranslated());
 			scenarioHeroPool[*currentMap].push_back(node);
 		}
 	}
@@ -321,7 +320,7 @@ std::unique_ptr<CMap> CampaignState::getMap(CampaignScenarioID scenarioId) const
 	CMapService mapService;
 	std::string scenarioName = getFilename().substr(0, getFilename().find('.'));
 	boost::to_lower(scenarioName);
-	scenarioName += ':' + std::to_string(static_cast<int>(scenarioId));
+	scenarioName += ':' + std::to_string(scenarioId.getNum());
 	const auto & mapContent = mapPieces.find(scenarioId)->second;
 	return mapService.loadMap(mapContent.data(), mapContent.size(), scenarioName, getModName(), getEncoding());
 }
@@ -334,7 +333,7 @@ std::unique_ptr<CMapHeader> CampaignState::getMapHeader(CampaignScenarioID scena
 	CMapService mapService;
 	std::string scenarioName = getFilename().substr(0, getFilename().find('.'));
 	boost::to_lower(scenarioName);
-	scenarioName += ':' + std::to_string(static_cast<int>(scenarioId));
+	scenarioName += ':' + std::to_string(scenarioId.getNum());
 	const auto & mapContent = mapPieces.find(scenarioId)->second;
 	return mapService.loadMapHeader(mapContent.data(), mapContent.size(), scenarioName, getModName(), getEncoding());
 }

+ 43 - 6
lib/constants/EntityIdentifiers.cpp

@@ -21,6 +21,7 @@
 #include <vcmi/HeroTypeService.h>
 #include <vcmi/HeroClass.h>
 #include <vcmi/HeroClassService.h>
+#include <vcmi/Services.h>
 
 #include <vcmi/spells/Spell.h>
 #include <vcmi/spells/Service.h>
@@ -28,6 +29,7 @@
 #include "modding/IdentifierStorage.h"
 #include "modding/ModScope.h"
 #include "VCMI_Lib.h"
+#include "CHeroHandler.h"
 #include "CArtHandler.h"//todo: remove
 #include "CCreatureHandler.h"//todo: remove
 #include "spells/CSpellHandler.h" //todo: remove
@@ -191,12 +193,12 @@ std::string HeroTypeID::entityType()
 
 const CArtifact * ArtifactIDBase::toArtifact() const
 {
-	return dynamic_cast<const CArtifact*>(toArtifact(VLC->artifacts()));
+	return dynamic_cast<const CArtifact*>(toEntity(VLC));
 }
 
-const Artifact * ArtifactIDBase::toArtifact(const ArtifactService * service) const
+const Artifact * ArtifactIDBase::toEntity(const Services * services) const
 {
-	return service->getByIndex(num);
+	return services->artifacts()->getByIndex(num);
 }
 
 si32 ArtifactID::decode(const std::string & identifier)
@@ -237,7 +239,12 @@ const CCreature * CreatureIDBase::toCreature() const
 	return VLC->creh->objects.at(num);
 }
 
-const Creature * CreatureIDBase::toCreature(const CreatureService * creatures) const
+const Creature * CreatureIDBase::toEntity(const Services * services) const
+{
+	return toEntity(services->creatures());
+}
+
+const Creature * CreatureIDBase::toEntity(const CreatureService * creatures) const
 {
 	return creatures->getByIndex(num);
 }
@@ -271,11 +278,26 @@ const CSpell * SpellIDBase::toSpell() const
 	return VLC->spellh->objects[num];
 }
 
-const spells::Spell * SpellIDBase::toSpell(const spells::Service * service) const
+const spells::Spell * SpellIDBase::toEntity(const Services * services) const
+{
+	return toEntity(services->spells());
+}
+
+const spells::Spell * SpellIDBase::toEntity(const spells::Service * service) const
 {
 	return service->getByIndex(num);
 }
 
+const CHero * HeroTypeID::toHeroType() const
+{
+	return dynamic_cast<const CHero*>(toEntity(VLC));
+}
+
+const HeroType * HeroTypeID::toEntity(const Services * services) const
+{
+	return services->heroTypes()->getByIndex(num);
+}
+
 si32 SpellID::decode(const std::string & identifier)
 {
 	auto rawId = VLC->identifiers()->getIdentifier(ModScope::scopeGame(), "spell", identifier);
@@ -369,7 +391,7 @@ si32 FactionID::decode(const std::string & identifier)
 	if(rawId)
 		return rawId.value();
 	else
-		return FactionID::DEFAULT;
+		return FactionID::DEFAULT.getNum();
 }
 
 std::string FactionID::encode(const si32 index)
@@ -458,6 +480,21 @@ std::string GameResID::entityType()
 	return "resource";
 }
 
+const std::array<GameResID, 7> & GameResID::ALL_RESOURCES()
+{
+	static const std::array allResources = {
+		GameResID(WOOD),
+		GameResID(MERCURY),
+		GameResID(ORE),
+		GameResID(SULFUR),
+		GameResID(CRYSTAL),
+		GameResID(GEMS),
+		GameResID(GOLD)
+	};
+
+	return allResources;
+}
+
 std::string SecondarySkill::entityType()
 {
 	return "secondarySkill";

+ 51 - 17
lib/constants/EntityIdentifiers.h

@@ -14,10 +14,14 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+class Services;
 class Artifact;
 class ArtifactService;
 class Creature;
 class CreatureService;
+class HeroType;
+class CHero;
+class HeroTypeService;
 
 namespace spells
 {
@@ -186,6 +190,9 @@ public:
 	DLL_LINKAGE static std::string encode(const si32 index);
 	static std::string entityType();
 
+	const CHero * toHeroType() const;
+	const HeroType * toEntity(const Services * services) const;
+
 	DLL_LINKAGE static const HeroTypeID NONE;
 	DLL_LINKAGE static const HeroTypeID RANDOM;
 
@@ -401,13 +408,9 @@ class BuildingID : public IdentifierWithEnum<BuildingID, BuildingIDBase>
 {
 public:
 	using IdentifierWithEnum<BuildingID, BuildingIDBase>::IdentifierWithEnum;
-
-	DLL_LINKAGE static si32 decode(const std::string & identifier);
-	DLL_LINKAGE static std::string encode(const si32 index);
-	static std::string entityType();
 };
 
-class ObjBase : public IdentifierBase
+class MapObjectBaseID : public IdentifierBase
 {
 public:
 	enum Type
@@ -552,15 +555,38 @@ public:
 	};
 };
 
-class DLL_LINKAGE Obj : public IdentifierWithEnum<Obj, ObjBase>
+class DLL_LINKAGE MapObjectID : public IdentifierWithEnum<MapObjectID, MapObjectBaseID>
 {
 public:
-	using IdentifierWithEnum<Obj, ObjBase>::IdentifierWithEnum;
+	using IdentifierWithEnum<MapObjectID, MapObjectBaseID>::IdentifierWithEnum;
 
 	static std::string encode(int32_t index);
 	static si32 decode(const std::string & identifier);
 };
 
+class MapObjectSubID : public Identifier<MapObjectSubID>
+{
+public:
+	constexpr MapObjectSubID(const IdentifierBase & value):
+		Identifier<MapObjectSubID>(value.getNum())
+	{}
+	constexpr MapObjectSubID(int32_t value = -1):
+		Identifier<MapObjectSubID>(value)
+	{}
+
+	MapObjectSubID & operator =(int32_t value)
+	{
+		this->num = value;
+		return *this;
+	}
+
+	MapObjectSubID & operator =(const IdentifierBase & value)
+	{
+		this->num = value.getNum();
+		return *this;
+	}
+};
+
 class DLL_LINKAGE RoadId : public Identifier<RoadId>
 {
 public:
@@ -607,20 +633,23 @@ public:
 		TRANSITION_POS = -3,
 		FIRST_AVAILABLE = -2,
 		PRE_FIRST = -1, //sometimes used as error, sometimes as first free in backpack
+		
+		// Hero
 		HEAD, SHOULDERS, NECK, RIGHT_HAND, LEFT_HAND, TORSO, //5
 		RIGHT_RING, LEFT_RING, FEET, //8
 		MISC1, MISC2, MISC3, MISC4, //12
 		MACH1, MACH2, MACH3, MACH4, //16
 		SPELLBOOK, MISC5, //18
-		AFTER_LAST,
-		//cres
+		BACKPACK_START = 19,
+		
+		// Creatures
 		CREATURE_SLOT = 0,
-		COMMANDER1 = 0, COMMANDER2, COMMANDER3, COMMANDER4, COMMANDER5, COMMANDER6, COMMANDER_AFTER_LAST,
-
-		BACKPACK_START = 19
+		
+		// Commander
+		COMMANDER1 = 0, COMMANDER2, COMMANDER3, COMMANDER4, COMMANDER5, COMMANDER6
 	};
 
-	static_assert (AFTER_LAST == BACKPACK_START, "incorrect number of artifact slots");
+	static_assert(MISC5 < BACKPACK_START, "incorrect number of artifact slots");
 
 	DLL_LINKAGE static si32 decode(const std::string & identifier);
 	DLL_LINKAGE static std::string encode(const si32 index);
@@ -653,7 +682,7 @@ public:
 	};
 
 	DLL_LINKAGE const CArtifact * toArtifact() const;
-	DLL_LINKAGE const Artifact * toArtifact(const ArtifactService * service) const;
+	DLL_LINKAGE const Artifact * toEntity(const Services * service) const;
 };
 
 class ArtifactID : public IdentifierWithEnum<ArtifactID, ArtifactIDBase>
@@ -693,7 +722,8 @@ public:
 	};
 
 	DLL_LINKAGE const CCreature * toCreature() const;
-	DLL_LINKAGE const Creature * toCreature(const CreatureService * creatures) const;
+	DLL_LINKAGE const Creature * toEntity(const Services * services) const;
+	DLL_LINKAGE const Creature * toEntity(const CreatureService * creatures) const;
 };
 
 class DLL_LINKAGE CreatureID : public IdentifierWithEnum<CreatureID, CreatureIDBase>
@@ -811,7 +841,8 @@ public:
 	};
 
 	const CSpell * toSpell() const; //deprecated
-	const spells::Spell * toSpell(const spells::Service * service) const;
+	const spells::Spell * toEntity(const Services * service) const;
+	const spells::Spell * toEntity(const spells::Service * service) const;
 };
 
 class DLL_LINKAGE SpellID : public IdentifierWithEnum<SpellID, SpellIDBase>
@@ -934,9 +965,11 @@ public:
 	static si32 decode(const std::string & identifier);
 	static std::string encode(const si32 index);
 	static std::string entityType();
+
+	static const std::array<GameResID, 7> & ALL_RESOURCES();
 };
 
-class BuildingTypeUniqueID : public Identifier<BuildingTypeUniqueID>
+class DLL_LINKAGE BuildingTypeUniqueID : public Identifier<BuildingTypeUniqueID>
 {
 public:
 	BuildingTypeUniqueID(FactionID faction, BuildingID building );
@@ -963,6 +996,7 @@ public:
 
 // Deprecated
 // TODO: remove
+using Obj = MapObjectID;
 using ETownType = FactionID;
 using EGameResID = GameResID;
 using River = RiverId;

+ 1 - 1
lib/constants/VariantIdentifier.h

@@ -15,7 +15,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 /// This class represents field that may contain value of multiple different identifer types
 template<typename... Types>
-class DLL_LINKAGE VariantIdentifier
+class VariantIdentifier
 {
 	std::variant<Types...> value;
 public:

+ 50 - 285
lib/gameState/CGameState.cpp

@@ -77,34 +77,6 @@ public:
 	}
 };
 
-static CGObjectInstance * createObject(const Obj & id, int subid, const int3 & pos, const PlayerColor & owner)
-{
-	CGObjectInstance * nobj;
-	switch(id)
-	{
-	case Obj::HERO:
-		{
-			auto handler = VLC->objtypeh->getHandlerFor(id, VLC->heroh->objects[subid]->heroClass->getIndex());
-			nobj = handler->create(handler->getTemplates().front());
-			break;
-		}
-	case Obj::TOWN:
-		nobj = new CGTownInstance();
-		break;
-	default: //rest of objects
-		nobj = new CGObjectInstance();
-		break;
-	}
-	nobj->ID = id;
-	nobj->subID = subid;
-	nobj->pos = pos;
-	nobj->tempOwner = owner;
-	if (id != Obj::HERO)
-		nobj->appearance = VLC->objtypeh->getHandlerFor(id, subid)->getTemplates().front();
-
-	return nobj;
-}
-
 HeroTypeID CGameState::pickNextHeroType(const PlayerColor & owner)
 {
 	const PlayerSettings &ps = scenarioOps->getIthPlayersSettings(owner);
@@ -153,223 +125,6 @@ HeroTypeID CGameState::pickUnusedHeroTypeRandomly(const PlayerColor & owner)
 	return HeroTypeID::NONE; // no available heroes at all
 }
 
-std::pair<Obj,int> CGameState::pickObject (CGObjectInstance *obj)
-{
-	switch(obj->ID)
-	{
-	case Obj::RANDOM_ART:
-		return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE | CArtifact::ART_MINOR | CArtifact::ART_MAJOR | CArtifact::ART_RELIC));
-	case Obj::RANDOM_TREASURE_ART:
-		return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE));
-	case Obj::RANDOM_MINOR_ART:
-		return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_MINOR));
-	case Obj::RANDOM_MAJOR_ART:
-		return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_MAJOR));
-	case Obj::RANDOM_RELIC_ART:
-		return std::make_pair(Obj::ARTIFACT, VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_RELIC));
-	case Obj::RANDOM_HERO:
-		return std::make_pair(Obj::HERO, pickNextHeroType(obj->tempOwner));
-	case Obj::RANDOM_MONSTER:
-		return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator()));
-	case Obj::RANDOM_MONSTER_L1:
-		return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 1));
-	case Obj::RANDOM_MONSTER_L2:
-		return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 2));
-	case Obj::RANDOM_MONSTER_L3:
-		return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 3));
-	case Obj::RANDOM_MONSTER_L4:
-		return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 4));
-	case Obj::RANDOM_RESOURCE:
-		return std::make_pair(Obj::RESOURCE,getRandomGenerator().nextInt(6)); //now it's OH3 style, use %8 for mithril
-	case Obj::RANDOM_TOWN:
-		{
-			PlayerColor align = (dynamic_cast<CGTownInstance *>(obj))->alignmentToPlayer;
-			si32 f; // can be negative (for random)
-			if(!align.isValidPlayer()) //same as owner / random
-			{
-				if(!obj->tempOwner.isValidPlayer())
-					f = -1; //random
-				else
-					f = scenarioOps->getIthPlayersSettings(obj->tempOwner).castle;
-			}
-			else
-			{
-				f = scenarioOps->getIthPlayersSettings(align).castle;
-			}
-			if(f<0)
-			{
-				do
-				{
-					f = getRandomGenerator().nextInt((int)VLC->townh->size() - 1);
-				}
-				while ((*VLC->townh)[f]->town == nullptr); // find playable faction
-			}
-			return std::make_pair(Obj::TOWN,f);
-		}
-	case Obj::RANDOM_MONSTER_L5:
-		return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 5));
-	case Obj::RANDOM_MONSTER_L6:
-		return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 6));
-	case Obj::RANDOM_MONSTER_L7:
-		return std::make_pair(Obj::MONSTER, VLC->creh->pickRandomMonster(getRandomGenerator(), 7));
-	case Obj::RANDOM_DWELLING:
-	case Obj::RANDOM_DWELLING_LVL:
-	case Obj::RANDOM_DWELLING_FACTION:
-		{
-			auto * dwl = dynamic_cast<CGDwelling *>(obj);
-			int faction;
-
-			//if castle alignment available
-			if(auto * info = dynamic_cast<CCreGenAsCastleInfo *>(dwl->info))
-			{
-				faction = getRandomGenerator().nextInt((int)VLC->townh->size() - 1);
-				if(info->asCastle && !info->instanceId.empty())
-				{
-					auto iter = map->instanceNames.find(info->instanceId);
-
-					if(iter == map->instanceNames.end())
-						logGlobal->error("Map object not found: %s", info->instanceId);
-					else
-					{
-						auto elem = iter->second;
-						if(elem->ID==Obj::RANDOM_TOWN)
-						{
-							randomizeObject(elem.get()); //we have to randomize the castle first
-							faction = elem->subID;
-						}
-						else if(elem->ID==Obj::TOWN)
-							faction = elem->subID;
-						else
-							logGlobal->error("Map object must be town: %s", info->instanceId);
-					}
-				}
-				else if(info->asCastle)
-				{
-
-					for(auto & elem : map->objects)
-					{
-						if(!elem)
-							continue;
-
-						if(elem->ID==Obj::RANDOM_TOWN
-							&& dynamic_cast<CGTownInstance*>(elem.get())->identifier == info->identifier)
-						{
-							randomizeObject(elem); //we have to randomize the castle first
-							faction = elem->subID;
-							break;
-						}
-						else if(elem->ID==Obj::TOWN
-							&& dynamic_cast<CGTownInstance*>(elem.get())->identifier == info->identifier)
-						{
-							faction = elem->subID;
-							break;
-						}
-					}
-				}
-				else
-				{
-					std::set<int> temp;
-
-					for(int i = 0; i < info->allowedFactions.size(); i++)
-						if(info->allowedFactions[i])
-							temp.insert(i);
-
-					if(temp.empty())
-						logGlobal->error("Random faction selection failed. Nothing is allowed. Fall back to random.");
-					else
-						faction = *RandomGeneratorUtil::nextItem(temp, getRandomGenerator());
-				}
-			}
-			else // castle alignment fixed
-				faction = obj->subID;
-
-			int level;
-
-			//if level set to range
-			if(auto * info = dynamic_cast<CCreGenLeveledInfo *>(dwl->info))
-			{
-				level = getRandomGenerator().nextInt(info->minLevel, info->maxLevel) - 1;
-			}
-			else // fixed level
-			{
-				level = obj->subID;
-			}
-
-			delete dwl->info;
-			dwl->info = nullptr;
-
-			std::pair<Obj, int> result(Obj::NO_OBJ, -1);
-			CreatureID cid;
-			if((*VLC->townh)[faction]->town)
-				cid = (*VLC->townh)[faction]->town->creatures[level][0];
-			else
-			{
-				//neutral faction
-				std::vector<CCreature*> possibleCreatures;
-				std::copy_if(VLC->creh->objects.begin(), VLC->creh->objects.end(), std::back_inserter(possibleCreatures), [faction](const CCreature * c)
-				{
-					return c->getFaction().getNum() == faction;
-				});
-				assert(!possibleCreatures.empty());
-				cid = (*RandomGeneratorUtil::nextItem(possibleCreatures, getRandomGenerator()))->getId();
-			}
-
-			//NOTE: this will pick last dwelling with this creature (Mantis #900)
-			//check for block map equality is better but more complex solution
-			auto testID = [&](const Obj & primaryID) -> void
-			{
-				auto dwellingIDs = VLC->objtypeh->knownSubObjects(primaryID);
-				for (si32 entry : dwellingIDs)
-				{
-					const auto * handler = dynamic_cast<const DwellingInstanceConstructor *>(VLC->objtypeh->getHandlerFor(primaryID, entry).get());
-
-					if (handler->producesCreature(VLC->creh->objects[cid]))
-						result = std::make_pair(primaryID, entry);
-				}
-			};
-
-			testID(Obj::CREATURE_GENERATOR1);
-			if (result.first == Obj::NO_OBJ)
-				testID(Obj::CREATURE_GENERATOR4);
-
-			if (result.first == Obj::NO_OBJ)
-			{
-				logGlobal->error("Error: failed to find dwelling for %s of level %d", (*VLC->townh)[faction]->getNameTranslated(), int(level));
-				result = std::make_pair(Obj::CREATURE_GENERATOR1, *RandomGeneratorUtil::nextItem(VLC->objtypeh->knownSubObjects(Obj::CREATURE_GENERATOR1), getRandomGenerator()));
-			}
-
-			return result;
-		}
-	}
-	return std::make_pair(Obj::NO_OBJ,-1);
-}
-
-void CGameState::randomizeObject(CGObjectInstance *cur)
-{
-	std::pair<Obj,int> ran = pickObject(cur);
-	if(ran.first == Obj::NO_OBJ || ran.second<0) //this is not a random object, or we couldn't find anything
-	{
-		if(cur->ID==Obj::TOWN || cur->ID==Obj::MONSTER)
-			cur->setType(cur->ID, cur->subID); // update def, if necessary
-	}
-	else if(ran.first==Obj::HERO)//special code for hero
-	{
-		auto * h = dynamic_cast<CGHeroInstance *>(cur);
-		cur->setType(ran.first, ran.second);
-		map->heroesOnMap.emplace_back(h);
-	}
-	else if(ran.first==Obj::TOWN)//special code for town
-	{
-		auto * t = dynamic_cast<CGTownInstance *>(cur);
-		cur->setType(ran.first, ran.second);
-		map->towns.emplace_back(t);
-	}
-	else
-	{
-		cur->setType(ran.first, ran.second);
-	}
-}
-
 int CGameState::getDate(Date mode) const
 {
 	int temp;
@@ -409,6 +164,8 @@ CGameState::CGameState()
 
 CGameState::~CGameState()
 {
+	// explicitly delete all ongoing battles first - BattleInfo destructor requires valid CGameState
+	currentBattles.clear();
 	map.dellNull();
 }
 
@@ -469,7 +226,7 @@ void CGameState::init(const IMapService * mapService, StartInfo * si, Load::Prog
 	// Explicitly initialize static variables
 	for(auto & elem : players)
 	{
-		CGKeys::playerKeyMap[elem.first] = std::set<ui8>();
+		CGKeys::playerKeyMap[elem.first] = std::set<MapObjectSubID>();
 	}
 	for(auto & elem : teams)
 	{
@@ -762,20 +519,21 @@ void CGameState::initRandomFactionsForPlayers()
 void CGameState::randomizeMapObjects()
 {
 	logGlobal->debug("\tRandomizing objects");
-	for(CGObjectInstance *obj : map->objects)
+	for(CGObjectInstance *object : map->objects)
 	{
-		if(!obj) continue;
+		if(!object)
+			continue;
 
-		randomizeObject(obj);
+		object->pickRandomObject(getRandomGenerator());
 
 		//handle Favouring Winds - mark tiles under it
-		if(obj->ID == Obj::FAVORABLE_WINDS)
+		if(object->ID == Obj::FAVORABLE_WINDS)
 		{
-			for (int i = 0; i < obj->getWidth() ; i++)
+			for (int i = 0; i < object->getWidth() ; i++)
 			{
-				for (int j = 0; j < obj->getHeight() ; j++)
+				for (int j = 0; j < object->getHeight() ; j++)
 				{
-					int3 pos = obj->pos - int3(i,j,0);
+					int3 pos = object->pos - int3(i,j,0);
 					if(map->isInTheMap(pos)) map->getTile(pos).extTileFlags |= 128;
 				}
 			}
@@ -808,7 +566,15 @@ void CGameState::placeStartingHero(const PlayerColor & playerColor, const HeroTy
 		}
 	}
 
-	CGObjectInstance * hero = createObject(Obj::HERO, heroTypeId.getNum(), townPos, playerColor);
+	auto handler = VLC->objtypeh->getHandlerFor(Obj::HERO, heroTypeId.toHeroType()->heroClass->getIndex());
+	CGObjectInstance * obj = handler->create(handler->getTemplates().front());
+	CGHeroInstance * hero = dynamic_cast<CGHeroInstance *>(obj);
+
+	hero->ID = Obj::HERO;
+	hero->setHeroType(heroTypeId);
+	hero->tempOwner = playerColor;
+
+	hero->pos = townPos;
 	hero->pos += hero->getVisitableOffset();
 	map->getEditManager()->insertObject(hero);
 }
@@ -864,7 +630,7 @@ void CGameState::initHeroes()
 
 		hero->initHero(getRandomGenerator());
 		getPlayerState(hero->getOwner())->heroes.push_back(hero);
-		map->allHeroes[hero->type->getIndex()] = hero;
+		map->allHeroes[hero->getHeroType().getNum()] = hero;
 	}
 
 	// generate boats for all heroes on water
@@ -878,8 +644,6 @@ void CGameState::initHeroes()
 			CGBoat * boat = dynamic_cast<CGBoat*>(handler->create());
 			handler->configureObject(boat, gs->getRandomGenerator());
 
-			boat->ID = Obj::BOAT;
-			boat->subID = hero->getBoatType().getNum();
 			boat->pos = hero->pos;
 			boat->appearance = handler->getTemplates().front();
 			boat->id = ObjectInstanceID(static_cast<si32>(gs->map->objects.size()));
@@ -894,19 +658,23 @@ void CGameState::initHeroes()
 	for(auto obj : map->objects) //prisons
 	{
 		if(obj && obj->ID == Obj::PRISON)
-			map->allHeroes[obj->subID] = dynamic_cast<CGHeroInstance*>(obj.get());
+		{
+			auto * hero = dynamic_cast<CGHeroInstance*>(obj.get());
+			hero->initHero(getRandomGenerator());
+			map->allHeroes[hero->getHeroType().getNum()] = hero;
+		}
 	}
 
 	std::set<HeroTypeID> heroesToCreate = getUnusedAllowedHeroes(); //ids of heroes to be created and put into the pool
 	for(auto ph : map->predefinedHeroes)
 	{
-		if(!vstd::contains(heroesToCreate, HeroTypeID(ph->subID)))
+		if(!vstd::contains(heroesToCreate, ph->getHeroType()))
 			continue;
 		ph->initHero(getRandomGenerator());
 		heroesPool->addHeroToPool(ph);
 		heroesToCreate.erase(ph->type->getId());
 
-		map->allHeroes[ph->subID] = ph;
+		map->allHeroes[ph->getHeroType().getNum()] = ph;
 	}
 
 	for(const HeroTypeID & htype : heroesToCreate) //all not used allowed heroes go with default state into the pool
@@ -992,7 +760,7 @@ void CGameState::initStartingBonus()
 					logGlobal->error("Cannot give starting artifact - no heroes!");
 					break;
 				}
-				const Artifact * toGive = VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE).toArtifact(VLC->artifacts());
+				const Artifact * toGive = VLC->arth->pickRandomArtifact(getRandomGenerator(), CArtifact::ART_TREASURE).toEntity(VLC);
 
 				CGHeroInstance *hero = elem.second.heroes[0];
 				if(!giveHeroArtifact(hero, toGive->getId()))
@@ -1017,10 +785,8 @@ void CGameState::initTowns()
 	for (auto & elem : map->towns)
 	{
 		CGTownInstance * vti =(elem);
-		if(!vti->town)
-		{
-			vti->town = (*VLC->townh)[vti->subID]->town;
-		}
+		assert(vti->town);
+
 		if(vti->getNameTranslated().empty())
 		{
 			size_t nameID = getRandomGenerator().nextInt(vti->getTown()->getRandomNamesCount() - 1);
@@ -1165,7 +931,7 @@ void CGameState::initMapObjects()
 		if(!obj)
 			continue;
 
-		switch (obj->ID)
+		switch(obj->ID.toEnum())
 		{
 			case Obj::QUEST_GUARD:
 			case Obj::SEER_HUT:
@@ -2103,7 +1869,7 @@ bool CGameState::giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid)
 	 auto slot = ArtifactUtils::getArtAnyPosition(h, aid);
 	 if(ArtifactUtils::isSlotEquipment(slot) || ArtifactUtils::isSlotBackpack(slot))
 	 {
-		 ai->putAt(ArtifactLocation(h, slot));
+		 ai->putAt(*h, slot);
 		 return true;
 	 }
 	 else
@@ -2130,12 +1896,15 @@ std::set<HeroTypeID> CGameState::getUnusedAllowedHeroes(bool alsoIncludeNotAllow
 		if(hero->type)
 			ret -= hero->type->getId();
 		else
-			ret -= HeroTypeID(hero->subID);
+			ret -= hero->getHeroType();
 	}
 
 	for(auto obj : map->objects) //prisons
-		if(obj && obj->ID == Obj::PRISON)
-			ret -= HeroTypeID(obj->subID);
+	{
+		auto * hero = dynamic_cast<const CGHeroInstance *>(obj.get());
+		if(hero && hero->ID == Obj::PRISON)
+			ret -= hero->getHeroType();
+	}
 
 	return ret;
 }
@@ -2147,23 +1916,19 @@ bool CGameState::isUsedHero(const HeroTypeID & hid) const
 
 CGHeroInstance * CGameState::getUsedHero(const HeroTypeID & hid) const
 {
-	for(auto hero : map->heroesOnMap)  //heroes instances initialization
-	{
-		if(hero->type && hero->type->getId() == hid)
-		{
-			return hero;
-		}
-	}
-
 	for(auto obj : map->objects) //prisons
 	{
-		if(obj && obj->ID == Obj::PRISON )
-		{
-			auto * hero = dynamic_cast<CGHeroInstance *>(obj.get());
-			assert(hero);
-			if ( hero->type && hero->type->getId() == hid )
-				return hero;
-		}
+		if (!obj)
+			continue;
+
+		if ( obj->ID !=Obj::PRISON && obj->ID != Obj::HERO)
+			continue;
+
+		auto * hero = dynamic_cast<CGHeroInstance *>(obj.get());
+		assert(hero);
+
+		if (hero->getHeroType() == hid)
+			return hero;
 	}
 
 	return nullptr;

+ 2 - 3
lib/gameState/CGameState.h

@@ -115,6 +115,8 @@ public:
 	void updateEntity(Metatype metatype, int32_t index, const JsonNode & data) override;
 
 	bool giveHeroArtifact(CGHeroInstance * h, const ArtifactID & aid);
+	/// picks next free hero type of the H3 hero init sequence -> chosen starting hero, then unused hero type randomly
+	HeroTypeID pickNextHeroType(const PlayerColor & owner);
 
 	void apply(CPack *pack);
 	BattleField battleGetBattlefieldType(int3 tile, CRandomGenerator & rand);
@@ -187,7 +189,6 @@ private:
 	void initGrailPosition();
 	void initRandomFactionsForPlayers();
 	void randomizeMapObjects();
-	void randomizeObject(CGObjectInstance *cur);
 	void initPlayerStates();
 	void placeStartingHeroes();
 	void placeStartingHero(const PlayerColor & playerColor, const HeroTypeID & heroTypeId, int3 townPos);
@@ -214,9 +215,7 @@ private:
 	CGHeroInstance * getUsedHero(const HeroTypeID & hid) const;
 	bool isUsedHero(const HeroTypeID & hid) const; //looks in heroes and prisons
 	std::set<HeroTypeID> getUnusedAllowedHeroes(bool alsoIncludeNotAllowed = false) const;
-	std::pair<Obj,int> pickObject(CGObjectInstance *obj); //chooses type of object to be randomized, returns <type, subtype>
 	HeroTypeID pickUnusedHeroTypeRandomly(const PlayerColor & owner); // picks a unused hero type randomly
-	HeroTypeID pickNextHeroType(const PlayerColor & owner); // picks next free hero type of the H3 hero init sequence -> chosen starting hero, then unused hero type randomly
 	UpgradeInfo fillUpgradeInfo(const CStackInstance &stack) const;
 
 	// ---- data -----

+ 14 - 14
lib/gameState/CGameStateCampaign.cpp

@@ -90,7 +90,7 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vector<CampaignHeroR
 					.And(Selector::subtype()(BonusSubtypeID(g)))
 					.And(Selector::sourceType()(BonusSource::HERO_BASE_SKILL));
 
-				cgh->getBonusLocalFirst(sel)->val = cgh->type->heroClass->primarySkillInitial[g];
+				cgh->getBonusLocalFirst(sel)->val = cgh->type->heroClass->primarySkillInitial[g.getNum()];
 			}
 		}
 	}
@@ -134,9 +134,9 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vector<CampaignHeroR
 
 				bool takeable = travelOptions.artifactsKeptByHero.count(art->artType->getId());
 
-				ArtifactLocation al(hero, artifactPosition);
-				if(!takeable  &&  !al.getSlot()->locked)  //don't try removing locked artifacts -> it crashes #1719
-					al.removeArtifact();
+				ArtifactLocation al(hero->id, artifactPosition);
+				if(!takeable && !hero->getSlot(al.slot)->locked)  //don't try removing locked artifacts -> it crashes #1719
+					hero->getArt(al.slot)->removeFrom(*hero, al.slot);
 			};
 
 			// process on copy - removal of artifact will invalidate container
@@ -213,7 +213,7 @@ void CGameStateCampaign::placeCampaignHeroes()
 	std::set<HeroTypeID> heroesToRemove = campaignState->getReservedHeroes();
 
 	for(auto & campaignHeroReplacement : campaignHeroReplacements)
-		heroesToRemove.insert(HeroTypeID(campaignHeroReplacement.hero->subID));
+		heroesToRemove.insert(campaignHeroReplacement.hero->getHeroType());
 
 	for(auto & heroID : heroesToRemove)
 	{
@@ -256,7 +256,7 @@ void CGameStateCampaign::placeCampaignHeroes()
 			assert(0); // should not happen
 		}
 
-		hero->subID = heroTypeId;
+		hero->setHeroType(heroTypeId);
 		gameState->map->getEditManager()->insertObject(hero);
 	}
 }
@@ -300,7 +300,7 @@ void CGameStateCampaign::giveCampaignBonusToHero(CGHeroInstance * hero)
 			CArtifactInstance * scroll = ArtifactUtils::createScroll(SpellID(curBonus->info2));
 			const auto slot = ArtifactUtils::getArtAnyPosition(hero, scroll->getTypeId());
 			if(ArtifactUtils::isSlotEquipment(slot) || ArtifactUtils::isSlotBackpack(slot))
-				scroll->putAt(ArtifactLocation(hero, slot));
+				scroll->putAt(*hero, slot);
 			else
 				logGlobal->error("Cannot give starting scroll - no free slots!");
 			break;
@@ -339,7 +339,7 @@ void CGameStateCampaign::replaceHeroesPlaceholders(const std::vector<CampaignHer
 		if(heroPlaceholder->tempOwner.isValidPlayer())
 			heroToPlace->tempOwner = heroPlaceholder->tempOwner;
 		heroToPlace->pos = heroPlaceholder->pos;
-		heroToPlace->type = VLC->heroh->objects[heroToPlace->subID];
+		heroToPlace->type = VLC->heroh->objects[heroToPlace->getHeroType().getNum()];
 		heroToPlace->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, heroToPlace->type->heroClass->getIndex())->getTemplates().front();
 
 		gameState->map->removeBlockVisTiles(heroPlaceholder, true);
@@ -375,7 +375,7 @@ std::vector<CampaignHeroReplacement> CGameStateCampaign::generateCampaignHeroesT
 		auto * heroPlaceholder = dynamic_cast<CGHeroPlaceholder *>(obj.get());
 
 		// only 1 field must be set
-		assert(heroPlaceholder->powerRank != heroPlaceholder->heroType);
+		assert(heroPlaceholder->powerRank.has_value() != heroPlaceholder->heroType.has_value());
 
 		if(heroPlaceholder->powerRank)
 			placeholdersByPower.push_back(heroPlaceholder);
@@ -396,7 +396,7 @@ std::vector<CampaignHeroReplacement> CGameStateCampaign::generateCampaignHeroesT
 
 		CGHeroInstance * hero = CampaignState::crossoverDeserialize(node, gameState->map);
 
-		logGlobal->info("Hero crossover: Loading placeholder for %d (%s)", hero->subID, hero->getNameTranslated());
+		logGlobal->info("Hero crossover: Loading placeholder for %d (%s)", hero->getHeroType(), hero->getNameTranslated());
 
 		campaignHeroReplacements.emplace_back(hero, placeholder->id);
 	}
@@ -422,7 +422,7 @@ std::vector<CampaignHeroReplacement> CGameStateCampaign::generateCampaignHeroesT
 			CGHeroInstance * hero = CampaignState::crossoverDeserialize(*nodeListIter, gameState->map);
 			nodeListIter++;
 
-			logGlobal->info("Hero crossover: Loading placeholder as %d (%s)", hero->subID, hero->getNameTranslated());
+			logGlobal->info("Hero crossover: Loading placeholder as %d (%s)", hero->getHeroType(), hero->getNameTranslated());
 
 			campaignHeroReplacements.emplace_back(hero, placeholder->id);
 		}
@@ -468,7 +468,7 @@ void CGameStateCampaign::initHeroes()
 		{
 			for (auto & heroe : heroes)
 			{
-				if (heroe->subID == chosenBonus->info1)
+				if (heroe->getHeroType().getNum() == chosenBonus->info1)
 				{
 					giveCampaignBonusToHero(heroe);
 					break;
@@ -498,7 +498,7 @@ void CGameStateCampaign::initStartingResources()
 		std::vector<const PlayerSettings *> people = getHumanPlayerInfo(); //players we will give resource bonus
 		for(const PlayerSettings *ps : people)
 		{
-			std::vector<int> res; //resources we will give
+			std::vector<GameResID> res; //resources we will give
 			switch (chosenBonus->info1)
 			{
 				case 0: case 1: case 2: case 3: case 4: case 5: case 6:
@@ -557,7 +557,7 @@ void CGameStateCampaign::initTowns()
 		if(gameState->scenarioOps->campState->formatVCMI())
 			newBuilding = BuildingID(chosenBonus->info1);
 		else
-			newBuilding = CBuildingHandler::campToERMU(chosenBonus->info1, town->subID, town->builtBuildings);
+			newBuilding = CBuildingHandler::campToERMU(chosenBonus->info1, town->getFaction(), town->builtBuildings);
 
 		// Build granted building & all prerequisites - e.g. Mages Guild Lvl 3 should also give Mages Guild Lvl 1 & 2
 		while(true)

+ 3 - 3
lib/gameState/TavernHeroesPool.cpp

@@ -25,7 +25,7 @@ std::map<HeroTypeID, CGHeroInstance*> TavernHeroesPool::unusedHeroesFromPool() c
 {
 	std::map<HeroTypeID, CGHeroInstance*> pool = heroesPool;
 	for(const auto & slot : currentTavern)
-		pool.erase(HeroTypeID(slot.hero->subID));
+		pool.erase(slot.hero->getHeroType());
 
 	return pool;
 }
@@ -34,7 +34,7 @@ TavernSlotRole TavernHeroesPool::getSlotRole(HeroTypeID hero) const
 {
 	for (auto const & slot : currentTavern)
 	{
-		if (HeroTypeID(slot.hero->subID) == hero)
+		if (slot.hero->getHeroType() == hero)
 			return slot.role;
 	}
 	return TavernSlotRole::NONE;
@@ -132,7 +132,7 @@ void TavernHeroesPool::onNewDay()
 
 void TavernHeroesPool::addHeroToPool(CGHeroInstance * hero)
 {
-	heroesPool[HeroTypeID(hero->subID)] = hero;
+	heroesPool[hero->getHeroType()] = hero;
 }
 
 void TavernHeroesPool::setAvailability(HeroTypeID hero, std::set<PlayerColor> mask)

+ 11 - 14
lib/mapObjectConstructors/CObjectClassesHandler.cpp

@@ -285,10 +285,9 @@ void CObjectClassesHandler::loadObject(std::string scope, std::string name, cons
 	VLC->identifiersHandler->registerObject(scope, "object", name, object->id);
 }
 
-void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNode config, si32 ID, si32 subID)
+void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNode config, MapObjectID ID, MapObjectSubID subID)
 {
 	config.setType(JsonNode::JsonType::DATA_STRUCT); // ensure that input is not NULL
-	assert(ID < objects.size());
 	assert(objects[ID]);
 
 	if ( subID >= objects[ID]->objects.size())
@@ -298,9 +297,8 @@ void CObjectClassesHandler::loadSubObject(const std::string & identifier, JsonNo
 	loadSubObject(config.meta, identifier, config, objects[ID], subID);
 }
 
-void CObjectClassesHandler::removeSubObject(si32 ID, si32 subID)
+void CObjectClassesHandler::removeSubObject(MapObjectID ID, MapObjectSubID subID)
 {
-	assert(ID < objects.size());
 	assert(objects[ID]);
 	assert(subID < objects[ID]->objects.size());
 	objects[ID]->objects[subID] = nullptr;
@@ -311,7 +309,7 @@ std::vector<bool> CObjectClassesHandler::getDefaultAllowed() const
 	return std::vector<bool>(); //TODO?
 }
 
-TObjectTypeHandler CObjectClassesHandler::getHandlerFor(si32 type, si32 subtype) const
+TObjectTypeHandler CObjectClassesHandler::getHandlerFor(MapObjectID type, MapObjectSubID subtype) const
 {
 	try
 	{
@@ -352,9 +350,9 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(CompoundMapObjectID comp
 	return getHandlerFor(compoundIdentifier.primaryID, compoundIdentifier.secondaryID);
 }
 
-std::set<si32> CObjectClassesHandler::knownObjects() const
+std::set<MapObjectID> CObjectClassesHandler::knownObjects() const
 {
-	std::set<si32> ret;
+	std::set<MapObjectID> ret;
 
 	for(auto * entry : objects)
 		if (entry)
@@ -363,9 +361,9 @@ std::set<si32> CObjectClassesHandler::knownObjects() const
 	return ret;
 }
 
-std::set<si32> CObjectClassesHandler::knownSubObjects(si32 primaryID) const
+std::set<MapObjectSubID> CObjectClassesHandler::knownSubObjects(MapObjectID primaryID) const
 {
-	std::set<si32> ret;
+	std::set<MapObjectSubID> ret;
 
 	if (!objects.at(primaryID))
 	{
@@ -461,7 +459,7 @@ void CObjectClassesHandler::generateExtraMonolithsForRMG()
 	}
 }
 
-std::string CObjectClassesHandler::getObjectName(si32 type, si32 subtype) const
+std::string CObjectClassesHandler::getObjectName(MapObjectID type, MapObjectSubID subtype) const
 {
 	const auto handler = getHandlerFor(type, subtype);
 	if (handler && handler->hasNameTextID())
@@ -470,7 +468,7 @@ std::string CObjectClassesHandler::getObjectName(si32 type, si32 subtype) const
 		return objects[type]->getNameTranslated();
 }
 
-SObjectSounds CObjectClassesHandler::getObjectSounds(si32 type, si32 subtype) const
+SObjectSounds CObjectClassesHandler::getObjectSounds(MapObjectID type, MapObjectSubID subtype) const
 {
 	// TODO: these objects may have subID's that does not have associated handler:
 	// Prison: uses hero type as subID
@@ -479,19 +477,18 @@ SObjectSounds CObjectClassesHandler::getObjectSounds(si32 type, si32 subtype) co
 	if(type == Obj::PRISON || type == Obj::HERO || type == Obj::SPELL_SCROLL)
 		subtype = 0;
 
-	assert(type < objects.size());
 	assert(objects[type]);
 	assert(subtype < objects[type]->objects.size());
 
 	return getHandlerFor(type, subtype)->getSounds();
 }
 
-std::string CObjectClassesHandler::getObjectHandlerName(si32 type) const
+std::string CObjectClassesHandler::getObjectHandlerName(MapObjectID type) const
 {
 	return objects.at(type)->handlerName;
 }
 
-std::string CObjectClassesHandler::getJsonKey(si32 type) const
+std::string CObjectClassesHandler::getJsonKey(MapObjectID type) const
 {
 	return objects.at(type)->getJsonKey();
 }

+ 9 - 9
lib/mapObjectConstructors/CObjectClassesHandler.h

@@ -105,8 +105,8 @@ public:
 	void loadObject(std::string scope, std::string name, const JsonNode & data) override;
 	void loadObject(std::string scope, std::string name, const JsonNode & data, size_t index) override;
 
-	void loadSubObject(const std::string & identifier, JsonNode config, si32 ID, si32 subID);
-	void removeSubObject(si32 ID, si32 subID);
+	void loadSubObject(const std::string & identifier, JsonNode config, MapObjectID ID, MapObjectSubID subID);
+	void removeSubObject(MapObjectID ID, MapObjectSubID subID);
 
 	void beforeValidate(JsonNode & object) override;
 	void afterLoadFinalization() override;
@@ -114,22 +114,22 @@ public:
 	std::vector<bool> getDefaultAllowed() const override;
 
 	/// Queries to detect loaded objects
-	std::set<si32> knownObjects() const;
-	std::set<si32> knownSubObjects(si32 primaryID) const;
+	std::set<MapObjectID> knownObjects() const;
+	std::set<MapObjectSubID> knownSubObjects(MapObjectID primaryID) const;
 
 	/// returns handler for specified object (ID-based). ObjectHandler keeps ownership
-	TObjectTypeHandler getHandlerFor(si32 type, si32 subtype) const;
+	TObjectTypeHandler getHandlerFor(MapObjectID type, MapObjectSubID subtype) const;
 	TObjectTypeHandler getHandlerFor(const std::string & scope, const std::string & type, const std::string & subtype) const;
 	TObjectTypeHandler getHandlerFor(CompoundMapObjectID compoundIdentifier) const;
 
-	std::string getObjectName(si32 type, si32 subtype) const;
+	std::string getObjectName(MapObjectID type, MapObjectSubID subtype) const;
 
-	SObjectSounds getObjectSounds(si32 type, si32 subtype) const;
+	SObjectSounds getObjectSounds(MapObjectID type, MapObjectSubID subtype) const;
 
 	/// Returns handler string describing the handler (for use in client)
-	std::string getObjectHandlerName(si32 type) const;
+	std::string getObjectHandlerName(MapObjectID type) const;
 
-	std::string getJsonKey(si32 type) const;
+	std::string getJsonKey(MapObjectID type) const;
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{

+ 2 - 2
lib/mapObjects/CArmedInstance.cpp

@@ -73,7 +73,7 @@ void CArmedInstance::updateMoraleBonusFromArmy()
 	for(const auto & slot : Slots())
 	{
 		const CStackInstance * inst = slot.second;
-		const CCreature * creature  = VLC->creh->objects[inst->getCreatureID()];
+		const auto * creature  = inst->getCreatureID().toEntity(VLC);
 
 		factions.insert(creature->getFaction());
 		// Check for undead flag instead of faction (undead mummies are neutral)
@@ -92,7 +92,7 @@ void CArmedInstance::updateMoraleBonusFromArmy()
 
 		for(auto f : factions)
 		{
-			if (VLC->factions()->getByIndex(f)->getAlignment() != EAlignment::EVIL)
+			if (VLC->factions()->getById(f)->getAlignment() != EAlignment::EVIL)
 				mixableFactions++;
 		}
 		if (mixableFactions > 0)

+ 18 - 22
lib/mapObjects/CBank.cpp

@@ -46,7 +46,7 @@ void CBank::initObj(CRandomGenerator & rand)
 {
 	daycounter = 0;
 	resetDuration = 0;
-	VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand);
+	getObjectHandler()->configureObject(this, rand);
 }
 
 bool CBank::isCoastVisitable() const
@@ -79,11 +79,7 @@ std::vector<Component> CBank::getPopupComponents(PlayerColor player) const
 
 	for (auto const & guard : guardsAmounts)
 	{
-		Component comp;
-		comp.id = Component::EComponentType::CREATURE;
-		comp.subtype = guard.first.getNum();
-		comp.val = guard.second;
-
+		Component comp(ComponentType::CREATURE, guard.first, guard.second);
 		result.push_back(comp);
 	}
 	return result;
@@ -141,7 +137,7 @@ void CBank::onHeroVisit(const CGHeroInstance * h) const
 	cb->sendAndApply(&cov);
 
 	int banktext = 0;
-	switch (ID)
+	switch (ID.toEnum())
 	{
 	case Obj::DERELICT_SHIP:
 		banktext = 41;
@@ -184,7 +180,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 
 	if (bc)
 	{
-		switch (ID)
+		switch (ID.toEnum())
 		{
 		case Obj::DERELICT_SHIP:
 			textID = 43;
@@ -207,7 +203,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 	}
 	else
 	{
-		switch (ID)
+		switch (ID.toEnum())
 		{
 		case Obj::SHIPWRECK:
 		case Obj::DERELICT_SHIP:
@@ -220,7 +216,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 			gbonus.bonus.sid = BonusSourceID(ID);
 			gbonus.bonus.type = BonusType::MORALE;
 			gbonus.bonus.val = -1;
-			switch (ID)
+			switch (ID.toEnum())
 			{
 			case Obj::SHIPWRECK:
 				textID = 123;
@@ -236,7 +232,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 				break;
 			}
 			cb->giveHeroBonus(&gbonus);
-			iw.components.emplace_back(Component::EComponentType::MORALE, 0, -1, 0);
+			iw.components.emplace_back(ComponentType::MORALE, -1);
 			iw.soundID = soundBase::GRAVEYARD;
 			break;
 		}
@@ -247,7 +243,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 			gb.id = hero->id.getNum();
 			cb->giveHeroBonus(&gb);
 			textID = 107;
-			iw.components.emplace_back(Component::EComponentType::LUCK, 0, -2, 0);
+			iw.components.emplace_back(ComponentType::LUCK, -2);
 			break;
 		}
 		case Obj::CREATURE_BANK:
@@ -267,24 +263,24 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 	//grant resources
 	if (bc)
 	{
-		for (int it = 0; it < bc->resources.size(); it++)
+		for (GameResID it : GameResID::ALL_RESOURCES())
 		{
 			if (bc->resources[it] != 0)
 			{
-				iw.components.emplace_back(Component::EComponentType::RESOURCE, it, bc->resources[it], 0);
+				iw.components.emplace_back(ComponentType::RESOURCE, it, bc->resources[it]);
 				loot.appendRawString("%d %s");
-				loot.replaceNumber(iw.components.back().val);
-				loot.replaceLocalString(EMetaText::RES_NAMES, iw.components.back().subtype);
-				cb->giveResource(hero->getOwner(), static_cast<EGameResID>(it), bc->resources[it]);
+				loot.replaceNumber(bc->resources[it]);
+				loot.replaceLocalString(EMetaText::RES_NAMES, it);
+				cb->giveResource(hero->getOwner(), it, bc->resources[it]);
 			}
 		}
 		//grant artifacts
 		for (auto & elem : bc->artifacts)
 		{
-			iw.components.emplace_back(Component::EComponentType::ARTIFACT, elem, 0, 0);
+			iw.components.emplace_back(ComponentType::ARTIFACT, elem);
 			loot.appendRawString("%s");
 			loot.replaceLocalString(EMetaText::ART_NAMES, elem);
-			cb->giveHeroNewArtifact(hero, VLC->arth->objects[elem], ArtifactPosition::FIRST_AVAILABLE);
+			cb->giveHeroNewArtifact(hero, elem.toArtifact(), ArtifactPosition::FIRST_AVAILABLE);
 		}
 		//display loot
 		if (!iw.components.empty())
@@ -318,14 +314,14 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 			}
 			for(const SpellID & spellId : bc->spells)
 			{
-				const auto * spell = spellId.toSpell(VLC->spells());
+				const auto * spell = spellId.toEntity(VLC);
 				iw.text.appendLocalString(EMetaText::SPELL_NAME, spellId);
 				if(spell->getLevel() <= hero->maxSpellLevel())
 				{
 					if(hero->canLearnSpell(spell))
 					{
 						spells.insert(spellId);
-						iw.components.emplace_back(Component::EComponentType::SPELL, spellId, 0, 0);
+						iw.components.emplace_back(ComponentType::SPELL, spellId);
 					}
 				}
 				else
@@ -356,7 +352,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 
 		for(const auto & elem : ourArmy.Slots())
 		{
-			iw.components.emplace_back(*elem.second);
+			iw.components.emplace_back(ComponentType::CREATURE, elem.second->getId(), elem.second->getCount());
 			loot.appendRawString("%s");
 			loot.replaceCreatureName(*elem.second);
 		}

+ 55 - 18
lib/mapObjects/CGCreature.cpp

@@ -28,7 +28,7 @@ std::string CGCreature::getHoverText(PlayerColor player) const
 	if(stacks.empty())
 	{
 		//should not happen...
-		logGlobal->error("Invalid stack at tile %s: subID=%d; id=%d", pos.toString(), subID, id.getNum());
+		logGlobal->error("Invalid stack at tile %s: subID=%d; id=%d", pos.toString(), getCreature(), id.getNum());
 		return "INVALID_STACK";
 	}
 
@@ -41,7 +41,7 @@ std::string CGCreature::getHoverText(PlayerColor player) const
 	else
 		ms.appendLocalString(EMetaText::ARRAY_TXT, quantityTextIndex);
 	ms.appendRawString(" ");
-	ms.appendLocalString(EMetaText::CRE_PL_NAMES,subID);
+	ms.appendLocalString(EMetaText::CRE_PL_NAMES, getCreature());
 	hoverName = ms.toString();
 	return hoverName;
 }
@@ -54,7 +54,7 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const
 		MetaString ms;
 		ms.appendNumber(stacks.begin()->second->count);
 		ms.appendRawString(" ");
-		ms.appendLocalString(EMetaText::CRE_PL_NAMES,subID);
+		ms.appendLocalString(EMetaText::CRE_PL_NAMES, getCreature());
 
 		ms.appendRawString("\n\n");
 
@@ -132,7 +132,7 @@ void CGCreature::onHeroVisit( const CGHeroInstance * h ) const
 			BlockingDialog ynd(true,false);
 			ynd.player = h->tempOwner;
 			ynd.text.appendLocalString(EMetaText::ADVOB_TXT, 86);
-			ynd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, subID);
+			ynd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, getCreature());
 			cb->showBlockingDialog(&ynd);
 			break;
 		}
@@ -146,13 +146,50 @@ void CGCreature::onHeroVisit( const CGHeroInstance * h ) const
 			std::string tmp = VLC->generaltexth->advobtxt[90];
 			boost::algorithm::replace_first(tmp, "%d", std::to_string(getStackCount(SlotID(0))));
 			boost::algorithm::replace_first(tmp, "%d", std::to_string(action));
-			boost::algorithm::replace_first(tmp,"%s",VLC->creh->objects[subID]->getNamePluralTranslated());
+			boost::algorithm::replace_first(tmp,"%s",VLC->creatures()->getById(getCreature())->getNamePluralTranslated());
 			ynd.text.appendRawString(tmp);
 			cb->showBlockingDialog(&ynd);
 			break;
 		}
 	}
+}
+
+CreatureID CGCreature::getCreature() const
+{
+	return CreatureID(getObjTypeIndex().getNum());
+}
 
+void CGCreature::pickRandomObject(CRandomGenerator & rand)
+{
+	switch(ID.toEnum())
+	{
+		case MapObjectID::RANDOM_MONSTER:
+			subID = VLC->creh->pickRandomMonster(rand);
+			break;
+		case MapObjectID::RANDOM_MONSTER_L1:
+			subID = VLC->creh->pickRandomMonster(rand, 1);
+			break;
+		case MapObjectID::RANDOM_MONSTER_L2:
+			subID = VLC->creh->pickRandomMonster(rand, 2);
+			break;
+		case MapObjectID::RANDOM_MONSTER_L3:
+			subID = VLC->creh->pickRandomMonster(rand, 3);
+			break;
+		case MapObjectID::RANDOM_MONSTER_L4:
+			subID = VLC->creh->pickRandomMonster(rand, 4);
+			break;
+		case MapObjectID::RANDOM_MONSTER_L5:
+			subID = VLC->creh->pickRandomMonster(rand, 5);
+			break;
+		case MapObjectID::RANDOM_MONSTER_L6:
+			subID = VLC->creh->pickRandomMonster(rand, 6);
+			break;
+		case MapObjectID::RANDOM_MONSTER_L7:
+			subID = VLC->creh->pickRandomMonster(rand, 7);
+			break;
+	}
+	ID = MapObjectID::MONSTER;
+	setType(ID, subID);
 }
 
 void CGCreature::initObj(CRandomGenerator & rand)
@@ -177,16 +214,16 @@ void CGCreature::initObj(CRandomGenerator & rand)
 		break;
 	}
 
-	stacks[SlotID(0)]->setType(CreatureID(subID));
+	stacks[SlotID(0)]->setType(getCreature());
 	TQuantity &amount = stacks[SlotID(0)]->count;
-	CCreature &c = *VLC->creh->objects[subID];
+	const Creature * c = VLC->creatures()->getById(getCreature());
 	if(amount == 0)
 	{
-		amount = rand.nextInt(c.ammMin, c.ammMax);
+		amount = rand.nextInt(c->getAdvMapAmountMin(), c->getAdvMapAmountMax());
 
 		if(amount == 0) //armies with 0 creatures are illegal
 		{
-			logGlobal->warn("Stack %s cannot have 0 creatures. Check properties of %s", nodeName(), c.nodeName());
+			logGlobal->warn("Stack cannot have 0 creatures. Check properties of %s", c->getJsonKey());
 			amount = 1;
 		}
 	}
@@ -252,7 +289,7 @@ int CGCreature::takenAction(const CGHeroInstance *h, bool allowJoin) const
 		powerFactor = -3;
 
 	std::set<CreatureID> myKindCres; //what creatures are the same kind as we
-	const CCreature * myCreature = VLC->creh->objects[subID];
+	const CCreature * myCreature = VLC->creh->objects[getCreature().getNum()];
 	myKindCres.insert(myCreature->getId()); //we
 	myKindCres.insert(myCreature->upgrades.begin(), myCreature->upgrades.end()); //our upgrades
 
@@ -290,7 +327,7 @@ int CGCreature::takenAction(const CGHeroInstance *h, bool allowJoin) const
 			return JOIN_FOR_FREE;
 
 		else if(diplomacy * 2  +  sympathy  +  1 >= character)
-			return VLC->creatures()->getByIndex(subID)->getRecruitCost(EGameResID::GOLD) * getStackCount(SlotID(0)); //join for gold
+			return VLC->creatures()->getById(getCreature())->getRecruitCost(EGameResID::GOLD) * getStackCount(SlotID(0)); //join for gold
 	}
 
 	//we are still here - creatures have not joined hero, flee or fight
@@ -390,7 +427,7 @@ void CGCreature::fight( const CGHeroInstance *h ) const
 			if(!upgrades.empty())
 			{
 				auto it = RandomGeneratorUtil::nextItem(upgrades, CRandomGenerator::getDefault());
-				cb->changeStackType(StackLocation(this, slotID), VLC->creh->objects[*it]);
+				cb->changeStackType(StackLocation(this, slotID), it->toCreature());
 			}
 		}
 	}
@@ -404,7 +441,7 @@ void CGCreature::flee( const CGHeroInstance * h ) const
 	BlockingDialog ynd(true,false);
 	ynd.player = h->tempOwner;
 	ynd.text.appendLocalString(EMetaText::ADVOB_TXT,91);
-	ynd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, subID);
+	ynd.text.replaceLocalString(EMetaText::CRE_PL_NAMES, getCreature());
 	cb->showBlockingDialog(&ynd);
 }
 
@@ -526,17 +563,17 @@ void CGCreature::giveReward(const CGHeroInstance * h) const
 	if(!resources.empty())
 	{
 		cb->giveResources(h->tempOwner, resources);
-		for(int i = 0; i < resources.size(); i++)
+		for(auto const & res : GameResID::ALL_RESOURCES())
 		{
-			if(resources[i] > 0)
-				iw.components.emplace_back(Component::EComponentType::RESOURCE, i, resources[i], 0);
+			if(resources[res] > 0)
+				iw.components.emplace_back(ComponentType::RESOURCE, res, resources[res]);
 		}
 	}
 
 	if(gainedArtifact != ArtifactID::NONE)
 	{
-		cb->giveHeroNewArtifact(h, VLC->arth->objects[gainedArtifact], ArtifactPosition::FIRST_AVAILABLE);
-		iw.components.emplace_back(Component::EComponentType::ARTIFACT, gainedArtifact, 0, 0);
+		cb->giveHeroNewArtifact(h, gainedArtifact.toArtifact(), ArtifactPosition::FIRST_AVAILABLE);
+		iw.components.emplace_back(ComponentType::ARTIFACT, gainedArtifact);
 	}
 
 	if(!iw.components.empty())

+ 2 - 0
lib/mapObjects/CGCreature.h

@@ -41,9 +41,11 @@ public:
 	std::string getHoverText(PlayerColor player) const override;
 	std::string getHoverText(const CGHeroInstance * hero) const override;
 	void initObj(CRandomGenerator & rand) override;
+	void pickRandomObject(CRandomGenerator & rand) override;
 	void newTurn(CRandomGenerator & rand) const override;
 	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
 	void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
+	CreatureID getCreature() const;
 
 	//stack formation depends on position,
 	bool containsUpgradedStack() const;

+ 127 - 65
lib/mapObjects/CGDwelling.cpp

@@ -11,8 +11,10 @@
 #include "StdInc.h"
 #include "CGDwelling.h"
 #include "../serializer/JsonSerializeFormat.h"
+#include "../mapping/CMap.h"
 #include "../mapObjectConstructors/AObjectTypeHandler.h"
 #include "../mapObjectConstructors/CObjectClassesHandler.h"
+#include "../mapObjectConstructors/DwellingInstanceConstructor.h"
 #include "../mapObjects/CGHeroInstance.h"
 #include "../networkPacks/StackLocation.h"
 #include "../networkPacks/PacksForClient.h"
@@ -26,76 +28,154 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-CSpecObjInfo::CSpecObjInfo():
-	owner(nullptr)
-{
-
-}
-
-void CCreGenAsCastleInfo::serializeJson(JsonSerializeFormat & handler)
+void CGDwellingRandomizationInfo::serializeJson(JsonSerializeFormat & handler)
 {
 	handler.serializeString("sameAsTown", instanceId);
+	handler.serializeIdArray("allowedFactions", allowedFactions);
+	handler.serializeInt("minLevel", minLevel, static_cast<uint8_t>(1));
+	handler.serializeInt("maxLevel", maxLevel, static_cast<uint8_t>(7));
 
 	if(!handler.saving)
 	{
-		asCastle = !instanceId.empty();
-		allowedFactions.clear();
+		//todo: safely allow any level > 7
+		vstd::abetween<uint8_t>(minLevel, 1, 7);
+		vstd::abetween<uint8_t>(maxLevel, minLevel, 7);
 	}
+}
 
-	if(!asCastle)
-	{
-		std::vector<bool> standard;
-		standard.resize(VLC->townh->size(), true);
+CGDwelling::CGDwelling() = default;
+CGDwelling::~CGDwelling() = default;
+
+FactionID CGDwelling::randomizeFaction(CRandomGenerator & rand)
+{
+	if (ID == Obj::RANDOM_DWELLING_FACTION)
+		return FactionID(subID);
 
-		JsonSerializeFormat::LIC allowedLIC(standard, &FactionID::decode, &FactionID::encode);
-		allowedLIC.any = allowedFactions;
+	assert(ID == Obj::RANDOM_DWELLING || ID == Obj::RANDOM_DWELLING_LVL);
+	assert(randomizationInfo.has_value());
+	if (!randomizationInfo)
+		return FactionID::CASTLE;
 
-		handler.serializeLIC("allowedFactions", allowedLIC);
+	CGTownInstance * linkedTown = nullptr;
+
+	if (!randomizationInfo->instanceId.empty())
+	{
+		auto iter = cb->gameState()->map->instanceNames.find(randomizationInfo->instanceId);
+
+		if(iter == cb->gameState()->map->instanceNames.end())
+			logGlobal->error("Map object not found: %s", randomizationInfo->instanceId);
+		linkedTown = dynamic_cast<CGTownInstance *>(iter->second.get());
+	}
 
-		if(!handler.saving)
+	if (randomizationInfo->identifier != 0)
+	{
+		for(auto & elem : cb->gameState()->map->objects)
 		{
-			allowedFactions = allowedLIC.any;
+			auto town = dynamic_cast<CGTownInstance*>(elem.get());
+			if(town && town->identifier == randomizationInfo->identifier)
+			{
+				linkedTown = town;
+				break;
+			}
 		}
 	}
-}
 
-void CCreGenLeveledInfo::serializeJson(JsonSerializeFormat & handler)
-{
-	handler.serializeInt("minLevel", minLevel, static_cast<uint8_t>(1));
-	handler.serializeInt("maxLevel", maxLevel, static_cast<uint8_t>(7));
-
-	if(!handler.saving)
+	if (linkedTown)
 	{
-		//todo: safely allow any level > 7
-		vstd::abetween<uint8_t>(minLevel, 1, 7);
-		vstd::abetween<uint8_t>(maxLevel, minLevel, 7);
+		if(linkedTown->ID==Obj::RANDOM_TOWN)
+			linkedTown->pickRandomObject(rand); //we have to randomize the castle first
+
+		assert(linkedTown->ID == Obj::TOWN);
+		if(linkedTown->ID==Obj::TOWN)
+			return linkedTown->getFaction();
 	}
-}
 
-void CCreGenLeveledCastleInfo::serializeJson(JsonSerializeFormat & handler)
-{
-	CCreGenAsCastleInfo::serializeJson(handler);
-	CCreGenLeveledInfo::serializeJson(handler);
+	if(!randomizationInfo->allowedFactions.empty())
+		return *RandomGeneratorUtil::nextItem(randomizationInfo->allowedFactions, rand);
+
+
+	std::vector<FactionID> potentialPicks;
+
+	for (FactionID faction(0); faction < FactionID(VLC->townh->size()); ++faction)
+		if (VLC->factions()->getById(faction)->hasTown())
+			potentialPicks.push_back(faction);
+
+	assert(!potentialPicks.empty());
+	return *RandomGeneratorUtil::nextItem(potentialPicks, rand);
 }
 
-CGDwelling::CGDwelling()
-	: info(nullptr)
+int CGDwelling::randomizeLevel(CRandomGenerator & rand)
 {
+	if (ID == Obj::RANDOM_DWELLING_LVL)
+		return subID.getNum();
+
+	assert(ID == Obj::RANDOM_DWELLING || ID == Obj::RANDOM_DWELLING_FACTION);
+	assert(randomizationInfo.has_value());
+
+	if (!randomizationInfo)
+		return rand.nextInt(1, 7) - 1;
+
+	if(randomizationInfo->minLevel == randomizationInfo->maxLevel)
+		return randomizationInfo->minLevel - 1;
+
+	return rand.nextInt(randomizationInfo->minLevel, randomizationInfo->maxLevel) - 1;
 }
 
-CGDwelling::~CGDwelling()
+void CGDwelling::pickRandomObject(CRandomGenerator & rand)
 {
-	vstd::clear_pointer(info);
+	if (ID == Obj::RANDOM_DWELLING || ID == Obj::RANDOM_DWELLING_LVL || ID == Obj::RANDOM_DWELLING_FACTION)
+	{
+		FactionID faction = randomizeFaction(rand);
+		int level = randomizeLevel(rand);
+		assert(faction != FactionID::NONE && faction != FactionID::NEUTRAL);
+		assert(level >= 0 && level <= 6);
+		randomizationInfo.reset();
+
+		CreatureID cid = (*VLC->townh)[faction]->town->creatures[level][0];
+
+		//NOTE: this will pick last dwelling with this creature (Mantis #900)
+		//check for block map equality is better but more complex solution
+		auto testID = [&](const Obj & primaryID) -> MapObjectSubID
+		{
+			auto dwellingIDs = VLC->objtypeh->knownSubObjects(primaryID);
+			for (si32 entry : dwellingIDs)
+			{
+				const auto * handler = dynamic_cast<const DwellingInstanceConstructor *>(VLC->objtypeh->getHandlerFor(primaryID, entry).get());
+
+				if (handler->producesCreature(cid.toCreature()))
+					return MapObjectSubID(entry);
+			}
+			return MapObjectSubID();
+		};
+
+		ID = Obj::CREATURE_GENERATOR1;
+		subID = testID(Obj::CREATURE_GENERATOR1);
+
+		if (subID == MapObjectSubID())
+		{
+			ID = Obj::CREATURE_GENERATOR4;
+			subID = testID(Obj::CREATURE_GENERATOR4);
+		}
+
+		if (subID == MapObjectSubID())
+		{
+			logGlobal->error("Error: failed to find dwelling for %s of level %d", (*VLC->townh)[faction]->getNameTranslated(), int(level));
+			ID = Obj::CREATURE_GENERATOR4;
+			subID = *RandomGeneratorUtil::nextItem(VLC->objtypeh->knownSubObjects(Obj::CREATURE_GENERATOR1), rand);
+		}
+
+		setType(ID, subID);
+	}
 }
 
 void CGDwelling::initObj(CRandomGenerator & rand)
 {
-	switch(ID)
+	switch(ID.toEnum())
 	{
 	case Obj::CREATURE_GENERATOR1:
 	case Obj::CREATURE_GENERATOR4:
 		{
-			VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand);
+			getObjectHandler()->configureObject(this, rand);
 
 			if (getOwner() != PlayerColor::NEUTRAL)
 				cb->gameState()->players[getOwner()].dwellings.emplace_back(this);
@@ -121,23 +201,6 @@ void CGDwelling::initObj(CRandomGenerator & rand)
 	}
 }
 
-void CGDwelling::initRandomObjectInfo()
-{
-	vstd::clear_pointer(info);
-	switch(ID)
-	{
-		case Obj::RANDOM_DWELLING: info = new CCreGenLeveledCastleInfo();
-			break;
-		case Obj::RANDOM_DWELLING_LVL: info = new CCreGenAsCastleInfo();
-			break;
-		case Obj::RANDOM_DWELLING_FACTION: info = new CCreGenLeveledInfo();
-			break;
-	}
-
-	if(info)
-		info->owner = this;
-}
-
 void CGDwelling::setPropertyDer(ui8 what, ui32 val)
 {
 	switch (what)
@@ -256,7 +319,7 @@ void CGDwelling::newTurn(CRandomGenerator & rand) const
 			else
 				creaturesAccumulate = VLC->settings()->getBoolean(EGameSettings::DWELLINGS_ACCUMULATE_WHEN_NEUTRAL);
 
-			CCreature *cre = VLC->creh->objects[creatures[i].second[0]];
+			const CCreature * cre =creatures[i].second[0].toCreature();
 			TQuantity amount = cre->getGrowth() * (1 + cre->valOfBonuses(BonusType::CREATURE_GROWTH_PERCENT)/100) + cre->valOfBonuses(BonusType::CREATURE_GROWTH);
 			if (creaturesAccumulate && ID != Obj::REFUGEE_CAMP) //camp should not try to accumulate different kinds of creatures
 				sac.creatures[i].first += amount;
@@ -281,7 +344,7 @@ void CGDwelling::updateGuards() const
 	//default condition - creatures are of level 5 or higher
 	for (auto creatureEntry : creatures)
 	{
-		if (VLC->creatures()->getByIndex(creatureEntry.second.at(0))->getLevel() >= 5 && ID != Obj::REFUGEE_CAMP)
+		if (VLC->creatures()->getById(creatureEntry.second.at(0))->getLevel() >= 5 && ID != Obj::REFUGEE_CAMP)
 		{
 			guarded = true;
 			break;
@@ -292,7 +355,7 @@ void CGDwelling::updateGuards() const
 	{
 		for (auto creatureEntry : creatures)
 		{
-			const CCreature * crea = VLC->creh->objects[creatureEntry.second.at(0)];
+			const CCreature * crea = creatureEntry.second.at(0).toCreature();
 			SlotID slot = getSlotFor(crea->getId());
 
 			if (hasStackAtSlot(slot)) //stack already exists, overwrite it
@@ -425,10 +488,7 @@ void CGDwelling::blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer)
 
 void CGDwelling::serializeJsonOptions(JsonSerializeFormat & handler)
 {
-	if(!handler.saving)
-		initRandomObjectInfo();
-
-	switch (ID)
+	switch (ID.toEnum())
 	{
 	case Obj::WAR_MACHINE_FACTORY:
 	case Obj::REFUGEE_CAMP:
@@ -437,8 +497,10 @@ void CGDwelling::serializeJsonOptions(JsonSerializeFormat & handler)
 	case Obj::RANDOM_DWELLING:
 	case Obj::RANDOM_DWELLING_LVL:
 	case Obj::RANDOM_DWELLING_FACTION:
-		info->serializeJson(handler);
-		//fall through
+		if (!handler.saving)
+			randomizationInfo = CGDwellingRandomizationInfo();
+		randomizationInfo->serializeJson(handler);
+		[[fallthrough]];
 	default:
 		serializeJsonOwner(handler);
 		break;

+ 11 - 34
lib/mapObjects/CGDwelling.h

@@ -16,62 +16,39 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 class CGDwelling;
 
-class DLL_LINKAGE CSpecObjInfo
+class DLL_LINKAGE CGDwellingRandomizationInfo
 {
 public:
-	CSpecObjInfo();
-	virtual ~CSpecObjInfo() = default;
-
-	virtual void serializeJson(JsonSerializeFormat & handler) = 0;
-
-	const CGDwelling * owner;
-};
-
-class DLL_LINKAGE CCreGenAsCastleInfo : public virtual CSpecObjInfo
-{
-public:
-	bool asCastle = false;
-	ui32 identifier = 0;//h3m internal identifier
-
-	std::vector<bool> allowedFactions;
+	std::set<FactionID> allowedFactions;
 
 	std::string instanceId;//vcmi map instance identifier
-	void serializeJson(JsonSerializeFormat & handler) override;
-};
+	int32_t identifier = 0;//h3m internal identifier
 
-class DLL_LINKAGE CCreGenLeveledInfo : public virtual CSpecObjInfo
-{
-public:
-	ui8 minLevel = 1;
-	ui8 maxLevel = 7; //minimal and maximal level of creature in dwelling: <1, 7>
-
-	void serializeJson(JsonSerializeFormat & handler) override;
-};
+	uint8_t minLevel = 1;
+	uint8_t maxLevel = 7; //minimal and maximal level of creature in dwelling: <1, 7>
 
-class DLL_LINKAGE CCreGenLeveledCastleInfo : public CCreGenAsCastleInfo, public CCreGenLeveledInfo
-{
-public:
-	CCreGenLeveledCastleInfo() = default;
-	void serializeJson(JsonSerializeFormat & handler) override;
+	void serializeJson(JsonSerializeFormat & handler);
 };
 
-
 class DLL_LINKAGE CGDwelling : public CArmedInstance
 {
 public:
 	typedef std::vector<std::pair<ui32, std::vector<CreatureID> > > TCreaturesSet;
 
-	CSpecObjInfo * info; //random dwelling options; not serialized
+	std::optional<CGDwellingRandomizationInfo> randomizationInfo; //random dwelling options; not serialized
 	TCreaturesSet creatures; //creatures[level] -> <vector of alternative ids (base creature and upgrades, creatures amount>
 
 	CGDwelling();
 	~CGDwelling() override;
 
-	void initRandomObjectInfo();
 protected:
 	void serializeJsonOptions(JsonSerializeFormat & handler) override;
 
 private:
+	FactionID randomizeFaction(CRandomGenerator & rand);
+	int randomizeLevel(CRandomGenerator & rand);
+
+	void pickRandomObject(CRandomGenerator & rand) override;
 	void initObj(CRandomGenerator & rand) override;
 	void onHeroVisit(const CGHeroInstance * h) const override;
 	void newTurn(CRandomGenerator & rand) const override;

+ 50 - 29
lib/mapObjects/CGHeroInstance.cpp

@@ -218,7 +218,10 @@ bool CGHeroInstance::canLearnSkill(const SecondarySkill & which) const
 	if (getSecSkillLevel(which) > 0)
 		return false;
 
-	if (type->heroClass->secSkillProbability[which] == 0)
+	if (type->heroClass->secSkillProbability.count(which) == 0)
+		return false;
+
+	if (type->heroClass->secSkillProbability.at(which) == 0)
 		return false;
 
 	return true;
@@ -285,27 +288,28 @@ PlayerColor CGHeroInstance::getOwner() const
 	return tempOwner;
 }
 
-void CGHeroInstance::initHero(CRandomGenerator & rand, const HeroTypeID & SUBID)
+HeroTypeID CGHeroInstance::getHeroType() const
 {
-	subID = SUBID.getNum();
-	initHero(rand);
+	return HeroTypeID(getObjTypeIndex().getNum());
 }
 
-void CGHeroInstance::setType(si32 ID, si32 subID)
+void CGHeroInstance::setHeroType(HeroTypeID heroType)
 {
-	assert(ID == Obj::HERO); // just in case
-	type = VLC->heroh->objects[subID];
+	assert(type == nullptr);
+	subID = heroType;
+}
 
-	CGObjectInstance::setType(ID, type->heroClass->getIndex()); // to find object handler we must use heroClass->id
-	this->subID = subID; // after setType subID used to store unique hero identify id. Check issue 2277 for details
-	randomizeArmy(type->heroClass->faction);
+void CGHeroInstance::initHero(CRandomGenerator & rand, const HeroTypeID & SUBID)
+{
+	subID = SUBID.getNum();
+	initHero(rand);
 }
 
 void CGHeroInstance::initHero(CRandomGenerator & rand)
 {
 	assert(validTypes(true));
 	if(!type)
-		type = VLC->heroh->objects[subID];
+		type = VLC->heroh->objects[getHeroType().getNum()];
 
 	if (ID == Obj::HERO)
 		appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front();
@@ -566,18 +570,35 @@ void CGHeroInstance::SecondarySkillsInfo::resetWisdomCounter()
 	wisdomCounter = 1;
 }
 
-void CGHeroInstance::initObj(CRandomGenerator & rand)
+void CGHeroInstance::pickRandomObject(CRandomGenerator & rand)
 {
-	if(!type)
-		initHero(rand); //TODO: set up everything for prison before specialties are configured
+	assert(ID == Obj::HERO || ID == Obj::PRISON || ID == Obj::RANDOM_HERO);
 
-	if (ID != Obj::PRISON)
+	if (ID == Obj::RANDOM_HERO)
 	{
-		auto terrain = cb->gameState()->getTile(visitablePos())->terType->getId();
-		auto customApp = VLC->objtypeh->getHandlerFor(ID, type->heroClass->getIndex())->getOverride(terrain, this);
-		if (customApp)
-			appearance = customApp;
+		ID = Obj::HERO;
+		subID = cb->gameState()->pickNextHeroType(getOwner());
+		type = VLC->heroh->objects[subID];
+		randomizeArmy(type->heroClass->faction);
 	}
+	else
+		type = VLC->heroh->objects[subID];
+
+	auto oldSubID = subID;
+
+	// to find object handler we must use heroClass->id
+	// after setType subID used to store unique hero identify id. Check issue 2277 for details
+	if (ID != Obj::PRISON)
+		setType(ID, type->heroClass->getIndex());
+	else
+		setType(ID, 0);
+
+	this->subID = oldSubID;
+}
+
+void CGHeroInstance::initObj(CRandomGenerator & rand)
+{
+
 }
 
 void CGHeroInstance::recreateSecondarySkillsBonuses()
@@ -926,7 +947,7 @@ void CGHeroInstance::showNecromancyDialog(const CStackBasicDescriptor &raisedSta
 	iw.type = EInfoWindowMode::AUTO;
 	iw.soundID = soundBase::pickup01 + rand.nextInt(6);
 	iw.player = tempOwner;
-	iw.components.emplace_back(raisedStack);
+	iw.components.emplace_back(ComponentType::CREATURE, raisedStack.getId(), raisedStack.count);
 
 	if (raisedStack.count > 1) // Practicing the dark arts of necromancy, ... (plural)
 	{
@@ -1050,7 +1071,7 @@ HeroTypeID CGHeroInstance::getPortraitSource() const
 	if (customPortraitSource.isValid())
 		return customPortraitSource;
 	else
-		return HeroTypeID(subID);
+		return getHeroType();
 }
 
 int32_t CGHeroInstance::getIconIndex() const
@@ -1092,7 +1113,7 @@ std::string CGHeroInstance::getBiographyTextID() const
 
 CGHeroInstance::ArtPlacementMap CGHeroInstance::putArtifact(ArtifactPosition pos, CArtifactInstance * art)
 {
-	assert(art->artType->canBePutAt(this, pos));
+	assert(art->canBePutAt(this, pos));
 
 	if(ArtifactUtils::isSlotEquipment(pos))
 		attachTo(*art);
@@ -1135,7 +1156,7 @@ void CGHeroInstance::removeSpellbook()
 
 	if(hasSpellbook())
 	{
-		ArtifactLocation(this, ArtifactPosition(ArtifactPosition::SPELLBOOK)).removeArtifact();
+		getArt(ArtifactPosition::SPELLBOOK)->removeFrom(*this, ArtifactPosition::SPELLBOOK);
 	}
 }
 
@@ -1502,7 +1523,7 @@ std::string CGHeroInstance::getHeroTypeName() const
 		}
 		else
 		{
-			return VLC->heroh->objects[subID]->getJsonKey();
+			return VLC->heroh->objects[getHeroType()]->getJsonKey();
 		}
 	}
 	return "";
@@ -1512,7 +1533,7 @@ void CGHeroInstance::afterAddToMap(CMap * map)
 {
 	auto existingHero = std::find_if(map->objects.begin(), map->objects.end(), [&](const CGObjectInstance * o) ->bool
 		{
-			return (o->ID == Obj::HERO || o->ID == Obj::PRISON) && o->subID == subID && o != this;
+			return o && (o->ID == Obj::HERO || o->ID == Obj::PRISON) && o->subID == subID && o != this;
 		});
 
 	if(existingHero != map->objects.end())
@@ -1529,14 +1550,14 @@ void CGHeroInstance::afterAddToMap(CMap * map)
 		}
 	}
 
-	if(ID == Obj::HERO)
+	if(ID != Obj::PRISON)
 	{		
 		map->heroesOnMap.emplace_back(this);
 	}
 }
 void CGHeroInstance::afterRemoveFromMap(CMap* map)
 {
-	if (ID == Obj::HERO)
+	if (ID == Obj::PRISON)
 		vstd::erase_if_present(map->heroesOnMap, this);
 }
 
@@ -1661,7 +1682,7 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler)
 		{
 			auto addSkill = [this](const std::string & skillId, const std::string & levelId)
 			{
-				const int rawId = CSkillHandler::decodeSkill(skillId);
+				const int rawId = SecondarySkill::decode(skillId);
 				if(rawId < 0)
 				{
 					logGlobal->error("Invalid secondary skill %s", skillId);
@@ -1736,7 +1757,7 @@ void CGHeroInstance::serializeJsonOptions(JsonSerializeFormat & handler)
 			if(!appearance)
 			{
 				// crossoverDeserialize
-				type = VLC->heroh->objects[subID];
+				type = VLC->heroh->objects[getHeroType()];
 				appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, type->heroClass->getIndex())->getTemplates().front();
 			}
 

+ 3 - 1
lib/mapObjects/CGHeroInstance.h

@@ -231,7 +231,8 @@ public:
 
 	//////////////////////////////////////////////////////////////////////////
 
-	void setType(si32 ID, si32 subID) override;
+	HeroTypeID getHeroType() const;
+	void setHeroType(HeroTypeID type);
 
 	void initHero(CRandomGenerator & rand);
 	void initHero(CRandomGenerator & rand, const HeroTypeID & SUBID);
@@ -294,6 +295,7 @@ public:
 	void deserializationFix();
 
 	void initObj(CRandomGenerator & rand) override;
+	void pickRandomObject(CRandomGenerator & rand) override;
 	void onHeroVisit(const CGHeroInstance * h) const override;
 	std::string getObjectName() const override;
 

+ 1 - 1
lib/mapObjects/CGMarket.cpp

@@ -25,7 +25,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 void CGMarket::initObj(CRandomGenerator & rand)
 {
-	VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand);
+	getObjectHandler()->configureObject(this, rand);
 }
 
 void CGMarket::onHeroVisit(const CGHeroInstance * h) const

Some files were not shown because too many files changed in this diff