Browse Source

Replaced hero crossover logic with one that actually matches H3

Ivan Savenko 2 years ago
parent
commit
48ac84110b

+ 2 - 5
lib/NetPacksLib.cpp

@@ -1066,17 +1066,14 @@ void PlayerEndsGame::applyGs(CGameState * gs) const
 		p->status = EPlayerStatus::WINNER;
 
 		// TODO: Campaign-specific code might as well go somewhere else
+		// keep all heroes from the winning player
 		if(p->human && gs->scenarioOps->campState)
 		{
 			std::vector<CGHeroInstance *> crossoverHeroes;
 			for (CGHeroInstance * hero : gs->map->heroesOnMap)
-			{
 				if (hero->tempOwner == player)
-				{
-					// keep all heroes from the winning player
 					crossoverHeroes.push_back(hero);
-				}
-			}
+
 			gs->scenarioOps->campState->setCurrentMapAsConquered(crossoverHeroes);
 		}
 	}

+ 1 - 2
lib/campaign/CampaignHandler.cpp

@@ -65,7 +65,6 @@ std::unique_ptr<Campaign> CampaignHandler::getHeader( const std::string & name)
 	auto ret = std::make_unique<Campaign>();
 	auto fileStream = CResourceHandler::get(modName)->load(resourceID);
 	std::vector<ui8> cmpgn = getFile(std::move(fileStream), true)[0];
-	JsonNode jsonCampaign((const char*)cmpgn.data(), cmpgn.size());
 
 	readCampaign(ret.get(), cmpgn, resourceID.getName(), modName, encoding);
 
@@ -436,7 +435,7 @@ CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader
 }
 
 template<typename Identifier>
-static void readContainer(std::set<Identifier> container, CBinaryReader & reader, int sizeBytes)
+static void readContainer(std::set<Identifier> & container, CBinaryReader & reader, int sizeBytes)
 {
 	for(int iId = 0, byte = 0; iId < sizeBytes * 8; ++iId)
 	{

+ 69 - 37
lib/campaign/CampaignState.cpp

@@ -185,70 +185,100 @@ bool CampaignScenario::isNotVoid() const
 	return !mapName.empty();
 }
 
+std::set<HeroTypeID> CampaignState::getReservedHeroes() const
+{
+	std::set<HeroTypeID> result;
+
+	for (auto const & scenarioID : allScenarios())
+	{
+		if (isConquered(scenarioID))
+			continue;
+
+		auto header = getMapHeader(scenarioID);
+
+		result.insert(header->reservedCampaignHeroes.begin(), header->reservedCampaignHeroes.end());
+	}
+
+	return result;
+}
+
 const CGHeroInstance * CampaignState::strongestHero(CampaignScenarioID scenarioId, const PlayerColor & owner) const
 {
 	std::function<bool(const JsonNode & node)> isOwned = [owner](const JsonNode & node)
 	{
-		auto * h = CampaignState::crossoverDeserialize(node);
+		auto * h = CampaignState::crossoverDeserialize(node, nullptr);
 		bool result = h->tempOwner == owner;
 		vstd::clear_pointer(h);
 		return result;
 	};
-	auto ownedHeroes = crossover.placedHeroes.at(scenarioId) | boost::adaptors::filtered(isOwned);
+	auto ownedHeroes = scenarioHeroPool.at(scenarioId) | boost::adaptors::filtered(isOwned);
 
-	auto i = vstd::maxElementByFun(ownedHeroes, [](const JsonNode & node)
-	{
-		auto * h = CampaignState::crossoverDeserialize(node);
-		double result = h->getHeroStrength();
-		vstd::clear_pointer(h);
-		return result;
-	});
-	return i == ownedHeroes.end() ? nullptr : CampaignState::crossoverDeserialize(*i);
+	if (ownedHeroes.empty())
+		return nullptr;
+
+	return CampaignState::crossoverDeserialize(ownedHeroes.front(), nullptr);
 }
 
-std::vector<CGHeroInstance *> CampaignState::getLostCrossoverHeroes(CampaignScenarioID scenarioId) const
+/// Returns heroes that can be instantiated as hero placeholders by power
+const std::vector<JsonNode> & CampaignState::getHeroesByPower(CampaignScenarioID scenarioId) const
 {
-	std::vector<CGHeroInstance *> lostCrossoverHeroes;
+	static const std::vector<JsonNode> emptyVector;
 
-	for(auto node2 :  crossover.placedHeroes.at(scenarioId))
-	{
-		auto * hero = CampaignState::crossoverDeserialize(node2);
-		auto it = range::find_if(crossover.crossoverHeroes.at(scenarioId), [hero](JsonNode node)
-		{
-				  auto * h = CampaignState::crossoverDeserialize(node);
-				  bool result = hero->subID == h->subID;
-				  vstd::clear_pointer(h);
-				  return result;
-	});
-		if(it == crossover.crossoverHeroes.at(scenarioId).end())
-		{
-			lostCrossoverHeroes.push_back(hero);
-		}
-	}
+	if (scenarioHeroPool.count(scenarioId))
+		return scenarioHeroPool.at(scenarioId);
 
-	return lostCrossoverHeroes;
+	return emptyVector;
 }
 
-std::vector<JsonNode> CampaignState::getCrossoverHeroes(CampaignScenarioID scenarioId) const
+/// Returns hero for instantiation as placeholder by type
+/// May return empty JsonNode if such hero was not found
+const JsonNode & CampaignState::getHeroByType(HeroTypeID heroID) const
 {
-	return crossover.crossoverHeroes.at(scenarioId);
+	static const JsonNode emptyNode;
+
+	if (!getReservedHeroes().count(heroID))
+		return emptyNode;
+
+	if (!globalHeroPool.count(heroID))
+		return emptyNode;
+
+	return globalHeroPool.at(heroID);
 }
 
-void CampaignState::setCurrentMapAsConquered(const std::vector<CGHeroInstance *> & heroes)
+void CampaignState::setCurrentMapAsConquered(std::vector<CGHeroInstance *> heroes)
 {
-	crossover.crossoverHeroes[*currentMap].clear();
-	for(CGHeroInstance * hero : heroes)
+	range::sort(heroes, [](const CGHeroInstance * a, const CGHeroInstance * b)
 	{
-		crossover.crossoverHeroes[*currentMap].push_back(crossoverSerialize(hero));
-	}
+		return a->getHeroStrength() > b->getHeroStrength();
+	});
+
+	logGlobal->info("Scenario %d of campaign %s (%s) has been completed", static_cast<int>(*currentMap), getFilename(), getName());
 
 	mapsConquered.push_back(*currentMap);
+	auto reservedHeroes = getReservedHeroes();
+
+	for (auto * hero : heroes)
+	{
+		HeroTypeID heroType(hero->subID);
+		JsonNode node = CampaignState::crossoverSerialize(hero);
+
+		if (reservedHeroes.count(heroType))
+		{
+			logGlobal->info("Hero crossover: %d (%s) exported to global pool", hero->subID, hero->getNameTranslated());
+			globalHeroPool[heroType] = node;
+		}
+		else
+		{
+			logGlobal->info("Hero crossover: %d (%s) exported to scenario pool", hero->subID, hero->getNameTranslated());
+			scenarioHeroPool[*currentMap].push_back(node);
+		}
+	}
 }
 
 std::optional<CampaignBonus> CampaignState::getBonus(CampaignScenarioID which) const
 {
 	auto bonuses = scenario(which).travelOptions.bonusesToChoose;
-	assert(chosenCampaignBonuses.count(*currentMap) || bonuses.size() == 0);
+	assert(chosenCampaignBonuses.count(*currentMap) || bonuses.empty());
 
 	if(bonuses.empty())
 		return std::optional<CampaignBonus>();
@@ -316,12 +346,14 @@ JsonNode CampaignState::crossoverSerialize(CGHeroInstance * hero)
 	return node;
 }
 
-CGHeroInstance * CampaignState::crossoverDeserialize(const JsonNode & node)
+CGHeroInstance * CampaignState::crossoverDeserialize(const JsonNode & node, CMap * map)
 {
 	JsonDeserializer handler(nullptr, const_cast<JsonNode&>(node));
 	auto * hero = new CGHeroInstance();
 	hero->ID = Obj::HERO;
 	hero->serializeJsonOptions(handler);
+	if (map)
+		hero->serializeJsonArtifacts(handler, "artifacts", map);
 	return hero;
 }
 

+ 23 - 24
lib/campaign/CampaignState.h

@@ -199,21 +199,6 @@ struct DLL_LINKAGE CampaignScenario
 	}
 };
 
-struct DLL_LINKAGE CampaignHeroes
-{
-	using ScenarioHeroesList = std::vector<JsonNode>;
-	using CampaignHeroesList = std::map<CampaignScenarioID, ScenarioHeroesList>;
-
-	CampaignHeroesList crossoverHeroes; // contains all heroes with the same state when the campaign scenario was finished
-	CampaignHeroesList placedHeroes; // contains all placed crossover heroes defined by hero placeholders when the scenario was started
-
-	template <typename Handler> void serialize(Handler &h, const int formatVersion)
-	{
-		h & crossoverHeroes;
-		h & placedHeroes;
-	}
-};
-
 /// Class that represents loaded campaign information
 class DLL_LINKAGE Campaign : public CampaignHeader
 {
@@ -238,6 +223,9 @@ public:
 class DLL_LINKAGE CampaignState : public Campaign
 {
 	friend class CampaignHandler;
+	using ScenarioPoolType = std::vector<JsonNode>;
+	using CampaignPoolType = std::map<CampaignScenarioID, ScenarioPoolType>;
+	using GlobalPoolType = std::map<HeroTypeID, JsonNode>;
 
 	/// List of all maps completed by player, in order of their completion
 	std::vector<CampaignScenarioID> mapsConquered;
@@ -246,9 +234,15 @@ class DLL_LINKAGE CampaignState : public Campaign
 	std::map<CampaignScenarioID, ui8> chosenCampaignBonuses;
 	std::optional<CampaignScenarioID> currentMap;
 
-	CampaignHeroes crossover;
+	/// Heroes from specific scenario, ordered by descending strength
+	CampaignPoolType scenarioHeroPool;
+
+	/// Pool of heroes currently reserved for usage in campaign
+	GlobalPoolType globalHeroPool;
 
 public:
+	CampaignState() = default;
+
 	/// Returns last completed scenario, if any
 	std::optional<CampaignScenarioID> lastScenario() const;
 
@@ -276,24 +270,29 @@ public:
 
 	void setCurrentMap(CampaignScenarioID which);
 	void setCurrentMapBonus(ui8 which);
-	void setCurrentMapAsConquered(const std::vector<CGHeroInstance*> & heroes);
+	void setCurrentMapAsConquered(std::vector<CGHeroInstance*> heroes);
 
+	/// Returns list of heroes that must be reserved for campaign and can only be used for hero placeholders
+	std::set<HeroTypeID> getReservedHeroes() const;
+
+	/// Returns strongest hero from specified scenario, or null if none found
 	const CGHeroInstance * strongestHero(CampaignScenarioID scenarioId, const PlayerColor & owner) const;
 
-	/// returns a list of crossover heroes which started the scenario, but didn't complete it
-	std::vector<CGHeroInstance *> getLostCrossoverHeroes(CampaignScenarioID scenarioId) const;
+	/// Returns heroes that can be instantiated as hero placeholders by power
+	const std::vector<JsonNode> & getHeroesByPower(CampaignScenarioID scenarioId) const;
 
-	std::vector<JsonNode> getCrossoverHeroes(CampaignScenarioID scenarioId) const;
+	/// Returns hero for instantiation as placeholder by type
+	/// May return empty JsonNode if such hero was not found
+	const JsonNode & getHeroByType(HeroTypeID heroID) const;
 
 	static JsonNode crossoverSerialize(CGHeroInstance * hero);
-	static CGHeroInstance * crossoverDeserialize(const JsonNode & node);
-
-	CampaignState() = default;
+	static CGHeroInstance * crossoverDeserialize(const JsonNode & node, CMap * map);
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & static_cast<Campaign&>(*this);
-		h & crossover;
+		h & scenarioHeroPool;
+		h & globalHeroPool;
 		h & mapPieces;
 		h & mapsConquered;
 		h & currentMap;

+ 107 - 201
lib/gameState/CGameStateCampaign.cpp

@@ -29,18 +29,6 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-void CrossoverHeroesList::addHeroToBothLists(CGHeroInstance * hero)
-{
-	heroesFromPreviousScenario.push_back(hero);
-	heroesFromAnyPreviousScenarios.push_back(hero);
-}
-
-void CrossoverHeroesList::removeHeroFromBothLists(CGHeroInstance * hero)
-{
-	heroesFromPreviousScenario -= hero;
-	heroesFromAnyPreviousScenarios -= hero;
-}
-
 CampaignHeroReplacement::CampaignHeroReplacement(CGHeroInstance * hero, const ObjectInstanceID & heroPlaceholderId):
 	hero(hero),
 	heroPlaceholderId(heroPlaceholderId)
@@ -58,77 +46,17 @@ std::optional<CampaignBonus> CGameStateCampaign::currentBonus() const
 {
 	auto campaignState = gameState->scenarioOps->campState;
 	return campaignState->getBonus(*campaignState->currentScenario());
-
 }
 
-CrossoverHeroesList CGameStateCampaign::getCrossoverHeroesFromPreviousScenarios() const
+std::optional<CampaignScenarioID> CGameStateCampaign::getHeroesSourceScenario() const
 {
-	CrossoverHeroesList crossoverHeroes;
-
 	auto campaignState = gameState->scenarioOps->campState;
 	auto bonus = currentBonus();
-	if(bonus && bonus->type == CampaignBonusType::HEROES_FROM_PREVIOUS_SCENARIO)
-	{
-		auto scenarioID = static_cast<CampaignScenarioID>(bonus->info2);
-
-		std::vector<CGHeroInstance *> heroes;
-		for(auto & node : campaignState->getCrossoverHeroes(scenarioID))
-		{
-			auto * h = CampaignState::crossoverDeserialize(node);
-			heroes.push_back(h);
-		}
-		crossoverHeroes.heroesFromAnyPreviousScenarios = heroes;
-		crossoverHeroes.heroesFromPreviousScenario = heroes;
-
-		return crossoverHeroes;
-	}
-
-	if(!campaignState->lastScenario())
-		return crossoverHeroes;
-
-	for(auto mapNr : campaignState->conqueredScenarios())
-	{
-		// create a list of deleted heroes
-		auto lostCrossoverHeroes = campaignState->getLostCrossoverHeroes(mapNr);
-
-		// remove heroes which didn't reached the end of the scenario, but were available at the start
-		for(auto * hero : lostCrossoverHeroes)
-		{
-			//					auto hero = CCampaignState::crossoverDeserialize(node);
-			vstd::erase_if(crossoverHeroes.heroesFromAnyPreviousScenarios, [hero](CGHeroInstance * h)
-			{
-				return hero->subID == h->subID;
-			});
-		}
-
-		// now add heroes which completed the scenario
-		for(auto node : campaignState->getCrossoverHeroes(mapNr))
-		{
-			auto * hero = CampaignState::crossoverDeserialize(node);
-			// add new heroes and replace old heroes with newer ones
-			auto it = range::find_if(crossoverHeroes.heroesFromAnyPreviousScenarios, [hero](CGHeroInstance * h)
-			{
-				return hero->subID == h->subID;
-			});
 
-			if(it != crossoverHeroes.heroesFromAnyPreviousScenarios.end())
-			{
-				// replace old hero with newer one
-				crossoverHeroes.heroesFromAnyPreviousScenarios[it - crossoverHeroes.heroesFromAnyPreviousScenarios.begin()] = hero;
-			}
-			else
-			{
-				// add new hero
-				crossoverHeroes.heroesFromAnyPreviousScenarios.push_back(hero);
-			}
+	if(bonus && bonus->type == CampaignBonusType::HEROES_FROM_PREVIOUS_SCENARIO)
+		return static_cast<CampaignScenarioID>(bonus->info2);;
 
-			if(mapNr == campaignState->lastScenario())
-			{
-				crossoverHeroes.heroesFromPreviousScenario.push_back(hero);
-			}
-		}
-	}
-	return crossoverHeroes;
+	return campaignState->lastScenario();
 }
 
 void CGameStateCampaign::trimCrossoverHeroesParameters(std::vector<CampaignHeroReplacement> & campaignHeroReplacements, const CampaignTravel & travelOptions)
@@ -194,7 +122,8 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vector<CampaignHeroR
 			for (size_t i = 0; i < totalArts; i++ )
 			{
 				auto artifactPosition = ArtifactPosition((si32)i);
-				if(artifactPosition == ArtifactPosition::SPELLBOOK) continue; // do not handle spellbook this way
+				if(artifactPosition == ArtifactPosition::SPELLBOOK)
+					continue; // do not handle spellbook this way
 
 				const ArtSlotInfo *info = hero->getSlot(artifactPosition);
 				if(!info)
@@ -242,23 +171,6 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vector<CampaignHeroR
 
 void CGameStateCampaign::placeCampaignHeroes()
 {
-	// WARNING: CURRENT CODE IS LIKELY INCORRECT AND LEADS TO MULTIPLE ISSUES WITH HERO TRANSFER
-	// Approximate behavior according to testing H3 game logic.
-	// 1) definitions:
-	// - 'reserved heroes' are heroes that have fixed placeholder in unfinished maps. See CMapHeader::reservedCampaignHeroes
-	// - 'campaign pool' are serialized heroes and is unique to a campaign
-	// - 'scenario pool' are serialized heroes and is unique to a scenario
-	//
-	// 2) scenario end logic:
-	// - at end of scenario, all 'reserved heroes' of a player go to 'campaign pool'
-	// - at end of scenario, rest of player's heroes go to 'scenario pool'
-	//
-	// 3) scenario start logic
-	// - at scenario start, all heroes that are placed on map but already exist in 'campaign pool' and are still 'reserved heroes' are replaced with other, randomly selected heroes (and probably marked as unavailable in map)
-	// - at scenario start, all fixed placeholders are replaced with heroes from 'campaign pool', if exist
-	// - at scenario start, all power placeholders owned by player are replaced by heroes from 'scenario pool' of last complete map, if exist
-	// - exception: if starting bonus is 'select player' then power placeholders are taken from 'scenario pool' of linked map
-
 	// place bonus hero
 	auto campaignState = gameState->scenarioOps->campState;
 	auto campaignBonus = campaignState->getBonus(*campaignState->currentScenario());
@@ -280,67 +192,66 @@ void CGameStateCampaign::placeCampaignHeroes()
 		}
 	}
 
-	// replace heroes placeholders
-	auto crossoverHeroes = getCrossoverHeroesFromPreviousScenarios();
+	logGlobal->debug("\tGenerate list of hero placeholders");
+	auto campaignHeroReplacements = generateCampaignHeroesToReplace();
 
-	if(!crossoverHeroes.heroesFromAnyPreviousScenarios.empty())
-	{
-		logGlobal->debug("\tGenerate list of hero placeholders");
-		auto campaignHeroReplacements = generateCampaignHeroesToReplace(crossoverHeroes);
+	logGlobal->debug("\tPrepare crossover heroes");
+	trimCrossoverHeroesParameters(campaignHeroReplacements, campaignState->scenario(*campaignState->currentScenario()).travelOptions);
+
+	// remove same heroes on the map which will be added through crossover heroes
+	// INFO: we will remove heroes because later it may be possible that the API doesn't allow having heroes
+	// with the same hero type id
+	std::vector<CGHeroInstance *> removedHeroes;
 
-		logGlobal->debug("\tPrepare crossover heroes");
-		trimCrossoverHeroesParameters(campaignHeroReplacements, campaignState->scenario(*campaignState->currentScenario()).travelOptions);
+	std::set<HeroTypeID> heroesToRemove = campaignState->getReservedHeroes();
 
-		// remove same heroes on the map which will be added through crossover heroes
-		// INFO: we will remove heroes because later it may be possible that the API doesn't allow having heroes
-		// with the same hero type id
-		std::vector<CGHeroInstance *> removedHeroes;
+	for(auto & campaignHeroReplacement : campaignHeroReplacements)
+		heroesToRemove.insert(HeroTypeID(campaignHeroReplacement.hero->subID));
 
-		for(auto & campaignHeroReplacement : campaignHeroReplacements)
+	for(auto & heroID : heroesToRemove)
+	{
+		auto * hero = gameState->getUsedHero(heroID);
+		if(hero)
 		{
-			auto * hero = gameState->getUsedHero(HeroTypeID(campaignHeroReplacement.hero->subID));
-			if(hero)
-			{
-				removedHeroes.push_back(hero);
-				gameState->map->heroesOnMap -= hero;
-				gameState->map->objects[hero->id.getNum()] = nullptr;
-				gameState->map->removeBlockVisTiles(hero, true);
-			}
+			removedHeroes.push_back(hero);
+			gameState->map->heroesOnMap -= hero;
+			gameState->map->objects[hero->id.getNum()] = nullptr;
+			gameState->map->removeBlockVisTiles(hero, true);
 		}
+	}
 
-		logGlobal->debug("\tReplace placeholders with heroes");
-		replaceHeroesPlaceholders(campaignHeroReplacements);
+	logGlobal->debug("\tReplace placeholders with heroes");
+	replaceHeroesPlaceholders(campaignHeroReplacements);
 
-		// now add removed heroes again with unused type ID
-		for(auto * hero : removedHeroes)
+	// now add removed heroes again with unused type ID
+	for(auto * hero : removedHeroes)
+	{
+		si32 heroTypeId = 0;
+		if(hero->ID == Obj::HERO)
+		{
+			heroTypeId = gameState->pickUnusedHeroTypeRandomly(hero->tempOwner);
+		}
+		else if(hero->ID == Obj::PRISON)
 		{
-			si32 heroTypeId = 0;
-			if(hero->ID == Obj::HERO)
+			auto unusedHeroTypeIds = gameState->getUnusedAllowedHeroes();
+			if(!unusedHeroTypeIds.empty())
 			{
-				heroTypeId = gameState->pickUnusedHeroTypeRandomly(hero->tempOwner);
-			}
-			else if(hero->ID == Obj::PRISON)
-			{
-				auto unusedHeroTypeIds = gameState->getUnusedAllowedHeroes();
-				if(!unusedHeroTypeIds.empty())
-				{
-					heroTypeId = (*RandomGeneratorUtil::nextItem(unusedHeroTypeIds, gameState->getRandomGenerator())).getNum();
-				}
-				else
-				{
-					logGlobal->error("No free hero type ID found to replace prison.");
-					assert(0);
-				}
+				heroTypeId = (*RandomGeneratorUtil::nextItem(unusedHeroTypeIds, gameState->getRandomGenerator())).getNum();
 			}
 			else
 			{
-				assert(0); // should not happen
+				logGlobal->error("No free hero type ID found to replace prison.");
+				assert(0);
 			}
-
-			hero->subID = heroTypeId;
-			hero->portrait = hero->subID;
-			gameState->map->getEditManager()->insertObject(hero);
 		}
+		else
+		{
+			assert(0); // should not happen
+		}
+
+		hero->subID = heroTypeId;
+		hero->portrait = hero->subID;
+		gameState->map->getEditManager()->insertObject(hero);
 	}
 }
 
@@ -424,23 +335,7 @@ void CGameStateCampaign::replaceHeroesPlaceholders(const std::vector<CampaignHer
 		heroToPlace->tempOwner = heroPlaceholder->tempOwner;
 		heroToPlace->pos = heroPlaceholder->pos;
 		heroToPlace->type = VLC->heroh->objects[heroToPlace->subID];
-		heroToPlace->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO,
-															   heroToPlace->type->heroClass->getIndex())->getTemplates().front();
-
-		for(auto &&i : heroToPlace->stacks)
-			i.second->type = VLC->creh->objects[i.second->getCreatureID()];
-
-		auto fixArtifact = [&](CArtifactInstance * art)
-		{
-			art->artType = VLC->arth->objects[art->artType->getId()];
-			gameState->map->artInstances.emplace_back(art);
-			art->id = ArtifactInstanceID((si32)gameState->map->artInstances.size() - 1);
-		};
-
-		for(auto &&i : heroToPlace->artifactsWorn)
-			fixArtifact(i.second.artifact);
-		for(auto &&i : heroToPlace->artifactsInBackpack)
-			fixArtifact(i.artifact);
+		heroToPlace->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, heroToPlace->type->heroClass->getIndex())->getTemplates().front();
 
 		gameState->map->removeBlockVisTiles(heroPlaceholder, true);
 		gameState->map->objects[heroPlaceholder->id.getNum()] = nullptr;
@@ -455,67 +350,78 @@ void CGameStateCampaign::replaceHeroesPlaceholders(const std::vector<CampaignHer
 	}
 }
 
-std::vector<CampaignHeroReplacement> CGameStateCampaign::generateCampaignHeroesToReplace(CrossoverHeroesList & crossoverHeroes)
+std::vector<CampaignHeroReplacement> CGameStateCampaign::generateCampaignHeroesToReplace()
 {
+	auto campaignState = gameState->scenarioOps->campState;
+
 	std::vector<CampaignHeroReplacement> campaignHeroReplacements;
+	std::vector<CGHeroPlaceholder *> placeholdersByPower;
+	std::vector<CGHeroPlaceholder *> placeholdersByType;
 
-	//selecting heroes by type
+	// find all placeholders on map
 	for(auto obj : gameState->map->objects)
 	{
-		if(obj && obj->ID == Obj::HERO_PLACEHOLDER)
-		{
-			auto * heroPlaceholder = dynamic_cast<CGHeroPlaceholder *>(obj.get());
-			if(heroPlaceholder->subID != 0xFF) //select by type
-			{
-				auto it = range::find_if(crossoverHeroes.heroesFromAnyPreviousScenarios, [heroPlaceholder](CGHeroInstance * hero)
-				{
-					return hero->subID == heroPlaceholder->subID;
-				});
+		if(!obj)
+			continue;
 
-				if(it != crossoverHeroes.heroesFromAnyPreviousScenarios.end())
-				{
-					auto * hero = *it;
-					crossoverHeroes.removeHeroFromBothLists(hero);
-					campaignHeroReplacements.emplace_back(CMemorySerializer::deepCopy(*hero).release(), heroPlaceholder->id);
-				}
-			}
-		}
-	}
+		if (obj->ID != Obj::HERO_PLACEHOLDER)
+			continue;
 
-	//selecting heroes by power
-	range::sort(crossoverHeroes.heroesFromPreviousScenario, [](const CGHeroInstance * a, const CGHeroInstance * b)
-	{
-		return a->getHeroStrength() > b->getHeroStrength();
-	}); //sort, descending strength
+		auto * heroPlaceholder = dynamic_cast<CGHeroPlaceholder *>(obj.get());
 
-	// sort hero placeholders descending power
-	std::vector<CGHeroPlaceholder *> heroPlaceholders;
-	for(auto obj : gameState->map->objects)
+		// only 1 field must be set
+		assert(heroPlaceholder->powerRank != heroPlaceholder->heroType);
+
+		if(heroPlaceholder->powerRank)
+			placeholdersByPower.push_back(heroPlaceholder);
+
+		if(heroPlaceholder->heroType)
+			placeholdersByType.push_back(heroPlaceholder);
+	}
+
+	//selecting heroes by type
+	for (auto const * placeholder : placeholdersByType)
 	{
-		if(obj && obj->ID == Obj::HERO_PLACEHOLDER)
+		auto const & node = campaignState->getHeroByType(*placeholder->heroType);
+		if (node.isNull())
 		{
-			auto * heroPlaceholder = dynamic_cast<CGHeroPlaceholder *>(obj.get());
-			if(heroPlaceholder->subID == 0xFF) //select by power
-			{
-				heroPlaceholders.push_back(heroPlaceholder);
-			}
+			logGlobal->info("Hero crossover: Unable to replace placeholder for %d (%s)!", placeholder->heroType->getNum(), VLC->heroTypes()->getById(*placeholder->heroType)->getNameTranslated());
+			continue;
 		}
+
+		CGHeroInstance * hero = CampaignState::crossoverDeserialize(node, gameState->map);
+
+		logGlobal->info("Hero crossover: Loading placeholder for %d (%s)", hero->subID, hero->getNameTranslated());
+
+		campaignHeroReplacements.emplace_back(hero, placeholder->id);
 	}
-	range::sort(heroPlaceholders, [](const CGHeroPlaceholder * a, const CGHeroPlaceholder * b)
-	{
-		return a->power > b->power;
-	});
 
-	for(int i = 0; i < heroPlaceholders.size(); ++i)
+	auto lastScenario = getHeroesSourceScenario();
+
+	if (!placeholdersByPower.empty() && lastScenario)
 	{
-		auto * heroPlaceholder = heroPlaceholders[i];
-		if(crossoverHeroes.heroesFromPreviousScenario.size() > i)
+		// sort hero placeholders descending power
+		boost::range::sort(placeholdersByPower, [](const CGHeroPlaceholder * a, const CGHeroPlaceholder * b)
+		{
+			return *a->powerRank > *b->powerRank;
+		});
+
+		auto const & nodeList = campaignState->getHeroesByPower(lastScenario.value());
+		auto nodeListIter = nodeList.begin();
+
+		for (auto const * placeholder : placeholdersByPower)
 		{
-			auto * hero = crossoverHeroes.heroesFromPreviousScenario[i];
-			campaignHeroReplacements.emplace_back(CMemorySerializer::deepCopy(*hero).release(), heroPlaceholder->id);
+			if (nodeListIter == nodeList.end())
+				break;
+
+			CGHeroInstance * hero = CampaignState::crossoverDeserialize(*nodeListIter, gameState->map);
+			nodeListIter++;
+
+			logGlobal->info("Hero crossover: Loading placeholder as %d (%s)", hero->subID, hero->getNameTranslated());
+
+			campaignHeroReplacements.emplace_back(hero, placeholder->id);
 		}
 	}
-
 	return campaignHeroReplacements;
 }
 

+ 4 - 10
lib/gameState/CGameStateCampaign.h

@@ -10,6 +10,7 @@
 #pragma once
 
 #include "../GameConstants.h"
+#include "../campaign/CampaignConstants.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -26,22 +27,15 @@ struct CampaignHeroReplacement
 	ObjectInstanceID heroPlaceholderId;
 };
 
-struct CrossoverHeroesList
-{
-	std::vector<CGHeroInstance *> heroesFromPreviousScenario;
-	std::vector<CGHeroInstance *> heroesFromAnyPreviousScenarios;
-	void addHeroToBothLists(CGHeroInstance * hero);
-	void removeHeroFromBothLists(CGHeroInstance * hero);
-};
-
 class CGameStateCampaign
 {
 	CGameState * gameState;
 
-	CrossoverHeroesList getCrossoverHeroesFromPreviousScenarios() const;
+	/// Returns ID of scenario from which hero placeholders should be selected
+	std::optional<CampaignScenarioID> getHeroesSourceScenario() const;
 
 	/// returns heroes and placeholders in where heroes will be put
-	std::vector<CampaignHeroReplacement> generateCampaignHeroesToReplace(CrossoverHeroesList & crossoverHeroes);
+	std::vector<CampaignHeroReplacement> generateCampaignHeroesToReplace();
 
 	std::optional<CampaignBonus> currentBonus() const;
 

+ 7 - 3
lib/mapObjects/CGHeroInstance.h

@@ -28,13 +28,17 @@ enum class EHeroGender : uint8_t;
 class CGHeroPlaceholder : public CGObjectInstance
 {
 public:
-	//subID stores id of hero type. If it's 0xff then following field is used
-	ui8 power;
+	/// if this is placeholder by power, then power rank of desired hero
+	std::optional<ui8> powerRank;
+
+	/// if this is placeholder by type, then hero type of desired hero
+	std::optional<HeroTypeID> heroType;
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{
 		h & static_cast<CGObjectInstance&>(*this);
-		h & power;
+		h & powerRank;
+		h & heroType;
 	}
 };
 

+ 2 - 3
lib/mapping/MapFormatH3M.cpp

@@ -1289,16 +1289,15 @@ CGObjectInstance * CMapLoaderH3M::readHeroPlaceholder(const int3 & mapPosition)
 	setOwnerAndValidate(mapPosition, object, reader->readPlayer());
 
 	HeroTypeID htid = reader->readHero(); //hero type id
-	object->subID = htid.getNum();
 
 	if(htid.getNum() == -1)
 	{
-		object->power = reader->readUInt8();
+		object->powerRank = reader->readUInt8();
 		logGlobal->debug("Map '%s': Hero placeholder: by power at %s, owned by %s", mapName, mapPosition.toString(), object->getOwner().getStr());
 	}
 	else
 	{
-		object->power = 0;
+		object->heroType = htid;
 		logGlobal->debug("Map '%s': Hero placeholder: %s at %s, owned by %s", mapName, VLC->heroh->getById(htid)->getJsonKey(), mapPosition.toString(), object->getOwner().getStr());
 	}
 

+ 36 - 36
server/CVCMIServer.cpp

@@ -408,51 +408,51 @@ void CVCMIServer::threadHandleClient(std::shared_ptr<CConnection> c)
 	setThreadName("CVCMIServer::handleConnection");
 	c->enterLobbyConnectionMode();
 
-#ifndef _MSC_VER
-	try
-	{
-#endif
+//#ifndef _MSC_VER
+//	try
+//	{
+//#endif
 		while(c->connected)
 		{
 			CPack * pack;
 			
-			try
-			{
+			//try
+			//{
 				pack = c->retrievePack();
-			}
-			catch(boost::system::system_error & e)
-			{
-				logNetwork->error("Network error receiving a pack. Connection %s dies. What happened: %s", c->toString(), e.what());
-				hangingConnections.insert(c);
-				connections.erase(c);
-				if(connections.empty() || hostClient == c)
-					state = EServerState::SHUTDOWN;
-				
-				if(gh && state == EServerState::GAMEPLAY)
-				{
-					gh->handleClientDisconnection(c);
-				}
-				break;
-			}
+			//}
+			//catch(boost::system::system_error & e)
+			//{
+			//	logNetwork->error("Network error receiving a pack. Connection %s dies. What happened: %s", c->toString(), e.what());
+			//	hangingConnections.insert(c);
+			//	connections.erase(c);
+			//	if(connections.empty() || hostClient == c)
+			//		state = EServerState::SHUTDOWN;
+			//
+			//	if(gh && state == EServerState::GAMEPLAY)
+			//	{
+			//		gh->handleClientDisconnection(c);
+			//	}
+			//	break;
+			//}
 
 			CVCMIServerPackVisitor visitor(*this, this->gh);
 			pack->visit(visitor);
 		}
-#ifndef _MSC_VER
-	 }
-	catch(const std::exception & e)
-	{
-        (void)e;
-		boost::unique_lock<boost::recursive_mutex> queueLock(mx);
-		logNetwork->error("%s dies... \nWhat happened: %s", c->toString(), e.what());
-	}
-	catch(...)
-	{
-		state = EServerState::SHUTDOWN;
-		handleException();
-		throw;
-	}
-#endif
+//#ifndef _MSC_VER
+//	 }
+//	catch(const std::exception & e)
+//	{
+//        (void)e;
+//		boost::unique_lock<boost::recursive_mutex> queueLock(mx);
+//		logNetwork->error("%s dies... \nWhat happened: %s", c->toString(), e.what());
+//	}
+//	catch(...)
+//	{
+//		state = EServerState::SHUTDOWN;
+//		handleException();
+//		throw;
+//	}
+//#endif
 
 	boost::unique_lock<boost::recursive_mutex> queueLock(mx);
 //	if(state != ENDING_AND_STARTING_GAME)