Browse Source

Added encapsulation for CampaignState class

Ivan Savenko 2 years ago
parent
commit
a08fe09517

+ 2 - 2
client/ClientCommandManager.cpp

@@ -210,8 +210,8 @@ void ClientCommandManager::handleConvertTextCommand()
 	for (auto const & campaignName : campaignList)
 	{
 		auto state = CampaignHandler::getCampaign(campaignName.getName());
-		for (auto const & part : state->mapPieces)
-			state->getMap(part.first);
+		for (auto const & part : state->allScenarios())
+			state->getMap(part);
 	}
 
 	VLC->generaltexth->dumpAllTexts();

+ 2 - 2
client/eventsSDL/UserEventHandler.cpp

@@ -51,10 +51,10 @@ void UserEventHandler::handleUserEvent(const SDL_UserEvent & user)
 			CSH->campaignServerRestartLock.set(true);
 			CSH->endGameplay();
 			auto ourCampaign = std::shared_ptr<CampaignState>(reinterpret_cast<CampaignState *>(user.data1));
-			auto & epilogue = ourCampaign->scenarios[ourCampaign->mapsConquered.back()].epilog;
+			auto & epilogue = ourCampaign->scenario(*ourCampaign->lastScenario()).epilog;
 			auto finisher = [=]()
 			{
-				if(!ourCampaign->mapsRemaining.empty())
+				if(!ourCampaign->isCampaignFinished())
 				{
 					GH.windows().pushWindow(CMM);
 					GH.windows().pushWindow(CMM->menu);

+ 19 - 23
client/lobby/CBonusSelection.cpp

@@ -64,7 +64,7 @@ CBonusSelection::CBonusSelection()
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 
-	std::string bgName = getCampaign()->header.campaignRegions.campPrefix + "_BG.BMP";
+	std::string bgName = getCampaign()->getHeader().campaignRegions.campPrefix + "_BG.BMP";
 	setBackground(bgName);
 
 	panelBackground = std::make_shared<CPicture>("CAMPBRF.BMP", 456, 6);
@@ -78,7 +78,7 @@ CBonusSelection::CBonusSelection()
 	iconsMapSizes = std::make_shared<CAnimImage>("SCNRMPSZ", 4, 0, 735, 26);
 
 	labelCampaignDescription = std::make_shared<CLabel>(481, 63, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[38]);
-	campaignDescription = std::make_shared<CTextBox>(getCampaign()->header.description, Rect(480, 86, 286, 117), 1);
+	campaignDescription = std::make_shared<CTextBox>(getCampaign()->getHeader().description, Rect(480, 86, 286, 117), 1);
 
 	mapName = std::make_shared<CLabel>(481, 219, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->mi->getName());
 	labelMapDescription = std::make_shared<CLabel>(481, 253, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::YELLOW, CGI->generaltexth->allTexts[496]);
@@ -99,27 +99,25 @@ CBonusSelection::CBonusSelection()
 		difficultyIcons[b] = std::make_shared<CAnimImage>("GSPBUT" + std::to_string(b + 3) + ".DEF", 0, 0, 709, 455);
 	}
 
-	if(getCampaign()->header.difficultyChoosenByPlayer)
+	if(getCampaign()->getHeader().difficultyChoosenByPlayer)
 	{
 		buttonDifficultyLeft = std::make_shared<CButton>(Point(694, 508), "SCNRBLF.DEF", CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this));
 		buttonDifficultyRight = std::make_shared<CButton>(Point(738, 508), "SCNRBRT.DEF", CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this));
 	}
 
-	for(int g = 0; g < getCampaign()->scenarios.size(); ++g)
+	for(auto scenarioID : getCampaign()->allScenarios())
 	{
-		auto scenarioID = static_cast<CampaignScenarioID>(g);
-
-		if(getCampaign()->conquerable(scenarioID))
-			regions.push_back(std::make_shared<CRegion>(scenarioID, true, true, getCampaign()->header.campaignRegions));
-		else if(getCampaign()->scenarios[scenarioID].conquered) //display as striped
-			regions.push_back(std::make_shared<CRegion>(scenarioID, false, false, getCampaign()->header.campaignRegions));
+		if(getCampaign()->isAvailable(scenarioID))
+			regions.push_back(std::make_shared<CRegion>(scenarioID, true, true, getCampaign()->getHeader().campaignRegions));
+		else if(getCampaign()->isConquered(scenarioID)) //display as striped
+			regions.push_back(std::make_shared<CRegion>(scenarioID, false, false, getCampaign()->getHeader().campaignRegions));
 	}
 }
 
 void CBonusSelection::createBonusesIcons()
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-	const CampaignScenario & scenario = getCampaign()->scenarios[CSH->campaignMap];
+	const CampaignScenario & scenario = getCampaign()->scenario(CSH->campaignMap);
 	const std::vector<CampaignBonus> & bonDescs = scenario.travelOptions.bonusesToChoose;
 	groupBonuses = std::make_shared<CToggleGroup>(std::bind(&IServerAPI::setCampaignBonus, CSH, _1));
 
@@ -172,7 +170,7 @@ void CBonusSelection::createBonusesIcons()
 			assert(faction != -1);
 
 			BuildingID buildID;
-			if(getCampaign()->header.version == CampaignVersion::VCMI)
+			if(getCampaign()->getHeader().version == CampaignVersion::VCMI)
 				buildID = BuildingID(bonDescs[i].info1);
 			else
 				buildID = CBuildingHandler::campToERMU(bonDescs[i].info1, faction, std::set<BuildingID>());
@@ -272,13 +270,13 @@ void CBonusSelection::createBonusesIcons()
 		}
 		case CampaignBonusType::HEROES_FROM_PREVIOUS_SCENARIO:
 		{
-			auto superhero = getCampaign()->scenarios[static_cast<CampaignScenarioID>(bonDescs[i].info2)].strongestHero(PlayerColor(bonDescs[i].info1));
+			auto superhero = getCampaign()->strongestHero(static_cast<CampaignScenarioID>(bonDescs[i].info2), PlayerColor(bonDescs[i].info1));
 			if(!superhero)
 				logGlobal->warn("No superhero! How could it be transferred?");
 			picNumber = superhero ? superhero->portrait : 0;
 			desc = CGI->generaltexth->allTexts[719];
 
-			boost::algorithm::replace_first(desc, "%s", getCampaign()->scenarios[static_cast<CampaignScenarioID>(bonDescs[i].info2)].scenarioName);
+			boost::algorithm::replace_first(desc, "%s", getCampaign()->scenario(static_cast<CampaignScenarioID>(bonDescs[i].info2)).scenarioName);
 			break;
 		}
 
@@ -313,10 +311,8 @@ void CBonusSelection::createBonusesIcons()
 		groupBonuses->addToggle(i, bonusButton);
 	}
 
-	if(vstd::contains(getCampaign()->chosenCampaignBonuses, CSH->campaignMap))
-	{
-		groupBonuses->setSelected(getCampaign()->chosenCampaignBonuses[CSH->campaignMap]);
-	}
+	if(getCampaign()->getBonusID(CSH->campaignMap))
+		groupBonuses->setSelected(*getCampaign()->getBonusID(CSH->campaignMap));
 }
 
 void CBonusSelection::updateAfterStateChange()
@@ -325,7 +321,7 @@ void CBonusSelection::updateAfterStateChange()
 	{
 		buttonRestart->disable();
 		buttonStart->enable();
-		if(!getCampaign()->mapsConquered.empty())
+		if(!getCampaign()->conqueredScenarios().empty())
 			buttonBack->block(true);
 		else
 			buttonBack->block(false);
@@ -342,7 +338,7 @@ void CBonusSelection::updateAfterStateChange()
 	}
 	if(CSH->campaignBonus == -1)
 	{
-		buttonStart->block(getCampaign()->scenarios[CSH->campaignMap].travelOptions.bonusesToChoose.size());
+		buttonStart->block(getCampaign()->scenario(CSH->campaignMap).travelOptions.bonusesToChoose.size());
 	}
 	else if(buttonStart->isBlocked())
 	{
@@ -398,7 +394,7 @@ void CBonusSelection::startMap()
 			CSH->sendStartGame();
 		};
 
-		const CampaignScenario & scenario = getCampaign()->scenarios[CSH->campaignMap];
+		const CampaignScenario & scenario = getCampaign()->scenario(CSH->campaignMap);
 		if(scenario.prolog.hasPrologEpilog)
 		{
 			GH.windows().createAndPushWindow<CPrologEpilogVideo>(scenario.prolog, exitCb);
@@ -464,7 +460,7 @@ CBonusSelection::CRegion::CRegion(CampaignScenarioID id, bool accessible, bool s
 	pos.y += desc.ypos;
 
 	std::string prefix = campDsc.campPrefix + desc.infix + "_";
-	std::string suffix = colors[campDsc.colorSuffixLength - 1][CSH->si->campState->scenarios[idOfMapAndRegion].regionColor];
+	std::string suffix = colors[campDsc.colorSuffixLength - 1][CSH->si->campState->scenario(idOfMapAndRegion).regionColor];
 	graphicsNotSelected = std::make_shared<CPicture>(prefix + "En" + suffix + ".BMP");
 	graphicsNotSelected->disable();
 	graphicsSelected = std::make_shared<CPicture>(prefix + "Se" + suffix + ".BMP");
@@ -513,7 +509,7 @@ void CBonusSelection::CRegion::clickLeft(tribool down, bool previousState)
 void CBonusSelection::CRegion::showPopupWindow()
 {
 	// FIXME: For some reason "down" is only ever contain indeterminate_value
-	auto text = CSH->si->campState->scenarios[idOfMapAndRegion].regionText;
+	auto text = CSH->si->campState->scenario(idOfMapAndRegion).regionText;
 	if(!graphicsNotSelected->getSurface()->isTransparent(GH.getCursorPosition() - pos.topLeft()) && text.size())
 	{
 		CRClickPopup::createAndPush(text);

+ 1 - 1
client/windows/CCastleInterface.cpp

@@ -923,7 +923,7 @@ 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 && si->campState &&                // We're in campaign,
-			(si->campState->header.filename == "DATA/YOG.H3C") && // which is "Birth of a Barbarian",
+			(si->campState->getHeader().filename == "DATA/YOG.H3C") && // which is "Birth of a Barbarian",
 			(hero->subID == 45))                                        // and the hero is Yog (based on Solmyr)
 		{
 			// "Yog has given up magic in all its forms..."

+ 0 - 14
lib/NetPacksLib.cpp

@@ -1076,21 +1076,7 @@ void PlayerEndsGame::applyGs(CGameState * gs) const
 					// keep all heroes from the winning player
 					crossoverHeroes.push_back(hero);
 				}
-				else if (vstd::contains(gs->scenarioOps->campState->getCurrentScenario().keepHeroes, HeroTypeID(hero->subID)))
-				{
-					// keep hero whether lost or won (like Xeron in AB campaign)
-					crossoverHeroes.push_back(hero);
-				}
-			}
-			// keep lost heroes which are in heroes pool
-			for (auto & heroPair : gs->hpool.heroesPool)
-			{
-				if (vstd::contains(gs->scenarioOps->campState->getCurrentScenario().keepHeroes, HeroTypeID(heroPair.first)))
-				{
-					crossoverHeroes.push_back(heroPair.second.get());
-				}
 			}
-
 			gs->scenarioOps->campState->setCurrentMapAsConquered(crossoverHeroes);
 		}
 	}

+ 2 - 2
lib/StartInfo.cpp

@@ -62,8 +62,8 @@ PlayerSettings * StartInfo::getPlayersSettings(const ui8 connectedPlayerId)
 
 std::string StartInfo::getCampaignName() const
 {
-	if(campState->header.name.length())
-		return campState->header.name;
+	if(campState->getHeader().name.empty())
+		return campState->getHeader().name;
 	else
 		return VLC->generaltexth->allTexts[508];
 }

+ 0 - 10
lib/campaign/CampaignHandler.cpp

@@ -111,14 +111,6 @@ std::shared_ptr<CampaignState> CampaignHandler::getCampaign( const std::string &
 		scenarioID++;
 	}
 
-	for(int i = 0; i < ret->scenarios.size(); i++)
-	{
-		auto scenarioID = static_cast<CampaignScenarioID>(i);
-
-		if(vstd::contains(ret->mapPieces, scenarioID)) //not all maps must be present in a campaign
-			ret->mapsRemaining.push_back(scenarioID);
-	}
-
 	return ret;
 }
 
@@ -188,7 +180,6 @@ CampaignScenario CampaignHandler::readScenarioFromJson(JsonNode & reader)
 	};
 
 	CampaignScenario ret;
-	ret.conquered = false;
 	ret.mapName = reader["map"].String();
 	for(auto & g : reader["preconditions"].Vector())
 		ret.preconditionRegions.insert(static_cast<CampaignScenarioID>(g.Integer()));
@@ -428,7 +419,6 @@ CampaignScenario CampaignHandler::readScenarioFromMemory( CBinaryReader & reader
 	};
 
 	CampaignScenario ret;
-	ret.conquered = false;
 	ret.mapName = reader.readBaseString();
 	reader.readUInt32(); //packedMapSize - not used
 	if(header.numberOfScenarios > 8) //unholy alliance

+ 103 - 30
lib/campaign/CampaignState.cpp

@@ -83,7 +83,12 @@ void CampaignHeader::loadLegacyData(ui8 campId)
 	numberOfScenarios = VLC->generaltexth->getCampaignLength(campId);
 }
 
-bool CampaignState::conquerable(CampaignScenarioID whichScenario) const
+bool CampaignState::isConquered(CampaignScenarioID whichScenario) const
+{
+	return vstd::contains(mapsConquered, whichScenario);
+}
+
+bool CampaignState::isAvailable(CampaignScenarioID whichScenario) const
 {
 	//check for void scenraio
 	if (!scenarios.at(whichScenario).isNotVoid())
@@ -91,14 +96,14 @@ bool CampaignState::conquerable(CampaignScenarioID whichScenario) const
 		return false;
 	}
 
-	if (scenarios.at(whichScenario).conquered)
+	if (vstd::contains(mapsConquered, whichScenario))
 	{
 		return false;
 	}
 	//check preconditioned regions
 	for (auto const & it : scenarios.at(whichScenario).preconditionRegions)
 	{
-		if (!scenarios.at(it).conquered)
+		if (!vstd::contains(mapsConquered, it))
 			return false;
 	}
 	return true;
@@ -109,18 +114,18 @@ bool CampaignScenario::isNotVoid() const
 	return !mapName.empty();
 }
 
-const CGHeroInstance * CampaignScenario::strongestHero(const PlayerColor & owner)
+const CGHeroInstance * CampaignState::strongestHero(CampaignScenarioID scenarioId, const PlayerColor & owner) const
 {
-	std::function<bool(JsonNode & node)> isOwned = [owner](JsonNode & node)
+	std::function<bool(const JsonNode & node)> isOwned = [owner](const JsonNode & node)
 	{
 		auto * h = CampaignState::crossoverDeserialize(node);
 		bool result = h->tempOwner == owner;
 		vstd::clear_pointer(h);
 		return result;
 	};
-	auto ownedHeroes = crossoverHeroes | boost::adaptors::filtered(isOwned);
+	auto ownedHeroes = crossover.placedHeroes.at(scenarioId) | boost::adaptors::filtered(isOwned);
 
-	auto i = vstd::maxElementByFun(ownedHeroes, [](JsonNode & node)
+	auto i = vstd::maxElementByFun(ownedHeroes, [](const JsonNode & node)
 	{
 		auto * h = CampaignState::crossoverDeserialize(node);
 		double result = h->getHeroStrength();
@@ -130,41 +135,43 @@ const CGHeroInstance * CampaignScenario::strongestHero(const PlayerColor & owner
 	return i == ownedHeroes.end() ? nullptr : CampaignState::crossoverDeserialize(*i);
 }
 
-std::vector<CGHeroInstance *> CampaignScenario::getLostCrossoverHeroes()
+std::vector<CGHeroInstance *> CampaignState::getLostCrossoverHeroes(CampaignScenarioID scenarioId) const
 {
 	std::vector<CGHeroInstance *> lostCrossoverHeroes;
-	if(conquered)
+
+	for(auto node2 :  crossover.placedHeroes.at(scenarioId))
 	{
-		for(auto node2 : placedCrossoverHeroes)
+		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())
 		{
-			auto * hero = CampaignState::crossoverDeserialize(node2);
-			auto it = range::find_if(crossoverHeroes, [hero](JsonNode node)
-			{
-				auto * h = CampaignState::crossoverDeserialize(node);
-				bool result = hero->subID == h->subID;
-				vstd::clear_pointer(h);
-				return result;
-			});
-			if(it == crossoverHeroes.end())
-			{
-				lostCrossoverHeroes.push_back(hero);
-			}
+			lostCrossoverHeroes.push_back(hero);
 		}
 	}
+
 	return lostCrossoverHeroes;
 }
 
+std::vector<JsonNode> CampaignState::getCrossoverHeroes(CampaignScenarioID scenarioId) const
+{
+	return crossover.crossoverHeroes.at(scenarioId);
+}
+
 void CampaignState::setCurrentMapAsConquered(const std::vector<CGHeroInstance *> & heroes)
 {
-	scenarios.at(*currentMap).crossoverHeroes.clear();
+	crossover.crossoverHeroes[*currentMap].clear();
 	for(CGHeroInstance * hero : heroes)
 	{
-		scenarios.at(*currentMap).crossoverHeroes.push_back(crossoverSerialize(hero));
+		crossover.crossoverHeroes[*currentMap].push_back(crossoverSerialize(hero));
 	}
 
 	mapsConquered.push_back(*currentMap);
-	mapsRemaining -= *currentMap;
-	scenarios.at(*currentMap).conquered = true;
 }
 
 std::optional<CampaignBonus> CampaignState::getBonusForCurrentMap() const
@@ -183,9 +190,12 @@ const CampaignScenario & CampaignState::getCurrentScenario() const
 	return scenarios.at(*currentMap);
 }
 
-CampaignScenario & CampaignState::getCurrentScenario()
+std::optional<ui8> CampaignState::getBonusID(CampaignScenarioID & which) const
 {
-	return scenarios.at(*currentMap);
+	if (!chosenCampaignBonuses.count(which))
+		return std::nullopt;
+
+	return chosenCampaignBonuses.at(which);
 }
 
 ui8 CampaignState::currentBonusID() const
@@ -242,11 +252,74 @@ JsonNode CampaignState::crossoverSerialize(CGHeroInstance * hero)
 	return node;
 }
 
-CGHeroInstance * CampaignState::crossoverDeserialize(JsonNode & node)
+CGHeroInstance * CampaignState::crossoverDeserialize(const JsonNode & node)
 {
-	JsonDeserializer handler(nullptr, node);
+	JsonDeserializer handler(nullptr, const_cast<JsonNode&>(node));
 	auto * hero = new CGHeroInstance();
 	hero->ID = Obj::HERO;
 	hero->serializeJsonOptions(handler);
 	return hero;
 }
+
+void CampaignState::setCurrentMap(CampaignScenarioID which)
+{
+	assert(scenarios.count(which));
+	assert(scenarios.at(which).isNotVoid());
+
+	currentMap = which;
+}
+
+void CampaignState::setCurrentMapBonus(ui8 which)
+{
+	chosenCampaignBonuses[*currentMap] = which;
+}
+
+std::optional<CampaignScenarioID> CampaignState::currentScenario() const
+{
+	return currentMap;
+}
+
+std::optional<CampaignScenarioID> CampaignState::lastScenario() const
+{
+	if (mapsConquered.empty())
+		return std::nullopt;
+	return mapsConquered.back();
+}
+
+std::set<CampaignScenarioID> CampaignState::conqueredScenarios() const
+{
+	std::set<CampaignScenarioID> result;
+	result.insert(mapsConquered.begin(), mapsConquered.end());
+	return result;
+}
+
+std::set<CampaignScenarioID> CampaignState::allScenarios() const
+{
+	std::set<CampaignScenarioID> result;
+
+	for (auto const & entry : scenarios)
+	{
+		if (entry.second.isNotVoid())
+			result.insert(entry.first);
+	}
+
+	return result;
+}
+
+const CampaignScenario & CampaignState::scenario(CampaignScenarioID which) const
+{
+	assert(scenarios.count(which));
+	assert(scenarios.at(which).isNotVoid());
+
+	return scenarios.at(which);
+}
+
+bool CampaignState::isCampaignFinished() const
+{
+	return conqueredScenarios() == allScenarios();
+}
+
+const CampaignHeader & CampaignState::getHeader() const
+{
+	return header;
+}

+ 54 - 20
lib/campaign/CampaignState.h

@@ -156,22 +156,15 @@ public:
 	std::set<CampaignScenarioID> preconditionRegions; //what we need to conquer to conquer this one (stored as bitfield in h3c)
 	ui8 regionColor = 0;
 	ui8 difficulty = 0;
-	bool conquered = false;
 
 	std::string regionText;
 	CampaignScenarioPrologEpilog prolog;
 	CampaignScenarioPrologEpilog epilog;
 
 	CampaignTravel travelOptions;
-	std::vector<HeroTypeID> keepHeroes; // contains list of heroes which should be kept for next scenario (doesn't matter if they lost)
-	std::vector<JsonNode> crossoverHeroes; // contains all heroes with the same state when the campaign scenario was finished
-	std::vector<JsonNode> placedCrossoverHeroes; // contains all placed crossover heroes defined by hero placeholders when the scenario was started
 
 	void loadPreconditionRegions(ui32 regions);
 	bool isNotVoid() const;
-	// FIXME: due to usage of JsonNode I can't make these methods const
-	const CGHeroInstance * strongestHero(const PlayerColor & owner);
-	std::vector<CGHeroInstance *> getLostCrossoverHeroes(); /// returns a list of crossover heroes which started the scenario, but didn't complete it
 
 	template <typename Handler> void serialize(Handler &h, const int formatVersion)
 	{
@@ -180,44 +173,85 @@ public:
 		h & preconditionRegions;
 		h & regionColor;
 		h & difficulty;
-		h & conquered;
 		h & regionText;
 		h & prolog;
 		h & epilog;
 		h & travelOptions;
+	}
+};
+
+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 & placedCrossoverHeroes;
-		h & keepHeroes;
+		h & placedHeroes;
 	}
 };
 
 class DLL_LINKAGE CampaignState
 {
-public:
-	CampaignHeader header;
-	std::map<CampaignScenarioID, CampaignScenario> scenarios;
-	std::map<CampaignScenarioID, std::string > mapPieces; //binary h3ms, scenario number -> map data
+	friend class CampaignHandler;
 
+	/// List of all maps completed by player, in order of their completion
 	std::vector<CampaignScenarioID> mapsConquered;
-	std::vector<CampaignScenarioID> mapsRemaining;
-	std::optional<CampaignScenarioID> currentMap;
 
+	std::map<CampaignScenarioID, CampaignScenario> scenarios;
+	std::map<CampaignScenarioID, std::string > mapPieces; //binary h3ms, scenario number -> map data
 	std::map<CampaignScenarioID, ui8> chosenCampaignBonuses;
+	std::optional<CampaignScenarioID> currentMap;
+
+	CampaignHeader header;
+	CampaignHeroes crossover;
 
 public:
+	std::optional<CampaignScenarioID> lastScenario() const;
+	std::optional<CampaignScenarioID> currentScenario() const;
+	std::set<CampaignScenarioID> allScenarios() const;
+	std::set<CampaignScenarioID> conqueredScenarios() const;
+
+	const CampaignScenario & scenario(CampaignScenarioID which) const;
+
 	std::optional<CampaignBonus> getBonusForCurrentMap() const;
 	const CampaignScenario & getCurrentScenario() const;
+
+	std::optional<ui8> getBonusID(CampaignScenarioID & which) const;
 	ui8 currentBonusID() const;
-	bool conquerable(CampaignScenarioID whichScenario) const;
+
+	/// Returns true if selected scenario can be selected and started by player
+	bool isAvailable(CampaignScenarioID whichScenario) const;
+
+	/// Returns true if selected scenario has been already completed by player
+	bool isConquered(CampaignScenarioID whichScenario) const;
+
+	/// Returns true if all available scenarios have been completed and campaign is finished
+	bool isCampaignFinished() const;
+
+	const CampaignHeader & getHeader() const;
 
 	std::unique_ptr<CMap> getMap(CampaignScenarioID scenarioId) const;
 	std::unique_ptr<CMapHeader> getMapHeader(CampaignScenarioID scenarioId) const;
 	std::shared_ptr<CMapInfo> getMapInfo(CampaignScenarioID scenarioId) const;
 
-	CampaignScenario & getCurrentScenario();
+	void setCurrentMap(CampaignScenarioID which);
+	void setCurrentMapBonus(ui8 which);
 	void setCurrentMapAsConquered(const std::vector<CGHeroInstance*> & heroes);
+
+	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;
+
+	std::vector<JsonNode> getCrossoverHeroes(CampaignScenarioID scenarioId) const;
+
 	static JsonNode crossoverSerialize(CGHeroInstance * hero);
-	static CGHeroInstance * crossoverDeserialize(JsonNode & node);
+	static CGHeroInstance * crossoverDeserialize(const JsonNode & node);
 
 	CampaignState() = default;
 
@@ -225,8 +259,8 @@ public:
 	{
 		h & header;
 		h & scenarios;
+		h & crossover;
 		h & mapPieces;
-		h & mapsRemaining;
 		h & mapsConquered;
 		h & currentMap;
 		h & chosenCampaignBonuses;

+ 8 - 11
lib/gameState/CGameStateCampaign.cpp

@@ -65,7 +65,7 @@ CrossoverHeroesList CGameStateCampaign::getCrossoverHeroesFromPreviousScenarios(
 		auto scenarioID = static_cast<CampaignScenarioID>(bonus->info2);
 
 		std::vector<CGHeroInstance *> heroes;
-		for(auto & node : campaignState->scenarios.at(scenarioID).crossoverHeroes)
+		for(auto & node : campaignState->getCrossoverHeroes(scenarioID))
 		{
 			auto * h = CampaignState::crossoverDeserialize(node);
 			heroes.push_back(h);
@@ -76,14 +76,13 @@ CrossoverHeroesList CGameStateCampaign::getCrossoverHeroesFromPreviousScenarios(
 		return crossoverHeroes;
 	}
 
-	if(campaignState->mapsConquered.empty())
+	if(!campaignState->lastScenario())
 		return crossoverHeroes;
 
-	for(auto mapNr : campaignState->mapsConquered)
+	for(auto mapNr : campaignState->conqueredScenarios())
 	{
 		// create a list of deleted heroes
-		auto & scenario = campaignState->scenarios[mapNr];
-		auto lostCrossoverHeroes = scenario.getLostCrossoverHeroes();
+		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)
@@ -96,7 +95,7 @@ CrossoverHeroesList CGameStateCampaign::getCrossoverHeroesFromPreviousScenarios(
 		}
 
 		// now add heroes which completed the scenario
-		for(auto node : scenario.crossoverHeroes)
+		for(auto node : campaignState->getCrossoverHeroes(mapNr))
 		{
 			auto * hero = CampaignState::crossoverDeserialize(node);
 			// add new heroes and replace old heroes with newer ones
@@ -116,7 +115,7 @@ CrossoverHeroesList CGameStateCampaign::getCrossoverHeroesFromPreviousScenarios(
 				crossoverHeroes.heroesFromAnyPreviousScenarios.push_back(hero);
 			}
 
-			if(mapNr == campaignState->mapsConquered.back())
+			if(mapNr == campaignState->lastScenario())
 			{
 				crossoverHeroes.heroesFromPreviousScenario.push_back(hero);
 			}
@@ -392,7 +391,7 @@ void CGameStateCampaign::giveCampaignBonusToHero(CGHeroInstance * hero)
 					continue;
 				}
 				auto bb = std::make_shared<Bonus>(
-					BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CAMPAIGN_BONUS, val, static_cast<int>(*gameState->scenarioOps->campState->currentMap), g
+					BonusDuration::PERMANENT, BonusType::PRIMARY_SKILL, BonusSource::CAMPAIGN_BONUS, val, static_cast<int>(*gameState->scenarioOps->campState->currentScenario()), g
 				);
 				hero->addNewBonus(bb);
 			}
@@ -445,8 +444,6 @@ void CGameStateCampaign::replaceHeroesPlaceholders(const std::vector<CampaignHer
 		gameState->map->instanceNames[heroToPlace->instanceName] = heroToPlace;
 
 		delete heroPlaceholder;
-
-		gameState->scenarioOps->campState->getCurrentScenario().placedCrossoverHeroes.push_back(CampaignState::crossoverSerialize(heroToPlace));
 	}
 }
 
@@ -628,7 +625,7 @@ void CGameStateCampaign::initTowns()
 					gameState->map->towns[g]->pos == pi.posOfMainTown)
 				{
 					BuildingID buildingId;
-					if(gameState->scenarioOps->campState->header.version == CampaignVersion::VCMI)
+					if(gameState->scenarioOps->campState->getHeader().version == CampaignVersion::VCMI)
 						buildingId = BuildingID(chosenBonus->info1);
 					else
 						buildingId = CBuildingHandler::campToERMU(chosenBonus->info1, gameState->map->towns[g]->subID, gameState->map->towns[g]->builtBuildings);

+ 5 - 5
server/CVCMIServer.cpp

@@ -296,8 +296,8 @@ bool CVCMIServer::prepareToStartGame()
 	{
 	case StartInfo::CAMPAIGN:
 		logNetwork->info("Preparing to start new campaign");
-		si->campState->currentMap = std::make_optional(campaignMap);
-		si->campState->chosenCampaignBonuses[campaignMap] = campaignBonus;
+		si->campState->setCurrentMap(campaignMap);
+		si->campState->setCurrentMapBonus(campaignBonus);
 		gh->init(si.get());
 		break;
 
@@ -668,7 +668,7 @@ void CVCMIServer::updateStartInfoOnMapChange(std::shared_ptr<CMapInfo> mapInfo,
 		si = CMemorySerializer::deepCopy(*mi->scenarioOptionsOfSave);
 		si->mode = StartInfo::LOAD_GAME;
 		if(si->campState)
-			campaignMap = si->campState->currentMap.value();
+			campaignMap = si->campState->currentScenario().value();
 
 		for(auto & ps : si->playerInfos)
 		{
@@ -873,7 +873,7 @@ void CVCMIServer::optionNextCastle(PlayerColor player, int dir)
 void CVCMIServer::setCampaignMap(CampaignScenarioID mapId)
 {
 	campaignMap = mapId;
-	si->difficulty = si->campState->scenarios[mapId].difficulty;
+	si->difficulty = si->campState->scenario(mapId).difficulty;
 	campaignBonus = -1;
 	updateStartInfoOnMapChange(si->campState->getMapInfo(mapId));
 }
@@ -882,7 +882,7 @@ void CVCMIServer::setCampaignBonus(int bonusId)
 {
 	campaignBonus = bonusId;
 
-	const CampaignScenario & scenario = si->campState->scenarios[campaignMap];
+	const CampaignScenario & scenario = si->campState->scenario(campaignMap);
 	const std::vector<CampaignBonus> & bonDescs = scenario.travelOptions.bonusesToChoose;
 	if(bonDescs[bonusId].type == CampaignBonusType::HERO)
 	{

+ 7 - 7
server/NetPacksLobbyServer.cpp

@@ -211,18 +211,18 @@ void ApplyOnServerNetPackVisitor::visitLobbySetMap(LobbySetMap & pack)
 
 void ApplyOnServerNetPackVisitor::visitLobbySetCampaign(LobbySetCampaign & pack)
 {
-	srv.si->mapname = pack.ourCampaign->header.filename;
+	srv.si->mapname = pack.ourCampaign->getHeader().filename;
 	srv.si->mode = StartInfo::CAMPAIGN;
 	srv.si->campState = pack.ourCampaign;
 	srv.si->turnTime = 0;
-	bool isCurrentMapConquerable = pack.ourCampaign->currentMap && pack.ourCampaign->conquerable(*pack.ourCampaign->currentMap);
-	for(int i = 0; i < pack.ourCampaign->scenarios.size(); i++)
-	{
-		auto scenarioID = static_cast<CampaignScenarioID>(i);
 
-		if(pack.ourCampaign->conquerable(scenarioID))
+	bool isCurrentMapConquerable = pack.ourCampaign->currentScenario() && pack.ourCampaign->isAvailable(*pack.ourCampaign->currentScenario());
+
+	for(auto scenarioID : pack.ourCampaign->allScenarios())
+	{
+		if(pack.ourCampaign->isAvailable(scenarioID))
 		{
-			if(!isCurrentMapConquerable || (isCurrentMapConquerable && scenarioID == *pack.ourCampaign->currentMap))
+			if(!isCurrentMapConquerable || (isCurrentMapConquerable && scenarioID == *pack.ourCampaign->currentScenario()))
 			{
 				srv.setCampaignMap(scenarioID);
 			}