Browse Source

Merge pull request #1915 from Nordsoft91/vcmi-campaign

VCMI campaigns format
Nordsoft91 2 years ago
parent
commit
f39f208e05

+ 13 - 39
client/lobby/CBonusSelection.cpp

@@ -51,6 +51,7 @@
 
 #include "../../lib/mapObjects/CGHeroInstance.h"
 
+
 std::shared_ptr<CCampaignState> CBonusSelection::getCampaign()
 {
 	return CSH->si->campState;
@@ -60,14 +61,9 @@ CBonusSelection::CBonusSelection()
 	: CWindowObject(BORDERED)
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-	static const std::string bgNames[] =
-	{
-		"E1_BG.BMP", "G2_BG.BMP", "E2_BG.BMP", "G1_BG.BMP", "G3_BG.BMP", "N1_BG.BMP",
-		"S1_BG.BMP", "BR_BG.BMP", "IS_BG.BMP", "KR_BG.BMP", "NI_BG.BMP", "TA_BG.BMP", "AR_BG.BMP", "HS_BG.BMP",
-		"BB_BG.BMP", "NB_BG.BMP", "EL_BG.BMP", "RN_BG.BMP", "UA_BG.BMP", "SP_BG.BMP"
-	};
-	loadPositionsOfGraphics();
-	setBackground(bgNames[getCampaign()->camp->header.mapVersion]);
+
+	std::string bgName = getCampaign()->camp->header.campaignRegions.campPrefix + "_BG.BMP";
+	setBackground(bgName);
 
 	panelBackground = std::make_shared<CPicture>("CAMPBRF.BMP", 456, 6);
 
@@ -110,35 +106,9 @@ CBonusSelection::CBonusSelection()
 	for(int g = 0; g < getCampaign()->camp->scenarios.size(); ++g)
 	{
 		if(getCampaign()->camp->conquerable(g))
-			regions.push_back(std::make_shared<CRegion>(g, true, true, campDescriptions[getCampaign()->camp->header.mapVersion]));
+			regions.push_back(std::make_shared<CRegion>(g, true, true, getCampaign()->camp->header.campaignRegions));
 		else if(getCampaign()->camp->scenarios[g].conquered) //display as striped
-			regions.push_back(std::make_shared<CRegion>(g, false, false, campDescriptions[getCampaign()->camp->header.mapVersion]));
-	}
-}
-
-void CBonusSelection::loadPositionsOfGraphics()
-{
-	const JsonNode config(ResourceID("config/campaign_regions.json"));
-
-	for(const JsonNode & campaign : config["campaign_regions"].Vector())
-	{
-		SCampPositions sc;
-
-		sc.campPrefix = campaign["prefix"].String();
-		sc.colorSuffixLength = static_cast<int>(campaign["color_suffix_length"].Float());
-
-		for(const JsonNode & desc : campaign["desc"].Vector())
-		{
-			SCampPositions::SRegionDesc rd;
-
-			rd.infix = desc["infix"].String();
-			rd.xpos = static_cast<int>(desc["x"].Float());
-			rd.ypos = static_cast<int>(desc["y"].Float());
-			sc.regions.push_back(rd);
-		}
-
-		campDescriptions.push_back(sc);
-
+			regions.push_back(std::make_shared<CRegion>(g, false, false, getCampaign()->camp->header.campaignRegions));
 	}
 }
 
@@ -196,7 +166,11 @@ void CBonusSelection::createBonusesIcons()
 			}
 			assert(faction != -1);
 
-			BuildingID buildID = CBuildingHandler::campToERMU(bonDescs[i].info1, faction, std::set<BuildingID>());
+			BuildingID buildID;
+			if(getCampaign()->camp->header.version == CampaignVersion::VCMI)
+				buildID = BuildingID(bonDescs[i].info1);
+			else
+				buildID = CBuildingHandler::campToERMU(bonDescs[i].info1, faction, std::set<BuildingID>());
 			picName = graphics->ERMUtoPicture[faction][buildID];
 			picNumber = -1;
 
@@ -470,7 +444,7 @@ void CBonusSelection::decreaseDifficulty()
 		CSH->setDifficulty(CSH->si->difficulty - 1);
 }
 
-CBonusSelection::CRegion::CRegion(int id, bool accessible, bool selectable, const SCampPositions & campDsc)
+CBonusSelection::CRegion::CRegion(int id, bool accessible, bool selectable, const CampaignRegions & campDsc)
 	: CIntObject(LCLICK | RCLICK), idOfMapAndRegion(id), accessible(accessible), selectable(selectable)
 {
 	OBJ_CONSTRUCTION;
@@ -480,7 +454,7 @@ CBonusSelection::CRegion::CRegion(int id, bool accessible, bool selectable, cons
 		{"Re", "Bl", "Br", "Gr", "Or", "Vi", "Te", "Pi"}
 	};
 
-	const SCampPositions::SRegionDesc & desc = campDsc.regions[idOfMapAndRegion];
+	const CampaignRegions::RegionDescription & desc = campDsc.regions[idOfMapAndRegion];
 	pos.x += desc.xpos;
 	pos.y += desc.ypos;
 

+ 2 - 18
client/lobby/CBonusSelection.h

@@ -8,6 +8,7 @@
  *
  */
 #pragma once
+#include "../../lib/mapping/CCampaignHandler.h"
 #include "../windows/CWindowObject.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -32,21 +33,6 @@ public:
 	std::shared_ptr<CCampaignState> getCampaign();
 	CBonusSelection();
 
-	struct SCampPositions
-	{
-		std::string campPrefix;
-		int colorSuffixLength;
-
-		struct SRegionDesc
-		{
-			std::string infix;
-			int xpos, ypos;
-		};
-
-		std::vector<SRegionDesc> regions;
-
-	};
-
 	class CRegion
 		: public CIntObject
 	{
@@ -57,13 +43,12 @@ public:
 		bool accessible; // false if region should be striped
 		bool selectable; // true if region should be selectable
 	public:
-		CRegion(int id, bool accessible, bool selectable, const SCampPositions & campDsc);
+		CRegion(int id, bool accessible, bool selectable, const CampaignRegions & campDsc);
 		void updateState();
 		void clickLeft(tribool down, bool previousState) override;
 		void clickRight(tribool down, bool previousState) override;
 	};
 
-	void loadPositionsOfGraphics();
 	void createBonusesIcons();
 	void updateAfterStateChange();
 
@@ -84,7 +69,6 @@ public:
 	std::shared_ptr<CLabel> mapName;
 	std::shared_ptr<CLabel> labelMapDescription;
 	std::shared_ptr<CTextBox> mapDescription;
-	std::vector<SCampPositions> campDescriptions;
 	std::vector<std::shared_ptr<CRegion>> regions;
 	std::shared_ptr<CFlagBox> flagbox;
 

+ 9 - 0
client/lobby/CLobbyScreen.cpp

@@ -131,6 +131,15 @@ void CLobbyScreen::startScenario(bool allowOnlyAI)
 		CSH->sendStartGame(allowOnlyAI);
 		buttonStart->block(true);
 	}
+	catch(CModHandler::Incompatibility & e)
+	{
+		logGlobal->warn("Incompatibility exception during start scenario: %s", e.what());
+		
+		auto errorMsg = VLC->generaltexth->translate("vcmi.server.errors.modsIncompatibility") + '\n';
+		errorMsg += e.what();
+		
+		CInfoWindow::showInfoDialog(errorMsg, CInfoWindow::TCompsInfo(), PlayerColor(1));
+	}
 	catch(std::exception & e)
 	{
 		logGlobal->error("Exception during startScenario: %s", e.what());

+ 5 - 4
client/lobby/SelectionTab.cpp

@@ -100,8 +100,8 @@ bool mapSorter::operator()(const std::shared_ptr<CMapInfo> aaa, const std::share
 		switch(sortBy)
 		{
 		case _numOfMaps: //by number of maps in campaign
-			return CGI->generaltexth->getCampaignLength(aaa->campaignHeader->mapVersion) <
-				   CGI->generaltexth->getCampaignLength(bbb->campaignHeader->mapVersion);
+			return aaa->campaignHeader->numberOfScenarios <
+				   bbb->campaignHeader->numberOfScenarios;
 			break;
 		case _name: //by name
 			return boost::ilexicographical_compare(aaa->campaignHeader->name, bbb->campaignHeader->name);
@@ -595,7 +595,8 @@ void SelectionTab::parseCampaigns(const std::unordered_set<ResourceID> & files)
 		//allItems[i].date = std::asctime(std::localtime(&files[i].date));
 		info->fileURI = file.getName();
 		info->campaignInit();
-		allItems.push_back(info);
+		if(info->campaignHeader)
+			allItems.push_back(info);
 	}
 }
 
@@ -657,7 +658,7 @@ void SelectionTab::ListItem::updateItem(std::shared_ptr<CMapInfo> info, bool sel
 		iconLossCondition->disable();
 		labelNumberOfCampaignMaps->enable();
 		std::ostringstream ostr(std::ostringstream::out);
-		ostr << CGI->generaltexth->getCampaignLength(info->campaignHeader->mapVersion);
+		ostr << info->campaignHeader->numberOfScenarios;
 		labelNumberOfCampaignMaps->setText(ostr.str());
 		labelNumberOfCampaignMaps->setColor(color);
 	}

+ 2 - 2
client/mainmenu/CPrologEpilogVideo.cpp

@@ -29,8 +29,8 @@ CPrologEpilogVideo::CPrologEpilogVideo(CCampaignScenario::SScenarioPrologEpilog
 	pos = center(Rect(0, 0, 800, 600));
 	updateShadow();
 
-	CCS->videoh->open(CCampaignHandler::prologVideoName(spe.prologVideo));
-	CCS->musich->playMusic("Music/" + CCampaignHandler::prologMusicName(spe.prologMusic), true, true);
+	CCS->videoh->open(spe.prologVideo);
+	CCS->musich->playMusic("Music/" + spe.prologMusic, true, true);
 	// MPTODO: Custom campaign crashing on this?
 //	voiceSoundHandle = CCS->soundh->playSound(CCampaignHandler::prologVoiceName(spe.prologVideo));
 

+ 16 - 13
lib/CGameState.cpp

@@ -722,7 +722,7 @@ void CGameState::preInit(Services * services)
 	this->services = services;
 }
 
-void CGameState::init(const IMapService * mapService, StartInfo * si, bool allowSavingRandomMap)
+void CGameState::init(const CMapService * mapService, StartInfo * si, bool allowSavingRandomMap)
 {
 	preInitAuto();
 	logGlobal->info("\tUsing random seed: %d", si->seedToBeUsed);
@@ -851,7 +851,7 @@ void CGameState::preInitAuto()
 	}
 }
 
-void CGameState::initNewGame(const IMapService * mapService, bool allowSavingRandomMap)
+void CGameState::initNewGame(const CMapService * mapService, bool allowSavingRandomMap)
 {
 	if(scenarioOps->createRandomMap())
 	{
@@ -1266,7 +1266,7 @@ void CGameState::prepareCrossoverHeroes(std::vector<CGameState::CampaignHeroRepl
 
 	// TODO replace magic numbers with named constants
 	// TODO this logic (what should be kept) should be part of CScenarioTravel and be exposed via some clean set of methods
-	if(!(travelOptions.whatHeroKeeps & 1))
+	if(!travelOptions.whatHeroKeeps.experience)
 	{
 		//trimming experience
 		for(CGHeroInstance * cgh : crossoverHeroes)
@@ -1275,7 +1275,7 @@ void CGameState::prepareCrossoverHeroes(std::vector<CGameState::CampaignHeroRepl
 		}
 	}
 
-	if(!(travelOptions.whatHeroKeeps & 2))
+	if(!travelOptions.whatHeroKeeps.primarySkills)
 	{
 		//trimming prim skills
 		for(CGHeroInstance * cgh : crossoverHeroes)
@@ -1291,7 +1291,7 @@ void CGameState::prepareCrossoverHeroes(std::vector<CGameState::CampaignHeroRepl
 		}
 	}
 
-	if(!(travelOptions.whatHeroKeeps & 4))
+	if(!travelOptions.whatHeroKeeps.secondarySkills)
 	{
 		//trimming sec skills
 		for(CGHeroInstance * cgh : crossoverHeroes)
@@ -1301,7 +1301,7 @@ void CGameState::prepareCrossoverHeroes(std::vector<CGameState::CampaignHeroRepl
 		}
 	}
 
-	if(!(travelOptions.whatHeroKeeps & 8))
+	if(!travelOptions.whatHeroKeeps.spells)
 	{
 		for(CGHeroInstance * cgh : crossoverHeroes)
 		{
@@ -1309,7 +1309,7 @@ void CGameState::prepareCrossoverHeroes(std::vector<CGameState::CampaignHeroRepl
 		}
 	}
 
-	if(!(travelOptions.whatHeroKeeps & 16))
+	if(!travelOptions.whatHeroKeeps.artifacts)
 	{
 		//trimming artifacts
 		for(CGHeroInstance * hero : crossoverHeroes)
@@ -1329,9 +1329,7 @@ void CGameState::prepareCrossoverHeroes(std::vector<CGameState::CampaignHeroRepl
 				if(!art)
 					continue;
 
-				int id  = art->artType->getId();
-				assert( 8*18 > id );//number of arts that fits into h3m format
-				bool takeable = travelOptions.artifsKeptByHero[id / 8] & ( 1 << (id%8) );
+				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
@@ -1346,7 +1344,7 @@ void CGameState::prepareCrossoverHeroes(std::vector<CGameState::CampaignHeroRepl
 		auto shouldSlotBeErased = [&](const std::pair<SlotID, CStackInstance *> & j) -> bool
 		{
 			CreatureID::ECreatureID crid = j.second->getCreatureID().toEnum();
-			return !(travelOptions.monstersKeptByHero[crid / 8] & (1 << (crid % 8)));
+			return !travelOptions.monstersKeptByHero.count(crid);
 		};
 
 		auto stacksCopy = cgh->stacks; //copy of the map, so we can iterate iover it and remove stacks
@@ -1725,8 +1723,13 @@ void CGameState::initTowns()
 					if (owner->human && //human-owned
 						map->towns[g]->pos == pi.posOfMainTown)
 					{
-						map->towns[g]->builtBuildings.insert(
-							CBuildingHandler::campToERMU(chosenBonus->info1, map->towns[g]->subID, map->towns[g]->builtBuildings));
+						BuildingID buildingId;
+						if(scenarioOps->campState->camp->header.version == CampaignVersion::VCMI)
+							buildingId = BuildingID(chosenBonus->info1);
+						else
+							buildingId = CBuildingHandler::campToERMU(chosenBonus->info1, map->towns[g]->subID, map->towns[g]->builtBuildings);
+
+						map->towns[g]->builtBuildings.insert(buildingId);
 						break;
 					}
 				}

+ 3 - 3
lib/CGameState.h

@@ -57,7 +57,7 @@ class CQuest;
 class CCampaignScenario;
 struct EventCondition;
 class CScenarioTravel;
-class IMapService;
+class CMapService;
 
 
 template<typename T> class CApplier;
@@ -161,7 +161,7 @@ public:
 
 	void preInit(Services * services);
 
-	void init(const IMapService * mapService, StartInfo * si, bool allowSavingRandomMap = false);
+	void init(const CMapService * mapService, StartInfo * si, bool allowSavingRandomMap = false);
 	void updateOnLoad(StartInfo * si);
 
 	ConstTransitivePtr<StartInfo> scenarioOps, initialOpts; //second one is a copy of settings received from pregame (not randomized)
@@ -252,7 +252,7 @@ private:
 
 	// ----- initialization -----
 	void preInitAuto();
-	void initNewGame(const IMapService * mapService, bool allowSavingRandomMap);
+	void initNewGame(const CMapService * mapService, bool allowSavingRandomMap);
 	void initCampaign();
 	void checkMapChecksum();
 	void initGlobalBonuses();

+ 5 - 0
lib/CModHandler.cpp

@@ -967,6 +967,11 @@ std::vector<std::string> CModHandler::getActiveMods()
 	return activeMods;
 }
 
+const CModInfo & CModHandler::getModInfo(const TModID & modId) const
+{
+	return allMods.at(modId);
+}
+
 static JsonNode genDefaultFS()
 {
 	// default FS config for mods: directory "Content" that acts as H3 root directory

+ 4 - 2
lib/CModHandler.h

@@ -174,7 +174,7 @@ public:
 	const ContentTypeHandler & operator[] (const std::string & name) const;
 };
 
-typedef std::string TModID;
+using TModID = std::string;
 
 class DLL_LINKAGE CModInfo
 {
@@ -186,7 +186,7 @@ public:
 		PASSED
 	};
 	
-	struct Version
+	struct DLL_LINKAGE Version
 	{
 		int major = 0;
 		int minor = 0;
@@ -347,6 +347,8 @@ public:
 	/// returns list of all (active) mods
 	std::vector<std::string> getAllMods();
 	std::vector<std::string> getActiveMods();
+	
+	const CModInfo & getModInfo(const TModID & modId) const;
 
 	/// load content from all available mods
 	void load();

+ 10 - 1
lib/StartInfo.cpp

@@ -15,6 +15,7 @@
 #include "mapping/CMapInfo.h"
 #include "mapping/CCampaignHandler.h"
 #include "mapping/CMap.h"
+#include "mapping/CMapService.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -67,8 +68,16 @@ std::string StartInfo::getCampaignName() const
 
 void LobbyInfo::verifyStateBeforeStart(bool ignoreNoHuman) const
 {
-	if(!mi)
+	if(!mi || !mi->mapHeader)
 		throw std::domain_error("ExceptionMapMissing");
+	
+	auto missingMods = CMapService::verifyMapHeaderMods(*mi->mapHeader);
+	CModHandler::Incompatibility::ModList modList;
+	for(const auto & m : missingMods)
+		modList.push_back({m.first, m.second.toString()});
+	
+	if(!modList.empty())
+		throw CModHandler::Incompatibility(std::move(modList));
 
 	//there must be at least one human player before game can be started
 	std::map<PlayerColor, PlayerSettings>::const_iterator i;

+ 1 - 1
lib/filesystem/MinizipExtensions.cpp

@@ -67,7 +67,7 @@ inline long streamSeek(voidpf opaque, voidpf stream, ZPOS64_T offset, int origin
 	}
 	if(ret == -1)
 		logGlobal->error("Stream seek failed");
-	return ret;
+	return 0;
 }
 
 template<class Stream>

+ 1 - 0
lib/filesystem/ResourceID.cpp

@@ -159,6 +159,7 @@ EResType::Type EResTypeHelper::getTypeFromExtension(std::string extension)
 		{".ERT",   EResType::ERT},
 		{".ERS",   EResType::ERS},
 		{".VMAP",  EResType::MAP},
+		{".VCMP",  EResType::CAMPAIGN},
 		{".VERM",  EResType::ERM},
 		{".LUA",   EResType::LUA}
 	};

+ 7 - 1
lib/mapObjects/CGHeroInstance.cpp

@@ -1477,7 +1477,10 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler)
 			if(portrait >= 0)
 			{
 				if(portrait < legacyHeroes || portrait >= moddedStart)
-					handler.serializeId<si32, si32, HeroTypeID>("portrait", portrait, -1);
+				{
+					int tempPortrait = portrait - GameConstants::HERO_PORTRAIT_SHIFT;
+					handler.serializeId<si32, si32, HeroTypeID>("portrait", tempPortrait, -1);
+				}
 				else
 					handler.serializeInt("portrait", portrait, -1);
 			}
@@ -1487,7 +1490,10 @@ void CGHeroInstance::serializeCommonOptions(JsonSerializeFormat & handler)
 			const JsonNode & portraitNode = handler.getCurrent()["portrait"];
 
 			if(portraitNode.getType() == JsonNode::JsonType::DATA_STRING)
+			{
 				handler.serializeId<si32, si32, HeroTypeID>("portrait", portrait, -1);
+				portrait += GameConstants::HERO_PORTRAIT_SHIFT;
+			}
 			else
 				handler.serializeInt("portrait", portrait, -1);
 		}

+ 358 - 51
lib/mapping/CCampaignHandler.cpp

@@ -24,6 +24,7 @@
 #include "../mapObjects/CGHeroInstance.h"//for hero crossover
 #include "../CHeroHandler.h"
 #include "../Languages.h"
+#include "../StringConstants.h"
 #include "CMapService.h"
 #include "CMap.h"
 #include "CMapInfo.h"
@@ -35,27 +36,73 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+CampaignRegions::RegionDescription CampaignRegions::RegionDescription::fromJson(const JsonNode & node)
+{
+	CampaignRegions::RegionDescription rd;
+	rd.infix = node["infix"].String();
+	rd.xpos = static_cast<int>(node["x"].Float());
+	rd.ypos = static_cast<int>(node["y"].Float());
+	return rd;
+}
+
+CampaignRegions CampaignRegions::fromJson(const JsonNode & node)
+{
+	CampaignRegions cr;
+	cr.campPrefix = node["prefix"].String();
+	cr.colorSuffixLength = static_cast<int>(node["color_suffix_length"].Float());
+
+	for(const JsonNode & desc : node["desc"].Vector())
+		cr.regions.push_back(CampaignRegions::RegionDescription::fromJson(desc));
+	
+	return cr;
+}
+
+CampaignRegions CampaignRegions::getLegacy(int campId)
+{
+	static std::vector<CampaignRegions> campDescriptions;
+	if(campDescriptions.empty()) //read once
+	{
+		const JsonNode config(ResourceID("config/campaign_regions.json"));
+		for(const JsonNode & campaign : config["campaign_regions"].Vector())
+			campDescriptions.push_back(CampaignRegions::fromJson(campaign));
+	}
+	
+	return campDescriptions.at(campId);
+}
+
+
 bool CScenarioTravel::STravelBonus::isBonusForHero() const
 {
 	return type == SPELL || type == MONSTER || type == ARTIFACT || type == SPELL_SCROLL || type == PRIMARY_SKILL
 		|| type == SECONDARY_SKILL;
 }
 
+void CCampaignHeader::loadLegacyData(ui8 campId)
+{
+	campaignRegions = CampaignRegions::getLegacy(campId);
+	numberOfScenarios = VLC->generaltexth->getCampaignLength(campId);
+}
+
 CCampaignHeader CCampaignHandler::getHeader( const std::string & name)
 {
 	ResourceID resourceID(name, EResType::CAMPAIGN);
 	std::string modName = VLC->modh->findResourceOrigin(resourceID);
 	std::string language = VLC->modh->getModLanguage(modName);
 	std::string encoding = Languages::getLanguageOptions(language).encoding;
+	
 	auto fileStream = CResourceHandler::get(modName)->load(resourceID);
-
 	std::vector<ui8> cmpgn = getFile(std::move(fileStream), true)[0];
-
-	CMemoryStream stream(cmpgn.data(), cmpgn.size());
-	CBinaryReader reader(&stream);
-	CCampaignHeader ret = readHeaderFromMemory(reader, resourceID.getName(), modName, encoding);
-
-	return ret;
+	JsonNode jsonCampaign((const char*)cmpgn.data(), cmpgn.size());
+	if(jsonCampaign.isNull())
+	{
+		//legacy OH3 campaign (*.h3c)
+		CMemoryStream stream(cmpgn.data(), cmpgn.size());
+		CBinaryReader reader(&stream);
+		return readHeaderFromMemory(reader, resourceID.getName(), modName, encoding);
+	}
+	
+	//VCMI (*.vcmp)
+	return readHeaderFromJson(jsonCampaign, resourceID.getName(), modName, encoding);
 }
 
 std::unique_ptr<CCampaign> CCampaignHandler::getCampaign( const std::string & name )
@@ -64,39 +111,42 @@ std::unique_ptr<CCampaign> CCampaignHandler::getCampaign( const std::string & na
 	std::string modName = VLC->modh->findResourceOrigin(resourceID);
 	std::string language = VLC->modh->getModLanguage(modName);
 	std::string encoding = Languages::getLanguageOptions(language).encoding;
-	auto fileStream = CResourceHandler::get(modName)->load(resourceID);
-
+	
 	auto ret = std::make_unique<CCampaign>();
+	
+	auto fileStream = CResourceHandler::get(modName)->load(resourceID);
 
-	std::vector<std::vector<ui8>> file = getFile(std::move(fileStream), false);
-
-	CMemoryStream stream(file[0].data(), file[0].size());
-	CBinaryReader reader(&stream);
-	ret->header = readHeaderFromMemory(reader, resourceID.getName(), modName, encoding);
-
-	int howManyScenarios = static_cast<int>(VLC->generaltexth->getCampaignLength(ret->header.mapVersion));
-	for(int g=0; g<howManyScenarios; ++g)
+	std::vector<std::vector<ui8>> files = getFile(std::move(fileStream), false);
+	
+	JsonNode jsonCampaign((const char*)files[0].data(), files[0].size());
+	if(jsonCampaign.isNull())
 	{
-		CCampaignScenario sc = readScenarioFromMemory(reader, resourceID.getName(), modName, encoding, ret->header.version, ret->header.mapVersion);
-		ret->scenarios.push_back(sc);
+		CMemoryStream stream(files[0].data(), files[0].size());
+		CBinaryReader reader(&stream);
+		ret->header = readHeaderFromMemory(reader, resourceID.getName(), modName, encoding);
+		
+		for(int g = 0; g < ret->header.numberOfScenarios; ++g)
+			ret->scenarios.emplace_back(readScenarioFromMemory(reader, ret->header));
 	}
-
-	int scenarioID = 0;
-
+	else
+	{
+		ret->header = readHeaderFromJson(jsonCampaign, resourceID.getName(), modName, encoding);
+		for(auto & scenario : jsonCampaign["scenarios"].Vector())
+			ret->scenarios.emplace_back(readScenarioFromJson(scenario));
+	}
+	
 	//first entry is campaign header. start loop from 1
-	for (int g=1; g<file.size() && scenarioID<howManyScenarios; ++g)
+	for(int scenarioID = 0, g = 1; g < files.size() && scenarioID < ret->header.numberOfScenarios; ++g)
 	{
 		while(!ret->scenarios[scenarioID].isNotVoid()) //skip void scenarios
-		{
 			scenarioID++;
-		}
 
 		std::string scenarioName = resourceID.getName();
 		boost::to_lower(scenarioName);
 		scenarioName += ':' + std::to_string(g - 1);
 
 		//set map piece appropriately, convert vector to string
-		ret->mapPieces[scenarioID].assign(reinterpret_cast< const char* >(file[g].data()), file[g].size());
+		ret->mapPieces[scenarioID].assign(reinterpret_cast<const char*>(files[g].data()), files[g].size());
 		CMapService mapService;
 		auto hdr = mapService.loadMapHeader(
 			reinterpret_cast<const ui8 *>(ret->mapPieces[scenarioID].c_str()),
@@ -151,26 +201,273 @@ std::string CCampaignHandler::readLocalizedString(CBinaryReader & reader, std::s
 	return VLC->generaltexth->translate(stringID.get());
 }
 
+CCampaignHeader CCampaignHandler::readHeaderFromJson(JsonNode & reader, std::string filename, std::string modName, std::string encoding)
+{
+	CCampaignHeader ret;
+
+	ret.version = reader["version"].Integer();
+	if(ret.version < CampaignVersion::VCMI_MIN || ret.version > CampaignVersion::VCMI_MAX)
+	{
+		logGlobal->info("VCMP Loading: Unsupported campaign %s version %d", filename, ret.version);
+		return ret;
+	}
+	
+	ret.version = CampaignVersion::VCMI;
+	ret.campaignRegions = CampaignRegions::fromJson(reader["regions"]);
+	ret.numberOfScenarios = reader["scenarios"].Vector().size();
+	ret.name = reader["name"].String();
+	ret.description = reader["description"].String();
+	ret.difficultyChoosenByPlayer = reader["allowDifficultySelection"].Bool();
+	//skip ret.music because it's unused in vcmi
+	ret.filename = filename;
+	ret.modName = modName;
+	ret.encoding = encoding;
+	ret.valid = true;
+	return ret;
+}
+
+CCampaignScenario CCampaignHandler::readScenarioFromJson(JsonNode & reader)
+{
+	auto prologEpilogReader = [](JsonNode & identifier) -> CCampaignScenario::SScenarioPrologEpilog
+	{
+		CCampaignScenario::SScenarioPrologEpilog ret;
+		ret.hasPrologEpilog = !identifier.isNull();
+		if(ret.hasPrologEpilog)
+		{
+			ret.prologVideo = identifier["video"].String();
+			ret.prologMusic = identifier["music"].String();
+			ret.prologText = identifier["text"].String();
+		}
+		return ret;
+	};
+
+	CCampaignScenario ret;
+	ret.conquered = false;
+	ret.mapName = reader["map"].String();
+	for(auto & g : reader["preconditions"].Vector())
+		ret.preconditionRegions.insert(g.Integer());
+
+	ret.regionColor = reader["color"].Integer();
+	ret.difficulty = reader["difficulty"].Integer();
+	ret.regionText = reader["regionText"].String();
+	ret.prolog = prologEpilogReader(reader["prolog"]);
+	ret.epilog = prologEpilogReader(reader["epilog"]);
+
+	ret.travelOptions = readScenarioTravelFromJson(reader);
+
+	return ret;
+}
+
+CScenarioTravel CCampaignHandler::readScenarioTravelFromJson(JsonNode & reader)
+{
+	CScenarioTravel ret;
+
+	std::map<std::string, ui8> startOptionsMap = {
+		{"none", 0},
+		{"bonus", 1},
+		{"crossover", 2},
+		{"hero", 3}
+	};
+	
+	std::map<std::string, CScenarioTravel::STravelBonus::EBonusType> bonusTypeMap = {
+		{"spell", CScenarioTravel::STravelBonus::EBonusType::SPELL},
+		{"creature", CScenarioTravel::STravelBonus::EBonusType::MONSTER},
+		{"building", CScenarioTravel::STravelBonus::EBonusType::BUILDING},
+		{"artifact", CScenarioTravel::STravelBonus::EBonusType::ARTIFACT},
+		{"scroll", CScenarioTravel::STravelBonus::EBonusType::SPELL_SCROLL},
+		{"primarySkill", CScenarioTravel::STravelBonus::EBonusType::PRIMARY_SKILL},
+		{"secondarySkill", CScenarioTravel::STravelBonus::EBonusType::SECONDARY_SKILL},
+		{"resource", CScenarioTravel::STravelBonus::EBonusType::RESOURCE},
+		//{"prevHero", CScenarioTravel::STravelBonus::EBonusType::HEROES_FROM_PREVIOUS_SCENARIO},
+		//{"hero", CScenarioTravel::STravelBonus::EBonusType::HERO},
+	};
+	
+	std::map<std::string, ui32> primarySkillsMap = {
+		{"attack", 0},
+		{"defence", 8},
+		{"spellpower", 16},
+		{"knowledge", 24},
+	};
+	
+	std::map<std::string, ui16> heroSpecialMap = {
+		{"strongest", 0xFFFD},
+		{"generated", 0xFFFE},
+		{"random", 0xFFFF}
+	};
+	
+	std::map<std::string, ui8> resourceTypeMap = {
+		//FD - wood+ore
+		//FE - mercury+sulfur+crystal+gem
+		{"wood", 0},
+		{"mercury", 1},
+		{"ore", 2},
+		{"sulfur", 3},
+		{"crystal", 4},
+		{"gems", 5},
+		{"gold", 6},
+		{"common", 0xFD},
+		{"rare", 0xFE}
+	};
+	
+	for(auto & k : reader["heroKeeps"].Vector())
+	{
+		if(k.String() == "experience") ret.whatHeroKeeps.experience = true;
+		if(k.String() == "primarySkills") ret.whatHeroKeeps.primarySkills = true;
+		if(k.String() == "secondarySkills") ret.whatHeroKeeps.secondarySkills = true;
+		if(k.String() == "spells") ret.whatHeroKeeps.spells = true;
+		if(k.String() == "artifacts") ret.whatHeroKeeps.artifacts = true;
+	}
+	
+	for(auto & k : reader["keepCreatures"].Vector())
+	{
+		if(auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "creature", k.String()))
+			ret.monstersKeptByHero.insert(CreatureID(identifier.value()));
+		else
+			logGlobal->warn("VCMP Loading: keepCreatures contains unresolved identifier %s", k.String());
+	}
+	for(auto & k : reader["keepArtifacts"].Vector())
+	{
+		if(auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "artifact", k.String()))
+			ret.artifactsKeptByHero.insert(ArtifactID(identifier.value()));
+		else
+			logGlobal->warn("VCMP Loading: keepArtifacts contains unresolved identifier %s", k.String());
+	}
+
+	ret.startOptions = startOptionsMap[reader["startOptions"].String()];
+	switch(ret.startOptions)
+	{
+	case 0:
+		//no bonuses. Seems to be OK
+		break;
+	case 1: //reading of bonuses player can choose
+		{
+			ret.playerColor = reader["playerColor"].Integer();
+			for(auto & bjson : reader["bonuses"].Vector())
+			{
+				CScenarioTravel::STravelBonus bonus;
+				bonus.type = bonusTypeMap[bjson["what"].String()];
+				switch (bonus.type)
+				{
+					case CScenarioTravel::STravelBonus::EBonusType::RESOURCE:
+						bonus.info1 = resourceTypeMap[bjson["type"].String()];
+						bonus.info2 = bjson["amount"].Integer();
+						break;
+						
+					case CScenarioTravel::STravelBonus::EBonusType::BUILDING:
+						bonus.info1 = vstd::find_pos(EBuildingType::names, bjson["type"].String());
+						if(bonus.info1 == -1)
+							logGlobal->warn("VCMP Loading: unresolved building identifier %s", bjson["type"].String());
+						break;
+						
+					default:
+						if(int heroId = heroSpecialMap[bjson["hero"].String()])
+							bonus.info1 = heroId;
+						else
+							if(auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "hero", bjson["hero"].String()))
+								bonus.info1 = identifier.value();
+							else
+								logGlobal->warn("VCMP Loading: unresolved hero identifier %s", bjson["hero"].String());
+	
+						bonus.info3 = bjson["amount"].Integer();
+						
+						switch(bonus.type)
+						{
+							case CScenarioTravel::STravelBonus::EBonusType::SPELL:
+							case CScenarioTravel::STravelBonus::EBonusType::MONSTER:
+							case CScenarioTravel::STravelBonus::EBonusType::SECONDARY_SKILL:
+							case CScenarioTravel::STravelBonus::EBonusType::ARTIFACT:
+								if(auto identifier  = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), bjson["what"].String(), bjson["type"].String()))
+									bonus.info2 = identifier.value();
+								else
+									logGlobal->warn("VCMP Loading: unresolved %s identifier %s", bjson["what"].String(), bjson["type"].String());
+								break;
+								
+							case CScenarioTravel::STravelBonus::EBonusType::SPELL_SCROLL:
+								if(auto Identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "spell", bjson["type"].String()))
+									bonus.info2 = Identifier.value();
+								else
+									logGlobal->warn("VCMP Loading: unresolved spell scroll identifier %s", bjson["type"].String());
+								break;
+								
+							case CScenarioTravel::STravelBonus::EBonusType::PRIMARY_SKILL:
+								for(auto & ps : primarySkillsMap)
+									bonus.info2 |= bjson[ps.first].Integer() << ps.second;
+								break;
+								
+							default:
+								bonus.info2 = bjson["type"].Integer();
+						}
+						break;
+				}
+				ret.bonusesToChoose.push_back(bonus);
+			}
+			break;
+		}
+	case 2: //reading of players (colors / scenarios ?) player can choose
+		{
+			for(auto & bjson : reader["bonuses"].Vector())
+			{
+				CScenarioTravel::STravelBonus bonus;
+				bonus.type = CScenarioTravel::STravelBonus::HEROES_FROM_PREVIOUS_SCENARIO;
+				bonus.info1 = bjson["playerColor"].Integer(); //player color
+				bonus.info2 = bjson["scenario"].Integer(); //from what scenario
+				ret.bonusesToChoose.push_back(bonus);
+			}
+			break;
+		}
+	case 3: //heroes player can choose between
+		{
+			for(auto & bjson : reader["bonuses"].Vector())
+			{
+				CScenarioTravel::STravelBonus bonus;
+				bonus.type = CScenarioTravel::STravelBonus::HERO;
+				bonus.info1 = bjson["playerColor"].Integer(); //player color
+				
+				if(int heroId = heroSpecialMap[bjson["hero"].String()])
+					bonus.info2 = heroId;
+				else
+					if (auto identifier = VLC->modh->identifiers.getIdentifier(CModHandler::scopeMap(), "hero", bjson["hero"].String()))
+						bonus.info2 = identifier.value();
+					else
+						logGlobal->warn("VCMP Loading: unresolved hero identifier %s", bjson["hero"].String());
+			
+				ret.bonusesToChoose.push_back(bonus);
+			}
+			break;
+		}
+	default:
+		{
+			logGlobal->warn("VCMP Loading: Unsupported start options value");
+			break;
+		}
+	}
+
+	return ret;
+}
+
+
 CCampaignHeader CCampaignHandler::readHeaderFromMemory( CBinaryReader & reader, std::string filename, std::string modName, std::string encoding )
 {
 	CCampaignHeader ret;
 
 	ret.version = reader.readUInt32();
-	ret.mapVersion = reader.readUInt8() - 1;//change range of it from [1, 20] to [0, 19]
+	ui8 campId = reader.readUInt8() - 1;//change range of it from [1, 20] to [0, 19]
+	ret.loadLegacyData(campId);
 	ret.name = readLocalizedString(reader, filename, modName, encoding, "name");
 	ret.description = readLocalizedString(reader, filename, modName, encoding, "description");
 	if (ret.version > CampaignVersion::RoE)
 		ret.difficultyChoosenByPlayer = reader.readInt8();
 	else
-		ret.difficultyChoosenByPlayer = 0;
-	ret.music = reader.readInt8();
+		ret.difficultyChoosenByPlayer = false;
+	reader.readInt8(); //music - skip as unused
 	ret.filename = filename;
 	ret.modName = modName;
 	ret.encoding = encoding;
+	ret.valid = true;
 	return ret;
 }
 
-CCampaignScenario CCampaignHandler::readScenarioFromMemory( CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, int version, int mapVersion )
+CCampaignScenario CCampaignHandler::readScenarioFromMemory( CBinaryReader & reader, const CCampaignHeader & header)
 {
 	auto prologEpilogReader = [&](const std::string & identifier) -> CCampaignScenario::SScenarioPrologEpilog
 	{
@@ -178,9 +475,9 @@ CCampaignScenario CCampaignHandler::readScenarioFromMemory( CBinaryReader & read
 		ret.hasPrologEpilog = reader.readUInt8();
 		if(ret.hasPrologEpilog)
 		{
-			ret.prologVideo = reader.readUInt8();
-			ret.prologMusic = reader.readUInt8();
-			ret.prologText = readLocalizedString(reader, filename, modName, encoding, identifier);
+			ret.prologVideo = CCampaignHandler::prologVideoName(reader.readUInt8());
+			ret.prologMusic = CCampaignHandler::prologMusicName(reader.readUInt8());
+			ret.prologText = readLocalizedString(reader, header.filename, header.modName, header.encoding, identifier);
 		}
 		return ret;
 	};
@@ -188,8 +485,8 @@ CCampaignScenario CCampaignHandler::readScenarioFromMemory( CBinaryReader & read
 	CCampaignScenario ret;
 	ret.conquered = false;
 	ret.mapName = reader.readBaseString();
-	ret.packedMapSize = reader.readUInt32();
-	if(mapVersion == 18)//unholy alliance
+	reader.readUInt32(); //packedMapSize - not used
+	if(header.numberOfScenarios > 8) //unholy alliance
 	{
 		ret.loadPreconditionRegions(reader.readUInt16());
 	}
@@ -199,11 +496,11 @@ CCampaignScenario CCampaignHandler::readScenarioFromMemory( CBinaryReader & read
 	}
 	ret.regionColor = reader.readUInt8();
 	ret.difficulty = reader.readUInt8();
-	ret.regionText = readLocalizedString(reader, filename, modName, encoding, ret.mapName + ".region");
+	ret.regionText = readLocalizedString(reader, header.filename, header.modName, header.encoding, ret.mapName + ".region");
 	ret.prolog = prologEpilogReader(ret.mapName + ".prolog");
 	ret.epilog = prologEpilogReader(ret.mapName + ".epilog");
 
-	ret.travelOptions = readScenarioTravelFromMemory(reader, version);
+	ret.travelOptions = readScenarioTravelFromMemory(reader, header.version);
 
 	return ret;
 }
@@ -221,18 +518,27 @@ CScenarioTravel CCampaignHandler::readScenarioTravelFromMemory(CBinaryReader & r
 {
 	CScenarioTravel ret;
 
-	ret.whatHeroKeeps = reader.readUInt8();
-	reader.getStream()->read(ret.monstersKeptByHero.data(), ret.monstersKeptByHero.size());
-
-	if (version < CampaignVersion::SoD)
+	ui8 whatHeroKeeps = reader.readUInt8();
+	ret.whatHeroKeeps.experience = whatHeroKeeps & 1;
+	ret.whatHeroKeeps.primarySkills = whatHeroKeeps & 2;
+	ret.whatHeroKeeps.secondarySkills = whatHeroKeeps & 4;
+	ret.whatHeroKeeps.spells = whatHeroKeeps & 8;
+	ret.whatHeroKeeps.artifacts = whatHeroKeeps & 16;
+	
+	//TODO: replace with template lambda form C++20 and make typed containers
+	auto bitMaskToId = [&reader](std::set<int> & container, int size)
 	{
-		ret.artifsKeptByHero.fill(0);
-		reader.getStream()->read(ret.artifsKeptByHero.data(), ret.artifsKeptByHero.size() - 1);
-	}
-	else
-	{
-		reader.getStream()->read(ret.artifsKeptByHero.data(), ret.artifsKeptByHero.size());
-	}
+		for(int iId = 0, byte = 0; iId < size * 8; ++iId)
+		{
+			if(iId % 8 == 0)
+				byte = reader.readUInt8();
+			if(byte & (1 << iId % 8))
+				container.insert(iId);
+		}
+	};
+	
+	bitMaskToId(ret.monstersKeptByHero, 19);
+	bitMaskToId(ret.artifactsKeptByHero, version < CampaignVersion::SoD ? 17 : 18);
 
 	ret.startOptions = reader.readUInt8();
 
@@ -490,12 +796,13 @@ CMap * CCampaignState::getMap(int scenarioId) const
 	// FIXME: there is certainly better way to handle maps inside campaigns
 	if(scenarioId == -1)
 		scenarioId = currentMap.value();
+	
+	CMapService mapService;
 	std::string scenarioName = camp->header.filename.substr(0, camp->header.filename.find('.'));
 	boost::to_lower(scenarioName);
 	scenarioName += ':' + std::to_string(scenarioId);
 	std::string & mapContent = camp->mapPieces.find(scenarioId)->second;
 	const auto * buffer = reinterpret_cast<const ui8 *>(mapContent.data());
-	CMapService mapService;
 	return mapService.loadMap(buffer, static_cast<int>(mapContent.size()), scenarioName, camp->header.modName, camp->header.encoding).release();
 }
 
@@ -503,13 +810,13 @@ std::unique_ptr<CMapHeader> CCampaignState::getHeader(int scenarioId) const
 {
 	if(scenarioId == -1)
 		scenarioId = currentMap.value();
-
+	
+	CMapService mapService;
 	std::string scenarioName = camp->header.filename.substr(0, camp->header.filename.find('.'));
 	boost::to_lower(scenarioName);
 	scenarioName += ':' + std::to_string(scenarioId);
 	std::string & mapContent = camp->mapPieces.find(scenarioId)->second;
 	const auto * buffer = reinterpret_cast<const ui8 *>(mapContent.data());
-	CMapService mapService;
 	return mapService.loadMapHeader(buffer, static_cast<int>(mapContent.size()), scenarioName, camp->header.modName, camp->header.encoding);
 }
 

+ 82 - 18
lib/mapping/CCampaignHandler.h

@@ -29,43 +29,105 @@ namespace CampaignVersion
 		RoE = 4,
 		AB = 5,
 		SoD = 6,
-		WoG = 6
+		WoG = 6,
+		VCMI = 1
 	};
+
+	const int VCMI_MIN = 1;
+	const int VCMI_MAX = 1;
 }
 
+struct DLL_LINKAGE CampaignRegions
+{
+	std::string campPrefix;
+	int colorSuffixLength;
+
+	struct DLL_LINKAGE RegionDescription
+	{
+		std::string infix;
+		int xpos, ypos;
+		
+		template <typename Handler> void serialize(Handler &h, const int formatVersion)
+		{
+			h & infix;
+			h & xpos;
+			h & ypos;
+		}
+		
+		static CampaignRegions::RegionDescription fromJson(const JsonNode & node);
+	};
+
+	std::vector<RegionDescription> regions;
+	
+	template <typename Handler> void serialize(Handler &h, const int formatVersion)
+	{
+		h & campPrefix;
+		h & colorSuffixLength;
+		h & regions;
+	}
+	
+	static CampaignRegions fromJson(const JsonNode & node);
+	static CampaignRegions getLegacy(int campId);
+};
+
 class DLL_LINKAGE CCampaignHeader
 {
 public:
 	si32 version = 0; //4 - RoE, 5 - AB, 6 - SoD and WoG
-	ui8 mapVersion = 0; //CampText.txt's format
+	CampaignRegions campaignRegions;
+	int numberOfScenarios = 0;
 	std::string name, description;
-	ui8 difficultyChoosenByPlayer = 0;
-	ui8 music = 0; //CmpMusic.txt, start from 0
+	bool difficultyChoosenByPlayer = false;
+	bool valid = false;
 
 	std::string filename;
 	std::string modName;
 	std::string encoding;
+	
+	void loadLegacyData(ui8 campId);
 
 	template <typename Handler> void serialize(Handler &h, const int formatVersion)
 	{
 		h & version;
-		h & mapVersion;
+		h & campaignRegions;
+		h & numberOfScenarios;
 		h & name;
 		h & description;
 		h & difficultyChoosenByPlayer;
-		h & music;
 		h & filename;
 		h & modName;
 		h & encoding;
+		h & valid;
 	}
 };
 
 class DLL_LINKAGE CScenarioTravel
 {
 public:
-	ui8 whatHeroKeeps = 0; //bitfield [0] - experience, [1] - prim skills, [2] - sec skills, [3] - spells, [4] - artifacts
-	std::array<ui8, 19> monstersKeptByHero;
-	std::array<ui8, 18> artifsKeptByHero;
+	
+	struct DLL_LINKAGE WhatHeroKeeps
+	{
+		bool experience = false;
+		bool primarySkills = false;
+		bool secondarySkills = false;
+		bool spells = false;
+		bool artifacts = false;
+		
+		template <typename Handler> void serialize(Handler &h, const int formatVersion)
+		{
+			h & experience;
+			h & primarySkills;
+			h & secondarySkills;
+			h & spells;
+			h & artifacts;
+		}
+	};
+	
+	WhatHeroKeeps whatHeroKeeps;
+	
+	//TODO: use typed containers
+	std::set<int> monstersKeptByHero;
+	std::set<int> artifactsKeptByHero;
 
 	ui8 startOptions = 0; //1 - start bonus, 2 - traveling hero, 3 - hero options
 
@@ -95,7 +157,7 @@ public:
 	{
 		h & whatHeroKeeps;
 		h & monstersKeptByHero;
-		h & artifsKeptByHero;
+		h & artifactsKeptByHero;
 		h & startOptions;
 		h & playerColor;
 		h & bonusesToChoose;
@@ -109,8 +171,8 @@ public:
 	struct DLL_LINKAGE SScenarioPrologEpilog
 	{
 		bool hasPrologEpilog = false;
-		ui8 prologVideo = 0; // from CmpMovie.txt
-		ui8 prologMusic = 0; // from CmpMusic.txt
+		std::string prologVideo; // from CmpMovie.txt
+		std::string prologMusic; // from CmpMusic.txt
 		std::string prologText;
 
 		template <typename Handler> void serialize(Handler &h, const int formatVersion)
@@ -124,7 +186,6 @@ public:
 
 	std::string mapName; //*.h3m
 	std::string scenarioName; //from header. human-readble
-	ui32 packedMapSize = 0; //generally not used
 	std::set<ui8> preconditionRegions; //what we need to conquer to conquer this one (stored as bitfield in h3c)
 	ui8 regionColor = 0;
 	ui8 difficulty = 0;
@@ -148,7 +209,6 @@ public:
 	{
 		h & mapName;
 		h & scenarioName;
-		h & packedMapSize;
 		h & preconditionRegions;
 		h & regionColor;
 		h & difficulty;
@@ -218,22 +278,26 @@ public:
 
 class DLL_LINKAGE CCampaignHandler
 {
-	std::vector<size_t> scenariosCountPerCampaign;
-
 	static std::string readLocalizedString(CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, std::string identifier);
+	
+	//parsers for VCMI campaigns (*.vcmp)
+	static CCampaignHeader readHeaderFromJson(JsonNode & reader, std::string filename, std::string modName, std::string encoding);
+	static CCampaignScenario readScenarioFromJson(JsonNode & reader);
+	static CScenarioTravel readScenarioTravelFromJson(JsonNode & reader);
 
+	//parsers for original H3C campaigns
 	static CCampaignHeader readHeaderFromMemory(CBinaryReader & reader, std::string filename, std::string modName, std::string encoding);
-	static CCampaignScenario readScenarioFromMemory(CBinaryReader & reader, std::string filename, std::string modName, std::string encoding, int version, int mapVersion );
+	static CCampaignScenario readScenarioFromMemory(CBinaryReader & reader, const CCampaignHeader & header);
 	static CScenarioTravel readScenarioTravelFromMemory(CBinaryReader & reader, int version);
 	/// returns h3c split in parts. 0 = h3c header, 1-end - maps (binary h3m)
 	/// headerOnly - only header will be decompressed, returned vector wont have any maps
 	static std::vector<std::vector<ui8>> getFile(std::unique_ptr<CInputStream> file, bool headerOnly);
 
-public:
 	static std::string prologVideoName(ui8 index);
 	static std::string prologMusicName(ui8 index);
 	static std::string prologVoiceName(ui8 index);
 
+public:
 	static CCampaignHeader getHeader( const std::string & name); //name - name of appropriate file
 
 	static std::unique_ptr<CCampaign> getCampaign(const std::string & name); //name - name of appropriate file

+ 9 - 0
lib/mapping/CMap.h

@@ -18,6 +18,7 @@
 #include "../int3.h"
 #include "../GameConstants.h"
 #include "../LogicalExpression.h"
+#include "../CModHandler.h"
 #include "CMapDefines.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -262,6 +263,10 @@ enum class EMapFormat: uint8_t
 	VCMI  = 0xF0
 };
 
+// Inherit from container to enable forward declaration
+class ModCompatibilityInfo: public std::map<TModID, CModInfo::Version>
+{};
+
 /// The map header holds information about loss/victory condition,map format, version, players, height, width,...
 class DLL_LINKAGE CMapHeader
 {
@@ -282,6 +287,8 @@ public:
 	ui8 levels() const;
 
 	EMapFormat version; /// The default value is EMapFormat::SOD.
+	ModCompatibilityInfo mods; /// set of mods required to play a map
+	
 	si32 height; /// The default value is 72.
 	si32 width; /// The default value is 72.
 	bool twoLevel; /// The default value is true.
@@ -310,6 +317,8 @@ public:
 	void serialize(Handler & h, const int Version)
 	{
 		h & version;
+		if(Version >= 821)
+			h & mods;
 		h & name;
 		h & description;
 		h & width;

+ 2 - 0
lib/mapping/CMapInfo.cpp

@@ -65,6 +65,8 @@ void CMapInfo::saveInit(const ResourceID & file)
 void CMapInfo::campaignInit()
 {
 	campaignHeader = std::make_unique<CCampaignHeader>(CCampaignHandler::getHeader(fileURI));
+	if(!campaignHeader->valid)
+		campaignHeader.reset();
 }
 
 void CMapInfo::countPlayers()

+ 18 - 0
lib/mapping/CMapService.cpp

@@ -86,6 +86,24 @@ void CMapService::saveMap(const std::unique_ptr<CMap> & map, boost::filesystem::
 	}
 }
 
+ModCompatibilityInfo CMapService::verifyMapHeaderMods(const CMapHeader & map)
+{
+	ModCompatibilityInfo modCompatibilityInfo;
+	const auto & activeMods = VLC->modh->getActiveMods();
+	for(const auto & mapMod : map.mods)
+	{
+		if(vstd::contains(activeMods, mapMod.first))
+		{
+			const auto & modInfo = VLC->modh->getModInfo(mapMod.first);
+			if(modInfo.version.compatible(mapMod.second))
+				continue;
+		}
+		
+		modCompatibilityInfo[mapMod.first] = mapMod.second;
+	}	
+	return modCompatibilityInfo;
+}
+
 std::unique_ptr<CInputStream> CMapService::getStreamFromFS(const ResourceID & name)
 {
 	return CResourceHandler::get()->load(name);

+ 29 - 27
lib/mapping/CMapService.h

@@ -21,31 +21,33 @@ class CInputStream;
 class IMapLoader;
 class IMapPatcher;
 
+class ModCompatibilityInfo;
+
 /**
- * The map service provides loading of VCMI/H3 map files. It can
- * be extended to save maps later as well.
+ * The map service provides loading and saving of VCMI/H3 map files.
  */
-class DLL_LINKAGE IMapService
+class DLL_LINKAGE CMapService
 {
 public:
-	IMapService() = default;
-	virtual ~IMapService() = default;
+	CMapService() = default;
+	virtual ~CMapService() = default;
+
 	/**
 	 * Loads the VCMI/H3 map file specified by the name.
 	 *
 	 * @param name the name of the map
 	 * @return a unique ptr to the loaded map class
 	 */
-	virtual std::unique_ptr<CMap> loadMap(const ResourceID & name) const = 0;
-
+	std::unique_ptr<CMap> loadMap(const ResourceID & name) const;
+	
 	/**
 	 * Loads the VCMI/H3 map header specified by the name.
 	 *
 	 * @param name the name of the map
 	 * @return a unique ptr to the loaded map header class
 	 */
-	virtual std::unique_ptr<CMapHeader> loadMapHeader(const ResourceID & name) const = 0;
-
+	std::unique_ptr<CMapHeader> loadMapHeader(const ResourceID & name) const;
+	
 	/**
 	 * Loads the VCMI/H3 map file from a buffer. This method is temporarily
 	 * in use to ease the transition to use the new map service.
@@ -58,8 +60,8 @@ public:
 	 * @param name indicates name of file that will be used during map header patching
 	 * @return a unique ptr to the loaded map class
 	 */
-	virtual std::unique_ptr<CMap> loadMap(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const = 0;
-
+	std::unique_ptr<CMap> loadMap(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const;
+	
 	/**
 	 * Loads the VCMI/H3 map header from a buffer. This method is temporarily
 	 * in use to ease the transition to use the new map service.
@@ -72,22 +74,22 @@ public:
 	 * @param name indicates name of file that will be used during map header patching
 	 * @return a unique ptr to the loaded map class
 	 */
-	virtual std::unique_ptr<CMapHeader> loadMapHeader(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const = 0;
-
-	virtual void saveMap(const std::unique_ptr<CMap> & map, boost::filesystem::path fullPath) const = 0;
-};
-
-class DLL_LINKAGE CMapService : public IMapService
-{
-public:
-	CMapService() = default;
-	virtual ~CMapService() = default;
-
-	std::unique_ptr<CMap> loadMap(const ResourceID & name) const override;
-	std::unique_ptr<CMapHeader> loadMapHeader(const ResourceID & name) const override;
-	std::unique_ptr<CMap> loadMap(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const override;
-	std::unique_ptr<CMapHeader> loadMapHeader(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const override;
-	void saveMap(const std::unique_ptr<CMap> & map, boost::filesystem::path fullPath) const override;
+	std::unique_ptr<CMapHeader> loadMapHeader(const ui8 * buffer, int size, const std::string & name, const std::string & modName, const std::string & encoding) const;
+	
+	/**
+	 * Tests if mods used in the map are currently loaded
+	 * @param map const reference to map header
+	 * @return data structure representing missing or incompatible mods (those which are needed from map but not loaded)
+	 */
+	static ModCompatibilityInfo verifyMapHeaderMods(const CMapHeader & map);
+	
+	/**
+	 * Saves map into VCMI format with name specified
+	 * @param map to save
+	 * @param fullPath full path to file to write, including extension
+	 */
+	void saveMap(const std::unique_ptr<CMap> & map, boost::filesystem::path fullPath) const;
+	
 private:
 	/**
 	 * Gets a map input stream object specified by a map name.

+ 4 - 0
lib/mapping/MapFormatH3M.cpp

@@ -147,6 +147,10 @@ void CMapLoaderH3M::readHeader()
 		features = MapFormatFeaturesH3M::find(mapHeader->version, 0);
 		reader->setFormatLevel(mapHeader->version, 0);
 	}
+	
+	// include basic mod
+	if(mapHeader->version == EMapFormat::WOG)
+		mapHeader->mods["wake-of-gods"];
 
 	// Read map name, description, dimensions,...
 	mapHeader->areAnyPlayers = reader->readBool();

+ 18 - 1
lib/mapping/MapFormatJson.cpp

@@ -339,7 +339,7 @@ namespace TerrainDetail
 
 ///CMapFormatJson
 const int CMapFormatJson::VERSION_MAJOR = 1;
-const int CMapFormatJson::VERSION_MINOR = 0;
+const int CMapFormatJson::VERSION_MINOR = 1;
 
 const std::string CMapFormatJson::HEADER_FILE_NAME = "header.json";
 const std::string CMapFormatJson::OBJECTS_FILE_NAME = "objects.json";
@@ -947,6 +947,13 @@ void CMapLoaderJson::readHeader(const bool complete)
 	JsonDeserializer handler(mapObjectResolver.get(), header);
 
 	mapHeader->version = EMapFormat::VCMI;//todo: new version field
+	
+	//loading mods
+	if(!header["mods"].isNull())
+	{
+		for(auto & mod : header["mods"].Vector())
+			mapHeader->mods[mod["name"].String()] = CModInfo::Version::fromString(mod["version"].String());
+	}
 
 	//todo: multilevel map load support
 	{
@@ -1279,6 +1286,16 @@ void CMapSaverJson::writeHeader()
 
 	header["versionMajor"].Float() = VERSION_MAJOR;
 	header["versionMinor"].Float() = VERSION_MINOR;
+	
+	//write mods
+	JsonNode & mods = header["mods"];
+	for(const auto & mod : mapHeader->mods)
+	{
+		JsonNode modWriter;
+		modWriter["name"].String() = mod.first;
+		modWriter["version"].String() = mod.second.toString();
+		mods.Vector().push_back(modWriter);
+	}
 
 	//todo: multilevel map save support
 	JsonNode & levels = header["mapLevels"];

+ 17 - 1
mapeditor/mainwindow.cpp

@@ -333,7 +333,23 @@ bool MainWindow::openMap(const QString & filenameSelect)
 	CMapService mapService;
 	try
 	{
-		controller.setMap(mapService.loadMap(resId));
+		if(auto header = mapService.loadMapHeader(resId))
+		{
+			auto missingMods = CMapService::verifyMapHeaderMods(*header);
+			CModHandler::Incompatibility::ModList modList;
+			for(const auto & m : missingMods)
+				modList.push_back({m.first, m.second.toString()});
+			
+			if(!modList.empty())
+				throw CModHandler::Incompatibility(std::move(modList));
+			
+			controller.setMap(mapService.loadMap(resId));
+		}
+	}
+	catch(const CModHandler::Incompatibility & e)
+	{
+		QMessageBox::warning(this, "Mods requiered", e.what());
+		return false;
 	}
 	catch(const std::exception & e)
 	{

+ 33 - 0
mapeditor/mapcontroller.cpp

@@ -554,3 +554,36 @@ void MapController::redo()
 	sceneForceUpdate(); //TODO: use smart invalidation (setDirty)
 	main->mapChanged();
 }
+
+ModCompatibilityInfo MapController::modAssessmentAll()
+{
+	ModCompatibilityInfo result;
+	for(auto primaryID : VLC->objtypeh->knownObjects())
+	{
+		for(auto secondaryID : VLC->objtypeh->knownSubObjects(primaryID))
+		{
+			auto handler = VLC->objtypeh->getHandlerFor(primaryID, secondaryID);
+			auto modName = QString::fromStdString(handler->getJsonKey()).split(":").at(0).toStdString();
+			if(modName != "core")
+				result[modName] = VLC->modh->getModInfo(modName).version;
+		}
+	}
+	return result;
+}
+
+ModCompatibilityInfo MapController::modAssessmentMap(const CMap & map)
+{
+	ModCompatibilityInfo result;
+	for(auto obj : map.objects)
+	{
+		if(obj->ID == Obj::HERO)
+			continue; //stub! 
+		
+		auto handler = VLC->objtypeh->getHandlerFor(obj->ID, obj->subID);
+		auto modName = QString::fromStdString(handler->getJsonKey()).split(":").at(0).toStdString();
+		if(modName != "core")
+			result[modName] = VLC->modh->getModInfo(modName).version;
+	}
+	//TODO: terrains?
+	return result;
+}

+ 3 - 0
mapeditor/mapcontroller.h

@@ -54,6 +54,9 @@ public:
 	bool discardObject(int level) const;
 	void createObject(int level, CGObjectInstance * obj) const;
 	bool canPlaceObject(int level, CGObjectInstance * obj, QString & error) const;
+	
+	static ModCompatibilityInfo modAssessmentAll();
+	static ModCompatibilityInfo modAssessmentMap(const CMap & map);
 
 	void undo();
 	void redo();

+ 140 - 1
mapeditor/mapsettings.cpp

@@ -17,8 +17,10 @@
 #include "../lib/CArtHandler.h"
 #include "../lib/CHeroHandler.h"
 #include "../lib/CGeneralTextHandler.h"
+#include "../lib/CModHandler.h"
 #include "../lib/mapObjects/CGHeroInstance.h"
 #include "../lib/mapObjects/MiscObjects.h"
+#include "../lib/mapping/CMapService.h"
 #include "../lib/StringConstants.h"
 #include "inspector/townbulidingswidget.h" //to convert BuildingID to string
 
@@ -82,6 +84,14 @@ std::vector<JsonNode> linearJsonArray(const JsonNode & json)
 	return result;
 }
 
+void traverseNode(QTreeWidgetItem * item, std::function<void(QTreeWidgetItem*)> action)
+{
+	// Do something with item
+	action(item);
+	for (int i = 0; i < item->childCount(); ++i)
+		traverseNode(item->child(i), action);
+}
+
 MapSettings::MapSettings(MapController & ctrl, QWidget *parent) :
 	QDialog(parent),
 	ui(new Ui::MapSettings),
@@ -93,10 +103,11 @@ MapSettings::MapSettings(MapController & ctrl, QWidget *parent) :
 
 	ui->mapNameEdit->setText(tr(controller.map()->name.c_str()));
 	ui->mapDescriptionEdit->setPlainText(tr(controller.map()->description.c_str()));
+	ui->heroLevelLimit->setValue(controller.map()->levelLimit);
+	ui->heroLevelLimitCheck->setChecked(controller.map()->levelLimit);
 	
 	show();
 	
-	
 	for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i)
 	{
 		auto * item = new QListWidgetItem(QString::fromStdString(VLC->skillh->objects[i]->getNameTranslated()));
@@ -385,6 +396,61 @@ MapSettings::MapSettings(MapController & ctrl, QWidget *parent) :
 			}
 		}
 	}
+	
+	//mods management
+	//collect all active mods
+	QMap<QString, QTreeWidgetItem*> addedMods;
+	QSet<QString> modsToProcess;
+	ui->treeMods->blockSignals(true);
+	
+	auto createModTreeWidgetItem = [&](QTreeWidgetItem * parent, const CModInfo & modInfo)
+	{
+		auto item = new QTreeWidgetItem(parent, {QString::fromStdString(modInfo.name), QString::fromStdString(modInfo.version.toString())});
+		item->setData(0, Qt::UserRole, QVariant(QString::fromStdString(modInfo.identifier)));
+		item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
+		item->setCheckState(0, controller.map()->mods.count(modInfo.identifier) ? Qt::Checked : Qt::Unchecked);
+		//set parent check
+		if(parent && item->checkState(0) == Qt::Checked)
+			parent->setCheckState(0, Qt::Checked);
+		return item;
+	};
+	
+	for(const auto & modName : VLC->modh->getActiveMods())
+	{
+		QString qmodName = QString::fromStdString(modName);
+		if(qmodName.split(".").size() == 1)
+		{
+			const auto & modInfo = VLC->modh->getModInfo(modName);
+			addedMods[qmodName] = createModTreeWidgetItem(nullptr, modInfo);
+			ui->treeMods->addTopLevelItem(addedMods[qmodName]);
+		}
+		else
+		{
+			modsToProcess.insert(qmodName);
+		}
+	}
+	
+	for(auto qmodIter = modsToProcess.begin(); qmodIter != modsToProcess.end();)
+	{
+		auto qmodName = *qmodIter;
+		auto pieces = qmodName.split(".");
+		assert(pieces.size() > 1);
+		
+		QString qs;
+		for(int i = 0; i < pieces.size() - 1; ++i)
+			qs += pieces[i];
+		
+		if(addedMods.count(qs))
+		{
+			const auto & modInfo = VLC->modh->getModInfo(qmodName.toStdString());
+			addedMods[qmodName] = createModTreeWidgetItem(addedMods[qs], modInfo);
+			modsToProcess.erase(qmodIter);
+			qmodIter = modsToProcess.begin();
+		}
+		else
+			++qmodIter;
+	}
+	ui->treeMods->blockSignals(false);
 }
 
 MapSettings::~MapSettings()
@@ -428,10 +494,30 @@ std::string MapSettings::getMonsterName(int monsterObjectIdx)
 	return name;
 }
 
+void MapSettings::updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods)
+{
+	//Mod management
+	auto widgetAction = [&](QTreeWidgetItem * item)
+	{
+		auto modName = item->data(0, Qt::UserRole).toString().toStdString();
+		item->setCheckState(0, mods.count(modName) ? Qt::Checked : Qt::Unchecked);
+	};
+	
+	for (int i = 0; i < ui->treeMods->topLevelItemCount(); ++i)
+	{
+		QTreeWidgetItem *item = ui->treeMods->topLevelItem(i);
+		traverseNode(item, widgetAction);
+	}
+}
+
 void MapSettings::on_pushButton_clicked()
 {
 	controller.map()->name = ui->mapNameEdit->text().toStdString();
 	controller.map()->description = ui->mapDescriptionEdit->toPlainText().toStdString();
+	if(ui->heroLevelLimitCheck->isChecked())
+		controller.map()->levelLimit = ui->heroLevelLimit->value();
+	else
+		controller.map()->levelLimit = 0;
 	controller.commitChangeWithoutRedraw();
 	
 	for(int i = 0; i < controller.map()->allowedAbilities.size(); ++i)
@@ -699,6 +785,23 @@ void MapSettings::on_pushButton_clicked()
 		controller.map()->triggeredEvents.push_back(specialDefeat);
 	}
 	
+	//Mod management
+	auto widgetAction = [&](QTreeWidgetItem * item)
+	{
+		if(item->checkState(0) == Qt::Checked)
+		{
+			auto modName = item->data(0, Qt::UserRole).toString().toStdString();
+			controller.map()->mods[modName] = VLC->modh->getModInfo(modName).version;
+		}
+	};
+	
+	controller.map()->mods.clear();
+	for (int i = 0; i < ui->treeMods->topLevelItemCount(); ++i)
+	{
+		QTreeWidgetItem *item = ui->treeMods->topLevelItem(i);
+		traverseNode(item, widgetAction);
+	}
+	
 	close();
 }
 
@@ -875,3 +978,39 @@ void MapSettings::on_loseComboBox_currentIndexChanged(int index)
 	}
 }
 
+
+void MapSettings::on_heroLevelLimitCheck_toggled(bool checked)
+{
+	ui->heroLevelLimit->setEnabled(checked);
+}
+
+void MapSettings::on_modResolution_map_clicked()
+{
+	updateModWidgetBasedOnMods(MapController::modAssessmentMap(*controller.map()));
+}
+
+
+void MapSettings::on_modResolution_full_clicked()
+{
+	updateModWidgetBasedOnMods(MapController::modAssessmentAll());
+}
+
+void MapSettings::on_treeMods_itemChanged(QTreeWidgetItem *item, int column)
+{
+	//set state for children
+	for (int i = 0; i < item->childCount(); ++i)
+		item->child(i)->setCheckState(0, item->checkState(0));
+	
+	//set state for parent
+	ui->treeMods->blockSignals(true);
+	if(item->checkState(0) == Qt::Checked)
+	{
+		while(item->parent())
+		{
+			item->parent()->setCheckState(0, Qt::Checked);
+			item = item->parent();
+		}
+	}
+	ui->treeMods->blockSignals(false);
+}
+

+ 10 - 0
mapeditor/mapsettings.h

@@ -32,12 +32,22 @@ private slots:
 
 	void on_loseComboBox_currentIndexChanged(int index);
 
+	void on_heroLevelLimitCheck_toggled(bool checked);
+
+	void on_modResolution_map_clicked();
+
+	void on_modResolution_full_clicked();
+
+	void on_treeMods_itemChanged(QTreeWidgetItem *item, int column);
+
 private:
 	
 	std::string getTownName(int townObjectIdx);
 	std::string getHeroName(int townObjectIdx);
 	std::string getMonsterName(int townObjectIdx);
 	
+	void updateModWidgetBasedOnMods(const ModCompatibilityInfo & mods);
+	
 	template<class T>
 	std::vector<int> getObjectIndexes() const
 	{

+ 114 - 10
mapeditor/mapsettings.ui

@@ -9,8 +9,8 @@
    <rect>
     <x>0</x>
     <y>0</y>
-    <width>470</width>
-    <height>480</height>
+    <width>543</width>
+    <height>494</height>
    </rect>
   </property>
   <property name="sizePolicy">
@@ -23,14 +23,7 @@
    <string>Map settings</string>
   </property>
   <layout class="QGridLayout" name="gridLayout">
-   <item row="2" column="1">
-    <widget class="QPushButton" name="pushButton">
-     <property name="text">
-      <string>Ok</string>
-     </property>
-    </widget>
-   </item>
-   <item row="1" column="1">
+   <item row="3" column="1">
     <widget class="QTabWidget" name="tabWidget">
      <property name="currentIndex">
       <number>0</number>
@@ -60,6 +53,39 @@
        <item>
         <widget class="QPlainTextEdit" name="mapDescriptionEdit"/>
        </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_4">
+         <property name="topMargin">
+          <number>10</number>
+         </property>
+         <item>
+          <widget class="QSpinBox" name="heroLevelLimit">
+           <property name="enabled">
+            <bool>false</bool>
+           </property>
+           <property name="minimumSize">
+            <size>
+             <width>48</width>
+             <height>0</height>
+            </size>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QCheckBox" name="heroLevelLimitCheck">
+           <property name="sizePolicy">
+            <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+             <horstretch>0</horstretch>
+             <verstretch>0</verstretch>
+            </sizepolicy>
+           </property>
+           <property name="text">
+            <string>Limit maximum heroes level</string>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
        <item>
         <widget class="QGroupBox" name="groupBox">
          <property name="title">
@@ -106,6 +132,77 @@
        </item>
       </layout>
      </widget>
+     <widget class="QWidget" name="tab_9">
+      <attribute name="title">
+       <string>Mods</string>
+      </attribute>
+      <layout class="QVBoxLayout" name="verticalLayout_8">
+       <item>
+        <widget class="QLabel" name="label_6">
+         <property name="text">
+          <string>Mandatory mods for playing this map</string>
+         </property>
+        </widget>
+       </item>
+       <item>
+        <widget class="QTreeWidget" name="treeMods">
+         <property name="sizeAdjustPolicy">
+          <enum>QAbstractScrollArea::AdjustIgnored</enum>
+         </property>
+         <attribute name="headerDefaultSectionSize">
+          <number>320</number>
+         </attribute>
+         <column>
+          <property name="text">
+           <string>Mod name</string>
+          </property>
+         </column>
+         <column>
+          <property name="text">
+           <string>Version</string>
+          </property>
+         </column>
+        </widget>
+       </item>
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout_3">
+         <item>
+          <widget class="QLabel" name="label_5">
+           <property name="text">
+            <string>Automatic assignment</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QPushButton" name="modResolution_map">
+           <property name="toolTip">
+            <string>Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods</string>
+           </property>
+           <property name="text">
+            <string>Map objects mods</string>
+           </property>
+           <property name="autoDefault">
+            <bool>false</bool>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QPushButton" name="modResolution_full">
+           <property name="toolTip">
+            <string>Set all mods having a game content as mandatory</string>
+           </property>
+           <property name="text">
+            <string>Full content mods</string>
+           </property>
+           <property name="autoDefault">
+            <bool>false</bool>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+      </layout>
+     </widget>
      <widget class="QWidget" name="tab_6">
       <attribute name="title">
        <string>Events</string>
@@ -336,6 +433,13 @@
      </widget>
     </widget>
    </item>
+   <item row="4" column="1">
+    <widget class="QPushButton" name="pushButton">
+     <property name="text">
+      <string>Ok</string>
+     </property>
+    </widget>
+   </item>
   </layout>
  </widget>
  <resources/>

+ 10 - 0
mapeditor/validator.cpp

@@ -10,6 +10,7 @@
 
 #include "StdInc.h"
 #include "validator.h"
+#include "mapcontroller.h"
 #include "ui_validator.h"
 #include "../lib/mapObjects/MapObjects.h"
 #include "../lib/CHeroHandler.h"
@@ -158,6 +159,15 @@ std::list<Validator::Issue> Validator::validate(const CMap * map)
 			issues.emplace_back("Map name is not specified", false);
 		if(map->description.empty())
 			issues.emplace_back("Map description is not specified", false);
+		
+		//verificationfor mods
+		for(auto & mod : MapController::modAssessmentMap(*map))
+		{
+			if(!map->mods.count(mod.first))
+			{
+				issues.emplace_back(QString("Map contains object from mod \"%1\", but doesn't require it").arg(QString::fromStdString(VLC->modh->getModInfo(mod.first).name)), true);
+			}
+		}
 	}
 	catch(const std::exception & e)
 	{

+ 1 - 1
mapeditor/windownewmap.cpp

@@ -264,7 +264,7 @@ void WindowNewMap::on_okButton_clicked()
 		nmap = f.get();
 	}
 	
-
+	nmap->mods = MapController::modAssessmentAll();
 	static_cast<MainWindow*>(parent())->controller.setMap(std::move(nmap));
 	static_cast<MainWindow*>(parent())->initializeMap(true);
 	close();