浏览代码

Mod management rework, part 1

- Replaced CModInfo class with constant ModDescription class
- Simplified mod loading logic
- Extracted some functionality from ModHandler into separate classes for
future reuse by Launcher
Ivan Savenko 11 月之前
父节点
当前提交
ba9e3dca9d

+ 3 - 3
client/globalLobby/GlobalLobbyRoomWindow.cpp

@@ -27,7 +27,7 @@
 #include "../widgets/ObjectLists.h"
 
 #include "../../lib/modding/CModHandler.h"
-#include "../../lib/modding/CModInfo.h"
+#include "../../lib/modding/ModDescription.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
 #include "../../lib/texts/MetaString.h"
 
@@ -128,14 +128,14 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s
 		GlobalLobbyRoomModInfo modInfo;
 		modInfo.status = modEntry.second;
 		if (modEntry.second == ModVerificationStatus::EXCESSIVE)
-			modInfo.version = CGI->modh->getModInfo(modEntry.first).getVerificationInfo().version.toString();
+			modInfo.version = CGI->modh->getModInfo(modEntry.first).getVersion().toString();
 		else
 			modInfo.version = roomDescription.modList.at(modEntry.first).version.toString();
 
 		if (modEntry.second == ModVerificationStatus::NOT_INSTALLED)
 			modInfo.modName = roomDescription.modList.at(modEntry.first).name;
 		else
-			modInfo.modName = CGI->modh->getModInfo(modEntry.first).getVerificationInfo().name;
+			modInfo.modName = CGI->modh->getModInfo(modEntry.first).getName();
 
 		modVerificationList.push_back(modInfo);
 	}

+ 23 - 29
launcher/modManager/cmodmanager.cpp

@@ -14,7 +14,6 @@
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/filesystem/CZipLoader.h"
 #include "../../lib/modding/CModHandler.h"
-#include "../../lib/modding/CModInfo.h"
 #include "../../lib/modding/IdentifierStorage.h"
 
 #include "../vcmiqt/jsonutils.h"
@@ -90,37 +89,32 @@ void CModManager::loadRepositories(QVector<QVariantMap> repomap)
 void CModManager::loadMods()
 {
 	CModHandler handler;
-	handler.loadMods();
 	auto installedMods = handler.getAllMods();
 	localMods.clear();
 
-	for(auto modname : installedMods)
-	{
-		auto resID = CModInfo::getModFile(modname);
-		if(CResourceHandler::get()->existsResource(resID))
-		{
-			//calculate mod size
-			qint64 total = 0;
-			ResourcePath resDir(CModInfo::getModDir(modname), EResType::DIRECTORY);
-			if(CResourceHandler::get()->existsResource(resDir))
-			{
-				for(QDirIterator iter(QString::fromStdString(CResourceHandler::get()->getResourceName(resDir)->string()), QDirIterator::Subdirectories); iter.hasNext(); iter.next())
-					total += iter.fileInfo().size();
-			}
-			
-			boost::filesystem::path name = *CResourceHandler::get()->getResourceName(resID);
-			auto mod = JsonUtils::JsonFromFile(pathToQString(name));
-			auto json = JsonUtils::toJson(mod);
-			json["localSizeBytes"].Float() = total;
-			if(!name.is_absolute())
-				json["storedLocally"].Bool() = true;
-
-			mod = JsonUtils::toVariant(json);
-			QString modNameQt = QString::fromUtf8(modname.c_str()).toLower();
-			localMods.insert(modNameQt, mod);
-			modSettings->registerNewMod(modNameQt, json["keepDisabled"].Bool());
-		}
-	}
+//	for(auto modname : installedMods)
+//	{
+//			//calculate mod size
+//			qint64 total = 0;
+//			ResourcePath resDir(CModInfo::getModDir(modname), EResType::DIRECTORY);
+//			if(CResourceHandler::get()->existsResource(resDir))
+//			{
+//				for(QDirIterator iter(QString::fromStdString(CResourceHandler::get()->getResourceName(resDir)->string()), QDirIterator::Subdirectories); iter.hasNext(); iter.next())
+//					total += iter.fileInfo().size();
+//			}
+//
+//			boost::filesystem::path name = *CResourceHandler::get()->getResourceName(resID);
+//			auto mod = JsonUtils::JsonFromFile(pathToQString(name));
+//			auto json = JsonUtils::toJson(mod);
+//			json["localSizeBytes"].Float() = total;
+//			if(!name.is_absolute())
+//				json["storedLocally"].Bool() = true;
+//
+//			mod = JsonUtils::toVariant(json);
+//			QString modNameQt = QString::fromUtf8(modname.c_str()).toLower();
+//			localMods.insert(modNameQt, mod);
+//			modSettings->registerNewMod(modNameQt, json["keepDisabled"].Bool());
+//	}
 	modList->setLocalModList(localMods);
 }
 

+ 4 - 2
lib/CMakeLists.txt

@@ -157,10 +157,11 @@ set(lib_MAIN_SRCS
 
 	modding/ActiveModsInSaveList.cpp
 	modding/CModHandler.cpp
-	modding/CModInfo.cpp
 	modding/CModVersion.cpp
 	modding/ContentTypeHandler.cpp
 	modding/IdentifierStorage.cpp
+	modding/ModDescription.cpp
+	modding/ModManager.cpp
 	modding/ModUtility.cpp
 	modding/ModVerificationInfo.cpp
 
@@ -547,11 +548,12 @@ set(lib_MAIN_HEADERS
 
 	modding/ActiveModsInSaveList.h
 	modding/CModHandler.h
-	modding/CModInfo.h
 	modding/CModVersion.h
 	modding/ContentTypeHandler.h
 	modding/IdentifierStorage.h
+	modding/ModDescription.h
 	modding/ModIncompatibility.h
+	modding/ModManager.h
 	modding/ModScope.h
 	modding/ModUtility.h
 	modding/ModVerificationInfo.h

+ 0 - 1
lib/IGameCallback.cpp

@@ -41,7 +41,6 @@
 #include "gameState/QuestInfo.h"
 #include "mapping/CMap.h"
 #include "modding/CModHandler.h"
-#include "modding/CModInfo.h"
 #include "modding/IdentifierStorage.h"
 #include "modding/CModVersion.h"
 #include "modding/ActiveModsInSaveList.h"

+ 0 - 2
lib/VCMI_Lib.cpp

@@ -26,7 +26,6 @@
 #include "entities/hero/CHeroHandler.h"
 #include "texts/CGeneralTextHandler.h"
 #include "modding/CModHandler.h"
-#include "modding/CModInfo.h"
 #include "modding/IdentifierStorage.h"
 #include "modding/CModVersion.h"
 #include "IGameEventsReceiver.h"
@@ -157,7 +156,6 @@ void LibClasses::loadModFilesystem()
 	CStopWatch loadTime;
 	modh = std::make_unique<CModHandler>();
 	identifiersHandler = std::make_unique<CIdentifierStorage>();
-	modh->loadMods();
 	logGlobal->info("\tMod handler: %d ms", loadTime.getDiff());
 
 	modh->loadModFilesystems();

+ 1 - 0
lib/filesystem/Filesystem.cpp

@@ -212,6 +212,7 @@ ISimpleResourceLoader * CResourceHandler::get()
 
 ISimpleResourceLoader * CResourceHandler::get(const std::string & identifier)
 {
+	assert(knownLoaders.count(identifier));
 	return knownLoaders.at(identifier);
 }
 

+ 2 - 2
lib/mapping/CMapService.cpp

@@ -17,8 +17,8 @@
 #include "../filesystem/CMemoryStream.h"
 #include "../filesystem/CMemoryBuffer.h"
 #include "../modding/CModHandler.h"
+#include "../modding/ModDescription.h"
 #include "../modding/ModScope.h"
-#include "../modding/CModInfo.h"
 #include "../VCMI_Lib.h"
 
 #include "CMap.h"
@@ -99,7 +99,7 @@ ModCompatibilityInfo CMapService::verifyMapHeaderMods(const CMapHeader & map)
 		if(vstd::contains(activeMods, mapMod.first))
 		{
 			const auto & modInfo = VLC->modh->getModInfo(mapMod.first);
-			if(modInfo.getVerificationInfo().version.compatible(mapMod.second.version))
+			if(modInfo.getVersion().compatible(mapMod.second.version))
 				continue;
 		}
 		missingMods[mapMod.first] = mapMod.second;

+ 5 - 5
lib/modding/ActiveModsInSaveList.cpp

@@ -11,7 +11,7 @@
 #include "ActiveModsInSaveList.h"
 
 #include "../VCMI_Lib.h"
-#include "CModInfo.h"
+#include "ModDescription.h"
 #include "CModHandler.h"
 #include "ModIncompatibility.h"
 
@@ -21,13 +21,13 @@ std::vector<TModID> ActiveModsInSaveList::getActiveGameplayAffectingMods()
 {
 	std::vector<TModID> result;
 	for (auto const & entry : VLC->modh->getActiveMods())
-		if (VLC->modh->getModInfo(entry).checkModGameplayAffecting())
+		if (VLC->modh->getModInfo(entry).affectsGameplay())
 			result.push_back(entry);
 
 	return result;
 }
 
-const ModVerificationInfo & ActiveModsInSaveList::getVerificationInfo(TModID mod)
+ModVerificationInfo ActiveModsInSaveList::getVerificationInfo(TModID mod)
 {
 	return VLC->modh->getModInfo(mod).getVerificationInfo();
 }
@@ -44,10 +44,10 @@ void ActiveModsInSaveList::verifyActiveMods(const std::map<TModID, ModVerificati
 			missingMods.push_back(modList.at(compared.first).name);
 
 		if (compared.second == ModVerificationStatus::DISABLED)
-			missingMods.push_back(VLC->modh->getModInfo(compared.first).getVerificationInfo().name);
+			missingMods.push_back(VLC->modh->getModInfo(compared.first).getName());
 
 		if (compared.second == ModVerificationStatus::EXCESSIVE)
-			excessiveMods.push_back(VLC->modh->getModInfo(compared.first).getVerificationInfo().name);
+			excessiveMods.push_back(VLC->modh->getModInfo(compared.first).getName());
 	}
 
 	if(!missingMods.empty() || !excessiveMods.empty())

+ 5 - 2
lib/modding/ActiveModsInSaveList.h

@@ -17,7 +17,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 class ActiveModsInSaveList
 {
 	std::vector<TModID> getActiveGameplayAffectingMods();
-	const ModVerificationInfo & getVerificationInfo(TModID mod);
+	ModVerificationInfo getVerificationInfo(TModID mod);
 
 	/// Checks whether provided mod list is compatible with current VLC and throws on failure
 	void verifyActiveMods(const std::map<TModID, ModVerificationInfo> & modList);
@@ -29,7 +29,10 @@ public:
 			std::vector<TModID> activeMods = getActiveGameplayAffectingMods();
 			h & activeMods;
 			for(const auto & m : activeMods)
-				h & getVerificationInfo(m);
+			{
+				ModVerificationInfo info = getVerificationInfo(m);
+				h & info;
+			}
 		}
 		else
 		{

+ 124 - 407
lib/modding/CModHandler.cpp

@@ -10,316 +10,49 @@
 #include "StdInc.h"
 #include "CModHandler.h"
 
-#include "CModInfo.h"
-#include "ModScope.h"
 #include "ContentTypeHandler.h"
 #include "IdentifierStorage.h"
-#include "ModIncompatibility.h"
+#include "ModDescription.h"
+#include "ModManager.h"
+#include "ModScope.h"
 
-#include "../CCreatureHandler.h"
 #include "../CConfigHandler.h"
-#include "../CStopWatch.h"
+#include "../CCreatureHandler.h"
 #include "../GameSettings.h"
-#include "../ScriptHandler.h"
-#include "../constants/StringConstants.h"
+#include "../VCMI_Lib.h"
 #include "../filesystem/Filesystem.h"
 #include "../json/JsonUtils.h"
-#include "../spells/CSpellHandler.h"
 #include "../texts/CGeneralTextHandler.h"
 #include "../texts/Languages.h"
-#include "../VCMI_Lib.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
-static JsonNode loadModSettings(const JsonPath & path)
-{
-	if (CResourceHandler::get("local")->existsResource(ResourcePath(path)))
-	{
-		return JsonNode(path);
-	}
-	// Probably new install. Create initial configuration
-	CResourceHandler::get("local")->createResource(path.getOriginalName() + ".json");
-	return JsonNode();
-}
-
 CModHandler::CModHandler()
 	: content(std::make_shared<CContentHandler>())
-	, coreMod(std::make_unique<CModInfo>())
+	, modManager(std::make_unique<ModManager>())
 {
 }
 
 CModHandler::~CModHandler() = default;
 
-// currentList is passed by value to get current list of depending mods
-bool CModHandler::hasCircularDependency(const TModID & modID, std::set<TModID> currentList) const
-{
-	const CModInfo & mod = allMods.at(modID);
-
-	// Mod already present? We found a loop
-	if (vstd::contains(currentList, modID))
-	{
-		logMod->error("Error: Circular dependency detected! Printing dependency list:");
-		logMod->error("\t%s -> ", mod.getVerificationInfo().name);
-		return true;
-	}
-
-	currentList.insert(modID);
-
-	// recursively check every dependency of this mod
-	for(const TModID & dependency : mod.dependencies)
-	{
-		if (hasCircularDependency(dependency, currentList))
-		{
-			logMod->error("\t%s ->\n", mod.getVerificationInfo().name); // conflict detected, print dependency list
-			return true;
-		}
-	}
-	return false;
-}
-
-// Returned vector affects the resource loaders call order (see CFilesystemList::load).
-// The loaders call order matters when dependent mod overrides resources in its dependencies.
-std::vector <TModID> CModHandler::validateAndSortDependencies(std::vector <TModID> modsToResolve) const
-{
-	// Topological sort algorithm.
-	// TODO: Investigate possible ways to improve performance.
-	boost::range::sort(modsToResolve); // Sort mods per name
-	std::vector <TModID> sortedValidMods; // Vector keeps order of elements (LIFO)
-	sortedValidMods.reserve(modsToResolve.size()); // push_back calls won't cause memory reallocation
-	std::set <TModID> resolvedModIDs; // Use a set for validation for performance reason, but set does not keep order of elements
-	std::set <TModID> notResolvedModIDs(modsToResolve.begin(), modsToResolve.end()); // Use a set for validation for performance reason
-
-	// Mod is resolved if it has no dependencies or all its dependencies are already resolved
-	auto isResolved = [&](const CModInfo & mod) -> bool
-	{
-		if(mod.dependencies.size() > resolvedModIDs.size())
-			return false;
-
-		for(const TModID & dependency : mod.dependencies)
-		{
-			if(!vstd::contains(resolvedModIDs, dependency))
-				return false;
-		}
-
-		for(const TModID & softDependency : mod.softDependencies)
-		{
-			if(vstd::contains(notResolvedModIDs, softDependency))
-				return false;
-		}
-
-		for(const TModID & conflict : mod.conflicts)
-		{
-			if(vstd::contains(resolvedModIDs, conflict))
-				return false;
-		}
-		for(const TModID & reverseConflict : resolvedModIDs)
-		{
-			if (vstd::contains(allMods.at(reverseConflict).conflicts, mod.identifier))
-				return false;
-		}
-		return true;
-	};
-
-	while(true)
-	{
-		std::set <TModID> resolvedOnCurrentTreeLevel;
-		for(auto it = modsToResolve.begin(); it != modsToResolve.end();) // One iteration - one level of mods tree
-		{
-			if(isResolved(allMods.at(*it)))
-			{
-				resolvedOnCurrentTreeLevel.insert(*it); // Not to the resolvedModIDs, so current node children will be resolved on the next iteration
-				sortedValidMods.push_back(*it);
-				it = modsToResolve.erase(it);
-				continue;
-			}
-			it++;
-		}
-		if(!resolvedOnCurrentTreeLevel.empty())
-		{
-			resolvedModIDs.insert(resolvedOnCurrentTreeLevel.begin(), resolvedOnCurrentTreeLevel.end());
-			for(const auto & it : resolvedOnCurrentTreeLevel)
-				notResolvedModIDs.erase(it);
-			continue;
-		}
-		// If there are no valid mods on the current mods tree level, no more mod can be resolved, should be ended.
-		break;
-	}
-
-	modLoadErrors = std::make_unique<MetaString>();
-
-	auto addErrorMessage = [this](const std::string & textID, const std::string & brokenModID, const std::string & missingModID)
-	{
-		modLoadErrors->appendTextID(textID);
-
-		if (allMods.count(brokenModID))
-			modLoadErrors->replaceRawString(allMods.at(brokenModID).getVerificationInfo().name);
-		else
-			modLoadErrors->replaceRawString(brokenModID);
-
-		if (allMods.count(missingModID))
-			modLoadErrors->replaceRawString(allMods.at(missingModID).getVerificationInfo().name);
-		else
-			modLoadErrors->replaceRawString(missingModID);
-
-	};
-
-	// Left mods have unresolved dependencies, output all to log.
-	for(const auto & brokenModID : modsToResolve)
-	{
-		const CModInfo & brokenMod = allMods.at(brokenModID);
-		bool showErrorMessage = false;
-		for(const TModID & dependency : brokenMod.dependencies)
-		{
-			if(!vstd::contains(resolvedModIDs, dependency) && brokenMod.config["modType"].String() != "Compatibility")
-			{
-				addErrorMessage("vcmi.server.errors.modNoDependency", brokenModID, dependency);
-				showErrorMessage = true;
-			}
-		}
-		for(const TModID & conflict : brokenMod.conflicts)
-		{
-			if(vstd::contains(resolvedModIDs, conflict))
-			{
-				addErrorMessage("vcmi.server.errors.modConflict", brokenModID, conflict);
-				showErrorMessage = true;
-			}
-		}
-		for(const TModID & reverseConflict : resolvedModIDs)
-		{
-			if (vstd::contains(allMods.at(reverseConflict).conflicts, brokenModID))
-			{
-				addErrorMessage("vcmi.server.errors.modConflict", brokenModID, reverseConflict);
-				showErrorMessage = true;
-			}
-		}
-
-		// some mods may in a (soft) dependency loop.
-		if(!showErrorMessage && brokenMod.config["modType"].String() != "Compatibility")
-		{
-			modLoadErrors->appendTextID("vcmi.server.errors.modDependencyLoop");
-			if (allMods.count(brokenModID))
-				modLoadErrors->replaceRawString(allMods.at(brokenModID).getVerificationInfo().name);
-			else
-				modLoadErrors->replaceRawString(brokenModID);
-		}
-
-	}
-	return sortedValidMods;
-}
-
-std::vector<std::string> CModHandler::getModList(const std::string & path) const
-{
-	std::string modDir = boost::to_upper_copy(path + "MODS/");
-	size_t depth = boost::range::count(modDir, '/');
-
-	auto list = CResourceHandler::get("initial")->getFilteredFiles([&](const ResourcePath & id) ->  bool
-	{
-		if (id.getType() != EResType::DIRECTORY)
-			return false;
-		if (!boost::algorithm::starts_with(id.getName(), modDir))
-			return false;
-		if (boost::range::count(id.getName(), '/') != depth )
-			return false;
-		return true;
-	});
-
-	//storage for found mods
-	std::vector<std::string> foundMods;
-	for(const auto & entry : list)
-	{
-		std::string name = entry.getName();
-		name.erase(0, modDir.size()); //Remove path prefix
-
-		if (!name.empty())
-			foundMods.push_back(name);
-	}
-	return foundMods;
-}
-
-
-
-void CModHandler::loadMods(const std::string & path, const std::string & parent, const JsonNode & modSettings, const std::vector<TModID> & modsToActivate, bool enableMods)
-{
-	for(const std::string & modName : getModList(path))
-		loadOneMod(modName, parent, modSettings, modsToActivate, enableMods);
-}
-
-void CModHandler::loadOneMod(std::string modName, const std::string & parent, const JsonNode & modSettings, const std::vector<TModID> & modsToActivate, bool enableMods)
-{
-	boost::to_lower(modName);
-	std::string modFullName = parent.empty() ? modName : parent + '.' + modName;
-
-	if ( ModScope::isScopeReserved(modFullName))
-	{
-		logMod->error("Can not load mod %s - this name is reserved for internal use!", modFullName);
-		return;
-	}
-
-	if(CResourceHandler::get("initial")->existsResource(CModInfo::getModFile(modFullName)))
-	{
-		bool thisModActive = vstd::contains(modsToActivate, modFullName);
-		CModInfo mod(modFullName, modSettings[modName], JsonNode(CModInfo::getModFile(modFullName)), thisModActive);
-		if (!parent.empty()) // this is submod, add parent to dependencies
-			mod.dependencies.insert(parent);
-
-		allMods[modFullName] = mod;
-		if (mod.isEnabled() && enableMods)
-			activeMods.push_back(modFullName);
-
-		loadMods(CModInfo::getModDir(modFullName) + '/', modFullName, modSettings[modName]["mods"], modsToActivate, enableMods && mod.isEnabled());
-	}
-}
-
-void CModHandler::loadMods()
-{
-	JsonNode modConfig;
-
-	modConfig = loadModSettings(JsonPath::builtin("config/modSettings.json"));
-	const JsonNode & modSettings = modConfig["activeMods"];
-	const std::string & currentPresetName = modConfig["activePreset"].String();
-	const JsonNode & currentPreset = modConfig["presets"][currentPresetName];
-	const JsonNode & modsToActivateJson = currentPreset["mods"];
-	std::vector<TModID> modsToActivate = modsToActivateJson.convertTo<std::vector<TModID>>();
-
-	for(const auto & settings : currentPreset["settings"].Struct())
-	{
-		if (!vstd::contains(modsToActivate, settings.first))
-			continue; // settings for inactive mod
-
-		for (const auto & submod : settings.second.Struct())
-		{
-			if (submod.second.Bool())
-				modsToActivate.push_back(settings.first + '.' + submod.first);
-		}
-	}
-
-	loadMods("", "", modSettings, modsToActivate, true);
-
-	coreMod = std::make_unique<CModInfo>(ModScope::scopeBuiltin(), modConfig[ModScope::scopeBuiltin()], JsonNode(JsonPath::builtin("config/gameConfig.json")), true);
-}
-
 std::vector<std::string> CModHandler::getAllMods() const
 {
-	std::vector<std::string> modlist;
-	modlist.reserve(allMods.size());
-	for (auto & entry : allMods)
-		modlist.push_back(entry.first);
-	return modlist;
+	return modManager->getActiveMods();// TODO: currently identical to active
 }
 
 std::vector<std::string> CModHandler::getActiveMods() const
 {
-	return activeMods;
+	return modManager->getActiveMods();
 }
 
 std::string CModHandler::getModLoadErrors() const
 {
-	return modLoadErrors->toString();
+	return ""; // TODO: modLoadErrors->toString();
 }
 
-const CModInfo & CModHandler::getModInfo(const TModID & modId) const
+const ModDescription & CModHandler::getModInfo(const TModID & modId) const
 {
-	return allMods.at(modId);
+	return modManager->getModDescription(modId);
 }
 
 static JsonNode genDefaultFS()
@@ -334,86 +67,95 @@ static JsonNode genDefaultFS()
 	return defaultFS;
 }
 
+static std::string getModDirectory(const TModID & modName)
+{
+	std::string result = modName;
+	boost::to_upper(result);
+	boost::algorithm::replace_all(result, ".", "/MODS/");
+	return "MODS/" + result;
+}
+
 static ISimpleResourceLoader * genModFilesystem(const std::string & modName, const JsonNode & conf)
 {
 	static const JsonNode defaultFS = genDefaultFS();
 
-	if (!conf["filesystem"].isNull())
-		return CResourceHandler::createFileSystem(CModInfo::getModDir(modName), conf["filesystem"]);
+	if (!conf.isNull())
+		return CResourceHandler::createFileSystem(getModDirectory(modName), conf);
 	else
-		return CResourceHandler::createFileSystem(CModInfo::getModDir(modName), defaultFS);
+		return CResourceHandler::createFileSystem(getModDirectory(modName), defaultFS);
 }
 
-static ui32 calculateModChecksum(const std::string & modName, ISimpleResourceLoader * filesystem)
-{
-	boost::crc_32_type modChecksum;
-	// first - add current VCMI version into checksum to force re-validation on VCMI updates
-	modChecksum.process_bytes(reinterpret_cast<const void*>(GameConstants::VCMI_VERSION.data()), GameConstants::VCMI_VERSION.size());
-
-	// second - add mod.json into checksum because filesystem does not contains this file
-	// FIXME: remove workaround for core mod
-	if (modName != ModScope::scopeBuiltin())
-	{
-		auto modConfFile = CModInfo::getModFile(modName);
-		ui32 configChecksum = CResourceHandler::get("initial")->load(modConfFile)->calculateCRC32();
-		modChecksum.process_bytes(reinterpret_cast<const void *>(&configChecksum), sizeof(configChecksum));
-	}
-	// third - add all detected text files from this mod into checksum
-	auto files = filesystem->getFilteredFiles([](const ResourcePath & resID)
-	{
-		return (resID.getType() == EResType::TEXT || resID.getType() == EResType::JSON) &&
-			   ( boost::starts_with(resID.getName(), "DATA") || boost::starts_with(resID.getName(), "CONFIG"));
-	});
-
-	for (const ResourcePath & file : files)
-	{
-		ui32 fileChecksum = filesystem->load(file)->calculateCRC32();
-		modChecksum.process_bytes(reinterpret_cast<const void *>(&fileChecksum), sizeof(fileChecksum));
-	}
-	return modChecksum.checksum();
-}
+//static ui32 calculateModChecksum(const std::string & modName, ISimpleResourceLoader * filesystem)
+//{
+//	boost::crc_32_type modChecksum;
+//	// first - add current VCMI version into checksum to force re-validation on VCMI updates
+//	modChecksum.process_bytes(reinterpret_cast<const void*>(GameConstants::VCMI_VERSION.data()), GameConstants::VCMI_VERSION.size());
+//
+//	// second - add mod.json into checksum because filesystem does not contains this file
+//	// FIXME: remove workaround for core mod
+//	if (modName != ModScope::scopeBuiltin())
+//	{
+//		auto modConfFile = CModInfo::getModFile(modName);
+//		ui32 configChecksum = CResourceHandler::get("initial")->load(modConfFile)->calculateCRC32();
+//		modChecksum.process_bytes(reinterpret_cast<const void *>(&configChecksum), sizeof(configChecksum));
+//	}
+//	// third - add all detected text files from this mod into checksum
+//	auto files = filesystem->getFilteredFiles([](const ResourcePath & resID)
+//	{
+//		return (resID.getType() == EResType::TEXT || resID.getType() == EResType::JSON) &&
+//			   ( boost::starts_with(resID.getName(), "DATA") || boost::starts_with(resID.getName(), "CONFIG"));
+//	});
+//
+//	for (const ResourcePath & file : files)
+//	{
+//		ui32 fileChecksum = filesystem->load(file)->calculateCRC32();
+//		modChecksum.process_bytes(reinterpret_cast<const void *>(&fileChecksum), sizeof(fileChecksum));
+//	}
+//	return modChecksum.checksum();
+//}
 
 void CModHandler::loadModFilesystems()
 {
 	CGeneralTextHandler::detectInstallParameters();
 
-	activeMods = validateAndSortDependencies(activeMods);
-
-	coreMod->updateChecksum(calculateModChecksum(ModScope::scopeBuiltin(), CResourceHandler::get(ModScope::scopeBuiltin())));
+	const auto & activeMods = modManager->getActiveMods();
 
-	std::map<std::string, ISimpleResourceLoader *> modFilesystems;
+	std::map<TModID, ISimpleResourceLoader *> modFilesystems;
 
-	for(std::string & modName : activeMods)
-		modFilesystems[modName] = genModFilesystem(modName, allMods[modName].config);
+	for(const TModID & modName : activeMods)
+		modFilesystems[modName] = genModFilesystem(modName, getModInfo(modName).getFilesystemConfig());
 
-	for(std::string & modName : activeMods)
+	for(const TModID & modName : activeMods)
 		CResourceHandler::addFilesystem("data", modName, modFilesystems[modName]);
 
 	if (settings["mods"]["validation"].String() == "full")
+		checkModFilesystemsConflicts(modFilesystems);
+}
+
+void CModHandler::checkModFilesystemsConflicts(const std::map<TModID, ISimpleResourceLoader *> & modFilesystems)
+{
+	for(const auto & [leftName, leftFilesystem] : modFilesystems)
 	{
-		for(std::string & leftModName : activeMods)
+		for(const auto & [rightName, rightFilesystem] : modFilesystems)
 		{
-			for(std::string & rightModName : activeMods)
-			{
-				if (leftModName == rightModName)
-					continue;
+			if (leftName == rightName)
+				continue;
 
-				if (getModDependencies(leftModName).count(rightModName) || getModDependencies(rightModName).count(leftModName))
-					continue;
+			if (getModDependencies(leftName).count(rightName) || getModDependencies(rightName).count(leftName))
+				continue;
 
-				if (getModSoftDependencies(leftModName).count(rightModName) || getModSoftDependencies(rightModName).count(leftModName))
-					continue;
+			if (getModSoftDependencies(leftName).count(rightName) || getModSoftDependencies(rightName).count(leftName))
+				continue;
 
-				const auto & filter = [](const ResourcePath &path){return path.getType() != EResType::DIRECTORY && path.getType() != EResType::JSON;};
+			const auto & filter = [](const ResourcePath &path){return path.getType() != EResType::DIRECTORY && path.getType() != EResType::JSON;};
 
-				std::unordered_set<ResourcePath> leftResources = modFilesystems[leftModName]->getFilteredFiles(filter);
-				std::unordered_set<ResourcePath> rightResources = modFilesystems[rightModName]->getFilteredFiles(filter);
+			std::unordered_set<ResourcePath> leftResources = leftFilesystem->getFilteredFiles(filter);
+			std::unordered_set<ResourcePath> rightResources = rightFilesystem->getFilteredFiles(filter);
 
-				for (auto const & leftFile : leftResources)
-				{
-					if (rightResources.count(leftFile))
-						logMod->warn("Potential confict detected between '%s' and '%s': both mods add file '%s'", leftModName, rightModName, leftFile.getOriginalName());
-				}
+			for (auto const & leftFile : leftResources)
+			{
+				if (rightResources.count(leftFile))
+					logMod->warn("Potential confict detected between '%s' and '%s': both mods add file '%s'", leftName, rightName, leftFile.getOriginalName());
 			}
 		}
 	}
@@ -423,7 +165,8 @@ TModID CModHandler::findResourceOrigin(const ResourcePath & name) const
 {
 	try
 	{
-		for(const auto & modID : boost::adaptors::reverse(activeMods))
+		auto activeMode = modManager->getActiveMods();
+		for(const auto & modID : boost::adaptors::reverse(activeMode))
 		{
 			if(CResourceHandler::get(modID)->existsResource(name))
 				return modID;
@@ -478,7 +221,7 @@ std::string CModHandler::getModLanguage(const TModID& modId) const
 		return VLC->generaltexth->getInstalledLanguage();
 	if(modId == "map")
 		return VLC->generaltexth->getPreferredLanguage();
-	return allMods.at(modId).baseLanguage;
+	return getModInfo(modId).getBaseLanguage();
 }
 
 std::set<TModID> CModHandler::getModDependencies(const TModID & modId) const
@@ -489,11 +232,9 @@ std::set<TModID> CModHandler::getModDependencies(const TModID & modId) const
 
 std::set<TModID> CModHandler::getModDependencies(const TModID & modId, bool & isModFound) const
 {
-	auto it = allMods.find(modId);
-	isModFound = (it != allMods.end());
-
-	if(isModFound)
-		return it->second.dependencies;
+	isModFound = modManager->isModActive(modId);
+	if (isModFound)
+		return modManager->getModDescription(modId).getDependencies();
 
 	logMod->error("Mod not found: '%s'", modId);
 	return {};
@@ -501,54 +242,37 @@ std::set<TModID> CModHandler::getModDependencies(const TModID & modId, bool & is
 
 std::set<TModID> CModHandler::getModSoftDependencies(const TModID & modId) const
 {
-	auto it = allMods.find(modId);
-	if(it != allMods.end())
-		return it->second.softDependencies;
-	logMod->error("Mod not found: '%s'", modId);
-	return {};
+	return modManager->getModDescription(modId).getSoftDependencies();
 }
 
 std::set<TModID> CModHandler::getModEnabledSoftDependencies(const TModID & modId) const
 {
 	std::set<TModID> softDependencies = getModSoftDependencies(modId);
-	for (auto it = softDependencies.begin(); it != softDependencies.end();)
-	{
-		if (allMods.find(*it) == allMods.end())
-			it = softDependencies.erase(it);
-		else
-			it++;
-	}
+
+	vstd::erase_if(softDependencies, [&](const TModID & dependency){ return !modManager->isModActive(dependency);});
+
 	return softDependencies;
 }
 
 void CModHandler::initializeConfig()
 {
-	VLC->settingsHandler->loadBase(JsonUtils::assembleFromFiles(coreMod->config["settings"]));
-
-	for(const TModID & modName : activeMods)
+	for(const TModID & modName : getActiveMods())
 	{
-		const auto & mod = allMods[modName];
-		if (!mod.config["settings"].isNull())
-			VLC->settingsHandler->loadBase(mod.config["settings"]);
+		const auto & mod = getModInfo(modName);
+		if (!mod.getConfig()["settings"].isNull())
+			VLC->settingsHandler->loadBase(mod.getConfig()["settings"]);
 	}
 }
 
-CModVersion CModHandler::getModVersion(TModID modName) const
-{
-	if (allMods.count(modName))
-		return allMods.at(modName).getVerificationInfo().version;
-	return {};
-}
-
 void CModHandler::loadTranslation(const TModID & modName)
 {
-	const auto & mod = allMods[modName];
+	const auto & mod = getModInfo(modName);
 
 	std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage();
-	std::string modBaseLanguage = allMods[modName].baseLanguage;
+	std::string modBaseLanguage = getModInfo(modName).getBaseLanguage();
 
-	JsonNode baseTranslation = JsonUtils::assembleFromFiles(mod.config["translations"]);
-	JsonNode extraTranslation = JsonUtils::assembleFromFiles(mod.config[preferredLanguage]["translations"]);
+	JsonNode baseTranslation = JsonUtils::assembleFromFiles(mod.getConfig()["translations"]);
+	JsonNode extraTranslation = JsonUtils::assembleFromFiles(mod.getConfig()[preferredLanguage]["translations"]);
 
 	VLC->generaltexth->loadTranslationOverrides(modName, modBaseLanguage, baseTranslation);
 	VLC->generaltexth->loadTranslationOverrides(modName, preferredLanguage, extraTranslation);
@@ -556,29 +280,22 @@ void CModHandler::loadTranslation(const TModID & modName)
 
 void CModHandler::load()
 {
-	CStopWatch totalTime;
-	CStopWatch timer;
-
-	logMod->info("\tInitializing content handler: %d ms", timer.getDiff());
+	logMod->info("\tInitializing content handler");
 
 	content->init();
 
-	for(const TModID & modName : activeMods)
-	{
-		logMod->trace("Generating checksum for %s", modName);
-		allMods[modName].updateChecksum(calculateModChecksum(modName, CResourceHandler::get(modName)));
-	}
+//	for(const TModID & modName : getActiveMods())
+//	{
+//		logMod->trace("Generating checksum for %s", modName);
+//		allMods[modName].updateChecksum(calculateModChecksum(modName, CResourceHandler::get(modName)));
+//	}
 
-	// first - load virtual builtin mod that contains all data
-	// TODO? move all data into real mods? RoE, AB, SoD, WoG
-	content->preloadData(*coreMod);
-	for(const TModID & modName : activeMods)
-		content->preloadData(allMods[modName]);
-	logMod->info("\tParsing mod data: %d ms", timer.getDiff());
+	for(const TModID & modName : getActiveMods())
+		content->preloadData(getModInfo(modName));
+	logMod->info("\tParsing mod data");
 
-	content->load(*coreMod);
-	for(const TModID & modName : activeMods)
-		content->load(allMods[modName]);
+	for(const TModID & modName : getActiveMods())
+		content->load(getModInfo(modName));
 
 #if SCRIPTING_ENABLED
 	VLC->scriptHandler->performRegistration(VLC);//todo: this should be done before any other handlers load
@@ -586,36 +303,36 @@ void CModHandler::load()
 
 	content->loadCustom();
 
-	for(const TModID & modName : activeMods)
+	for(const TModID & modName : getActiveMods())
 		loadTranslation(modName);
 
-	logMod->info("\tLoading mod data: %d ms", timer.getDiff());
+	logMod->info("\tLoading mod data");
 	VLC->creh->loadCrExpMod();
 	VLC->identifiersHandler->finalize();
-	logMod->info("\tResolving identifiers: %d ms", timer.getDiff());
+	logMod->info("\tResolving identifiers");
 
 	content->afterLoadFinalization();
-	logMod->info("\tHandlers post-load finalization: %d ms ", timer.getDiff());
-	logMod->info("\tAll game content loaded in %d ms", totalTime.getDiff());
+	logMod->info("\tHandlers post-load finalization");
+	logMod->info("\tAll game content loaded");
 }
 
 void CModHandler::afterLoad(bool onlyEssential)
 {
-	JsonNode modSettings;
-	for (auto & modEntry : allMods)
-	{
-		std::string pointer = "/" + boost::algorithm::replace_all_copy(modEntry.first, ".", "/mods/");
-
-		modSettings["activeMods"].resolvePointer(pointer) = modEntry.second.saveLocalData();
-	}
-	modSettings[ModScope::scopeBuiltin()] = coreMod->saveLocalData();
-	modSettings[ModScope::scopeBuiltin()]["name"].String() = "Original game files";
-
-	if(!onlyEssential)
-	{
-		std::fstream file(CResourceHandler::get()->getResourceName(ResourcePath("config/modSettings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc);
-		file << modSettings.toString();
-	}
+	//JsonNode modSettings;
+	//for (auto & modEntry : getActiveMods())
+	//{
+	//	std::string pointer = "/" + boost::algorithm::replace_all_copy(modEntry.first, ".", "/mods/");
+
+	//	modSettings["activeMods"].resolvePointer(pointer) = modEntry.second.saveLocalData();
+	//}
+	//modSettings[ModScope::scopeBuiltin()] = coreMod->saveLocalData();
+	//modSettings[ModScope::scopeBuiltin()]["name"].String() = "Original game files";
+
+	//if(!onlyEssential)
+	//{
+	//	std::fstream file(CResourceHandler::get()->getResourceName(ResourcePath("config/modSettings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc);
+	//	file << modSettings.toString();
+	//}
 }
 
 VCMI_LIB_NAMESPACE_END

+ 8 - 33
lib/modding/CModHandler.h

@@ -12,51 +12,26 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 class CModHandler;
-class CModIdentifier;
-class CModInfo;
-struct CModVersion;
-class JsonNode;
-class IHandlerBase;
-class CIdentifierStorage;
+class ModDescription;
 class CContentHandler;
-struct ModVerificationInfo;
 class ResourcePath;
-class MetaString;
+class ModManager;
+class ISimpleResourceLoader;
 
 using TModID = std::string;
 
 class DLL_LINKAGE CModHandler final : boost::noncopyable
 {
-	std::map <TModID, CModInfo> allMods;
-	std::vector <TModID> activeMods;//active mods, in order in which they were loaded
-	std::unique_ptr<CModInfo> coreMod;
-	mutable std::unique_ptr<MetaString> modLoadErrors;
-
-	bool hasCircularDependency(const TModID & mod, std::set<TModID> currentList = std::set<TModID>()) const;
-
-	/**
-	* 1. Set apart mods with resolved dependencies from mods which have unresolved dependencies
-	* 2. Sort resolved mods using topological algorithm
-	* 3. Log all problem mods and their unresolved dependencies
-	*
-	* @param modsToResolve list of valid mod IDs (checkDependencies returned true - TODO: Clarify it.)
-	* @return a vector of the topologically sorted resolved mods: child nodes (dependent mods) have greater index than parents
-	*/
-	std::vector<TModID> validateAndSortDependencies(std::vector <TModID> modsToResolve) const;
-
-	std::vector<std::string> getModList(const std::string & path) const;
-	void loadMods(const std::string & path, const std::string & parent, const JsonNode & modSettings, const std::vector<TModID> & modsToActivate, bool enableMods);
-	void loadOneMod(std::string modName, const std::string & parent, const JsonNode & modSettings, const std::vector<TModID> & modsToActivate, bool enableMods);
-	void loadTranslation(const TModID & modName);
+	std::unique_ptr<ModManager> modManager;
 
-	CModVersion getModVersion(TModID modName) const;
+	void loadTranslation(const TModID & modName);
+	void checkModFilesystemsConflicts(const std::map<TModID, ISimpleResourceLoader *> & modFilesystems);
 
 public:
-	std::shared_ptr<CContentHandler> content; //(!)Do not serialize FIXME: make private
+	std::shared_ptr<CContentHandler> content; //FIXME: make private
 
 	/// receives list of available mods and trying to load mod.json from all of them
 	void initializeConfig();
-	void loadMods();
 	void loadModFilesystems();
 
 	/// returns ID of mod that provides selected file resource
@@ -82,7 +57,7 @@ public:
 	/// Returns human-readable string that describes errors encounter during mod loading, such as missing dependencies
 	std::string getModLoadErrors() const;
 	
-	const CModInfo & getModInfo(const TModID & modId) const;
+	const ModDescription & getModInfo(const TModID & modId) const;
 
 	/// load content from all available mods
 	void load();

+ 0 - 204
lib/modding/CModInfo.cpp

@@ -1,204 +0,0 @@
-/*
- * CModInfo.cpp, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#include "StdInc.h"
-#include "CModInfo.h"
-
-#include "../texts/CGeneralTextHandler.h"
-#include "../VCMI_Lib.h"
-#include "../filesystem/Filesystem.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-static JsonNode addMeta(JsonNode config, const std::string & meta)
-{
-	config.setModScope(meta);
-	return config;
-}
-
-std::set<TModID> CModInfo::readModList(const JsonNode & input)
-{
-	std::set<TModID> result;
-
-	for (auto const & string : input.convertTo<std::set<std::string>>())
-		result.insert(boost::to_lower_copy(string));
-
-	return result;
-}
-
-CModInfo::CModInfo():
-	explicitlyEnabled(false),
-	implicitlyEnabled(true),
-	validation(PENDING)
-{
-
-}
-
-CModInfo::CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode & config, bool isActive):
-	identifier(identifier),
-	dependencies(readModList(config["depends"])),
-	softDependencies(readModList(config["softDepends"])),
-	conflicts(readModList(config["conflicts"])),
-	explicitlyEnabled(isActive),
-	implicitlyEnabled(true),
-	validation(PENDING),
-	config(addMeta(config, identifier))
-{
-	if (!config["name"].String().empty())
-		verificationInfo.name = config["name"].String();
-	else
-		verificationInfo.name = identifier;
-
-	verificationInfo.version = CModVersion::fromString(config["version"].String());
-	verificationInfo.parent = identifier.substr(0, identifier.find_last_of('.'));
-	if(verificationInfo.parent == identifier)
-		verificationInfo.parent.clear();
-
-	if(!config["compatibility"].isNull())
-	{
-		vcmiCompatibleMin = CModVersion::fromString(config["compatibility"]["min"].String());
-		vcmiCompatibleMax = CModVersion::fromString(config["compatibility"]["max"].String());
-	}
-
-	if (!config["language"].isNull())
-		baseLanguage = config["language"].String();
-	else
-		baseLanguage = "english";
-
-	loadLocalData(local);
-}
-
-JsonNode CModInfo::saveLocalData() const
-{
-	std::ostringstream stream;
-	stream << std::noshowbase << std::hex << std::setw(8) << std::setfill('0') << verificationInfo.checksum;
-
-	JsonNode conf;
-	conf["active"].Bool() = explicitlyEnabled;
-	conf["validated"].Bool() = validation != FAILED;
-	conf["checksum"].String() = stream.str();
-	return conf;
-}
-
-std::string CModInfo::getModDir(const std::string & name)
-{
-	return "MODS/" + boost::algorithm::replace_all_copy(name, ".", "/MODS/");
-}
-
-JsonPath CModInfo::getModFile(const std::string & name)
-{
-	return JsonPath::builtinTODO(getModDir(name) + "/mod.json");
-}
-
-void CModInfo::updateChecksum(ui32 newChecksum)
-{
-	// comment-out next line to force validation of all mods ignoring checksum
-	if (newChecksum != verificationInfo.checksum)
-	{
-		verificationInfo.checksum = newChecksum;
-		validation = PENDING;
-	}
-}
-
-void CModInfo::loadLocalData(const JsonNode & data)
-{
-	bool validated = false;
-	implicitlyEnabled = true;
-	verificationInfo.checksum = 0;
-	if (data.isStruct())
-	{
-		validated = data["validated"].Bool();
-		updateChecksum(strtol(data["checksum"].String().c_str(), nullptr, 16));
-	}
-
-	//check compatibility
-	implicitlyEnabled &= (vcmiCompatibleMin.isNull() || CModVersion::GameVersion().compatible(vcmiCompatibleMin, true, true));
-	implicitlyEnabled &= (vcmiCompatibleMax.isNull() || vcmiCompatibleMax.compatible(CModVersion::GameVersion(), true, true));
-
-	if(!implicitlyEnabled)
-		logGlobal->warn("Mod %s is incompatible with current version of VCMI and cannot be enabled", verificationInfo.name);
-
-	if (config["modType"].String() == "Translation")
-	{
-		if (baseLanguage != CGeneralTextHandler::getPreferredLanguage())
-		{
-			if (identifier.find_last_of('.') == std::string::npos)
-				logGlobal->warn("Translation mod %s was not loaded: language mismatch!", verificationInfo.name);
-			implicitlyEnabled = false;
-		}
-	}
-	if (config["modType"].String() == "Compatibility")
-	{
-		// compatibility mods are always explicitly enabled
-		// however they may be implicitly disabled - if one of their dependencies is missing
-		explicitlyEnabled = true;
-	}
-
-	if (isEnabled())
-		validation = validated ? PASSED : PENDING;
-	else
-		validation = validated ? PASSED : FAILED;
-
-	verificationInfo.impactsGameplay = checkModGameplayAffecting();
-}
-
-bool CModInfo::checkModGameplayAffecting() const
-{
-	if (modGameplayAffecting.has_value())
-		return *modGameplayAffecting;
-
-	static const std::vector<std::string> keysToTest = {
-		"heroClasses",
-		"artifacts",
-		"creatures",
-		"factions",
-		"objects",
-		"heroes",
-		"spells",
-		"skills",
-		"templates",
-		"scripts",
-		"battlefields",
-		"terrains",
-		"rivers",
-		"roads",
-		"obstacles"
-	};
-
-	JsonPath modFileResource(CModInfo::getModFile(identifier));
-
-	if(CResourceHandler::get("initial")->existsResource(modFileResource))
-	{
-		const JsonNode modConfig(modFileResource);
-
-		for(const auto & key : keysToTest)
-		{
-			if (!modConfig[key].isNull())
-			{
-				modGameplayAffecting = true;
-				return *modGameplayAffecting;
-			}
-		}
-	}
-	modGameplayAffecting = false;
-	return *modGameplayAffecting;
-}
-
-const ModVerificationInfo & CModInfo::getVerificationInfo() const
-{
-	assert(!verificationInfo.name.empty());
-	return verificationInfo;
-}
-
-bool CModInfo::isEnabled() const
-{
-	return implicitlyEnabled && explicitlyEnabled;
-}
-
-VCMI_LIB_NAMESPACE_END

+ 0 - 85
lib/modding/CModInfo.h

@@ -1,85 +0,0 @@
-/*
- * CModInfo.h, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#pragma once
-
-#include "../json/JsonNode.h"
-#include "ModVerificationInfo.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class DLL_LINKAGE CModInfo
-{
-	/// cached result of checkModGameplayAffecting() call
-	/// Do not serialize - depends on local mod version, not server/save mod version
-	mutable std::optional<bool> modGameplayAffecting;
-
-	static std::set<TModID> readModList(const JsonNode & input);
-public:
-	enum EValidationStatus
-	{
-		PENDING,
-		FAILED,
-		PASSED
-	};
-	
-	/// identifier, identical to name of folder with mod
-	std::string identifier;
-
-	/// detailed mod description
-	std::string description;
-
-	/// Base language of mod, all mod strings are assumed to be in this language
-	std::string baseLanguage;
-
-	/// vcmi versions compatible with the mod
-	CModVersion vcmiCompatibleMin, vcmiCompatibleMax;
-
-	/// list of mods that should be loaded before this one
-	std::set <TModID> dependencies;
-
-	/// list of mods if they are enabled, should be loaded before this one. this mod will overwrite any conflicting items from its soft dependency mods
-	std::set <TModID> softDependencies;
-
-	/// list of mods that can't be used in the same time as this one
-	std::set <TModID> conflicts;
-
-	EValidationStatus validation;
-
-	JsonNode config;
-
-	CModInfo();
-	CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode & config, bool isActive);
-
-	JsonNode saveLocalData() const;
-	void updateChecksum(ui32 newChecksum);
-
-	bool isEnabled() const;
-
-	static std::string getModDir(const std::string & name);
-	static JsonPath getModFile(const std::string & name);
-
-	/// return true if this mod can affect gameplay, e.g. adds or modifies any game objects
-	bool checkModGameplayAffecting() const;
-	
-	const ModVerificationInfo & getVerificationInfo() const;
-
-private:
-	/// true if mod is enabled by user, e.g. in Launcher UI
-	bool explicitlyEnabled;
-
-	/// true if mod can be loaded - compatible and has no missing deps
-	bool implicitlyEnabled;
-	
-	ModVerificationInfo verificationInfo;
-
-	void loadLocalData(const JsonNode & data);
-};
-
-VCMI_LIB_NAMESPACE_END

+ 38 - 33
lib/modding/ContentTypeHandler.cpp

@@ -11,7 +11,8 @@
 #include "ContentTypeHandler.h"
 
 #include "CModHandler.h"
-#include "CModInfo.h"
+#include "ModDescription.h"
+#include "ModManager.h"
 #include "ModScope.h"
 
 #include "../BattleFieldHandler.h"
@@ -294,39 +295,43 @@ void CContentHandler::afterLoadFinalization()
 	}
 }
 
-void CContentHandler::preloadData(CModInfo & mod)
+void CContentHandler::preloadData(const ModDescription & mod)
 {
-	bool validate = validateMod(mod);
-
-	// print message in format [<8-symbols checksum>] <modname>
-	auto & info = mod.getVerificationInfo();
-	logMod->info("\t\t[%08x]%s", info.checksum, info.name);
-
-	if (validate && mod.identifier != ModScope::scopeBuiltin())
-	{
-		if (!JsonUtils::validate(mod.config, "vcmi:mod", mod.identifier))
-			mod.validation = CModInfo::FAILED;
-	}
-	if (!preloadModData(mod.identifier, mod.config, validate))
-		mod.validation = CModInfo::FAILED;
+	preloadModData(mod.getID(), mod.getConfig(), false);
+
+//	bool validate = validateMod(mod);
+//
+//	// print message in format [<8-symbols checksum>] <modname>
+//	auto & info = mod.getVerificationInfo();
+//	logMod->info("\t\t[%08x]%s", info.checksum, info.name);
+//
+//	if (validate && mod.identifier != ModScope::scopeBuiltin())
+//	{
+//		if (!JsonUtils::validate(mod.config, "vcmi:mod", mod.identifier))
+//			mod.validation = CModInfo::FAILED;
+//	}
+//	if (!preloadModData(mod.identifier, mod.config, validate))
+//		mod.validation = CModInfo::FAILED;
 }
 
-void CContentHandler::load(CModInfo & mod)
+void CContentHandler::load(const ModDescription & mod)
 {
-	bool validate = validateMod(mod);
-
-	if (!loadMod(mod.identifier, validate))
-		mod.validation = CModInfo::FAILED;
-
-	if (validate)
-	{
-		if (mod.validation != CModInfo::FAILED)
-			logMod->info("\t\t[DONE] %s", mod.getVerificationInfo().name);
-		else
-			logMod->error("\t\t[FAIL] %s", mod.getVerificationInfo().name);
-	}
-	else
-		logMod->info("\t\t[SKIP] %s", mod.getVerificationInfo().name);
+	loadMod(mod.getID(), false);
+
+//	bool validate = validateMod(mod);
+//
+//	if (!loadMod(mod.identifier, validate))
+//		mod.validation = CModInfo::FAILED;
+//
+//	if (validate)
+//	{
+//		if (mod.validation != CModInfo::FAILED)
+//			logMod->info("\t\t[DONE] %s", mod.getVerificationInfo().name);
+//		else
+//			logMod->error("\t\t[FAIL] %s", mod.getVerificationInfo().name);
+//	}
+//	else
+//		logMod->info("\t\t[SKIP] %s", mod.getVerificationInfo().name);
 }
 
 const ContentTypeHandler & CContentHandler::operator[](const std::string & name) const
@@ -334,13 +339,13 @@ const ContentTypeHandler & CContentHandler::operator[](const std::string & name)
 	return handlers.at(name);
 }
 
-bool CContentHandler::validateMod(const CModInfo & mod) const
+bool CContentHandler::validateMod(const ModDescription & mod) const
 {
 	if (settings["mods"]["validation"].String() == "full")
 		return true;
 
-	if (mod.validation == CModInfo::PASSED)
-		return false;
+//	if (mod.validation == CModInfo::PASSED)
+//		return false;
 
 	if (settings["mods"]["validation"].String() == "off")
 		return false;

+ 4 - 4
lib/modding/ContentTypeHandler.h

@@ -14,7 +14,7 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 class IHandlerBase;
-class CModInfo;
+class ModDescription;
 
 /// internal type to handle loading of one data type (e.g. artifacts, creatures)
 class DLL_LINKAGE ContentTypeHandler
@@ -58,15 +58,15 @@ class DLL_LINKAGE CContentHandler
 
 	std::map<std::string, ContentTypeHandler> handlers;
 
-	bool validateMod(const CModInfo & mod) const;
+	bool validateMod(const ModDescription & mod) const;
 public:
 	void init();
 
 	/// preloads all data from fileList as data from modName.
-	void preloadData(CModInfo & mod);
+	void preloadData(const ModDescription & mod);
 
 	/// actually loads data in mod
-	void load(CModInfo & mod);
+	void load(const ModDescription & mod);
 
 	void loadCustom();
 

+ 114 - 0
lib/modding/ModDescription.cpp

@@ -0,0 +1,114 @@
+/*
+ * ModDescription.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "ModDescription.h"
+
+#include "CModVersion.h"
+#include "ModVerificationInfo.h"
+
+#include "../json/JsonNode.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+ModDescription::ModDescription(const TModID & fullID, const JsonNode & config)
+	: identifier(fullID)
+	, config(std::make_unique<JsonNode>(config))
+	, dependencies(loadModList(config["depends"]))
+	, softDependencies(loadModList(config["softDepends"]))
+	, conflicts(loadModList(config["conflicts"]))
+{
+	if(getID() != "core")
+		dependencies.insert("core");
+}
+
+ModDescription::~ModDescription() = default;
+
+TModSet ModDescription::loadModList(const JsonNode & configNode) const
+{
+	TModSet result;
+	for(const auto & entry : configNode.Vector())
+		result.insert(boost::algorithm::to_lower_copy(entry.String()));
+	return result;
+}
+
+const TModID & ModDescription::getID() const
+{
+	return identifier;
+}
+
+TModID ModDescription::getParentID() const
+{
+	size_t dotPos = identifier.find_last_of('.');
+
+	if(dotPos == std::string::npos)
+		return {};
+
+	return identifier.substr(0, dotPos);
+}
+
+const TModSet & ModDescription::getDependencies() const
+{
+	return dependencies;
+}
+
+const TModSet & ModDescription::getSoftDependencies() const
+{
+	return softDependencies;
+}
+
+const TModSet & ModDescription::getConflicts() const
+{
+	return conflicts;
+}
+
+const std::string & ModDescription::getBaseLanguage() const
+{
+	static const std::string defaultLanguage = "english";
+
+	return getConfig()["language"].isString() ? getConfig()["language"].String() : defaultLanguage;
+}
+
+const std::string & ModDescription::getName() const
+{
+	return getConfig()["name"].String();
+}
+
+const JsonNode & ModDescription::getFilesystemConfig() const
+{
+	return getConfig()["filesystem"];
+}
+
+const JsonNode & ModDescription::getConfig() const
+{
+	return *config;
+}
+
+CModVersion ModDescription::getVersion() const
+{
+	return CModVersion::fromString(getConfig()["version"].String());
+}
+
+ModVerificationInfo ModDescription::getVerificationInfo() const
+{
+	ModVerificationInfo result;
+	result.name = getName();
+	result.version = getVersion();
+	result.impactsGameplay = affectsGameplay();
+	result.parent = getParentID();
+
+	return result;
+}
+
+bool ModDescription::affectsGameplay() const
+{
+	return false; // TODO
+}
+
+VCMI_LIB_NAMESPACE_END

+ 56 - 0
lib/modding/ModDescription.h

@@ -0,0 +1,56 @@
+/*
+ * ModDescription.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct CModVersion;
+struct ModVerificationInfo;
+class JsonNode;
+
+using TModID = std::string;
+using TModList = std::vector<TModID>;
+using TModSet = std::set<TModID>;
+
+class DLL_LINKAGE ModDescription : boost::noncopyable
+{
+	TModID identifier;
+	TModSet dependencies;
+	TModSet softDependencies;
+	TModSet conflicts;
+
+	std::unique_ptr<JsonNode> config;
+
+	TModSet loadModList(const JsonNode & configNode) const;
+
+public:
+	ModDescription(const TModID & fullID, const JsonNode & config);
+	~ModDescription();
+
+	const TModID & getID() const;
+	TModID getParentID() const;
+
+	const TModSet & getDependencies() const;
+	const TModSet & getSoftDependencies() const;
+	const TModSet & getConflicts() const;
+
+	const std::string & getBaseLanguage() const;
+	const std::string & getName() const;
+
+	const JsonNode & getFilesystemConfig() const;
+	const JsonNode & getConfig() const;
+
+	CModVersion getVersion() const;
+	ModVerificationInfo getVerificationInfo() const;
+
+	bool affectsGameplay() const;
+};
+
+VCMI_LIB_NAMESPACE_END

+ 367 - 0
lib/modding/ModManager.cpp

@@ -0,0 +1,367 @@
+/*
+ * ModManager.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "ModManager.h"
+
+#include "ModDescription.h"
+#include "ModScope.h"
+
+#include "../filesystem/Filesystem.h"
+#include "../json/JsonNode.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+static std::string getModSettingsDirectory(const TModID & modName)
+{
+	std::string result = modName;
+	boost::to_upper(result);
+	boost::algorithm::replace_all(result, ".", "/MODS/");
+	return "MODS/" + result + "/MODS/";
+}
+
+static JsonPath getModDescriptionFile(const TModID & modName)
+{
+	std::string result = modName;
+	boost::to_upper(result);
+	boost::algorithm::replace_all(result, ".", "/MODS/");
+	return JsonPath::builtin("MODS/" + result + "/mod");
+}
+
+ModsState::ModsState()
+{
+	modList.push_back(ModScope::scopeBuiltin());
+
+	std::vector<TModID> testLocations = scanModsDirectory("MODS/");
+
+	while(!testLocations.empty())
+	{
+		std::string target = testLocations.back();
+		testLocations.pop_back();
+		modList.push_back(boost::algorithm::to_lower_copy(target));
+
+		for(const auto & submod : scanModsDirectory(getModSettingsDirectory(target)))
+			testLocations.push_back(target + '.' + submod);
+
+		// TODO: check that this is vcmi mod and not era mod?
+		// TODO: check that mod name is not reserved (ModScope::isScopeReserved(modFullName)))
+	}
+}
+
+TModList ModsState::getAllMods() const
+{
+	return modList;
+}
+
+std::vector<TModID> ModsState::scanModsDirectory(const std::string & modDir) const
+{
+	size_t depth = boost::range::count(modDir, '/');
+
+	const auto & modScanFilter = [&](const ResourcePath & id) -> bool
+	{
+		if(id.getType() != EResType::DIRECTORY)
+			return false;
+		if(!boost::algorithm::starts_with(id.getName(), modDir))
+			return false;
+		if(boost::range::count(id.getName(), '/') != depth)
+			return false;
+		return true;
+	};
+
+	auto list = CResourceHandler::get("initial")->getFilteredFiles(modScanFilter);
+
+	//storage for found mods
+	std::vector<TModID> foundMods;
+	for(const auto & entry : list)
+	{
+		std::string name = entry.getName();
+		name.erase(0, modDir.size()); //Remove path prefix
+
+		if(name.empty())
+			continue;
+
+		if(name.find('.') != std::string::npos)
+			continue;
+
+		if(!CResourceHandler::get("initial")->existsResource(JsonPath::builtin(entry.getName() + "/MOD")))
+			continue;
+
+		foundMods.push_back(name);
+	}
+	return foundMods;
+}
+
+///////////////////////////////////////////////////////////////////////////////
+
+static JsonNode loadModSettings(const JsonPath & path)
+{
+	if(CResourceHandler::get("local")->existsResource(ResourcePath(path)))
+	{
+		return JsonNode(path);
+	}
+	// Probably new install. Create initial configuration
+	CResourceHandler::get("local")->createResource(path.getOriginalName() + ".json");
+	return JsonNode();
+}
+
+ModsPresetState::ModsPresetState()
+{
+	modConfig = loadModSettings(JsonPath::builtin("config/modSettings.json"));
+
+	if(modConfig["presets"].isNull())
+	{
+		modConfig["activePreset"] = JsonNode("default");
+		if(modConfig["activeMods"].isNull())
+			createInitialPreset(); // new install
+		else
+			importInitialPreset(); // 1.5 format import
+
+		saveConfiguration(modConfig);
+	}
+}
+
+void ModsPresetState::saveConfiguration(const JsonNode & modSettings)
+{
+	std::fstream file(CResourceHandler::get()->getResourceName(ResourcePath("config/modSettings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc);
+	file << modSettings.toString();
+}
+
+void ModsPresetState::createInitialPreset()
+{
+	// TODO: scan mods directory for all its content? Probably unnecessary since this looks like new install, but who knows?
+	modConfig["presets"]["default"]["mods"].Vector().push_back(JsonNode("vcmi"));
+}
+
+void ModsPresetState::importInitialPreset()
+{
+	JsonNode preset;
+
+	for(const auto & mod : modConfig["activeMods"].Struct())
+	{
+		if(mod.second["active"].Bool())
+			preset["mods"].Vector().push_back(JsonNode(mod.first));
+
+		for(const auto & submod : mod.second["mods"].Struct())
+			preset["settings"][mod.first][submod.first] = submod.second["active"];
+	}
+	modConfig["presets"]["default"] = preset;
+}
+
+std::vector<TModID> ModsPresetState::getActiveMods() const
+{
+	const std::string & currentPresetName = modConfig["activePreset"].String();
+	const JsonNode & currentPreset = modConfig["presets"][currentPresetName];
+	const JsonNode & modsToActivateJson = currentPreset["mods"];
+	std::vector<TModID> modsToActivate = modsToActivateJson.convertTo<std::vector<TModID>>();
+
+	modsToActivate.push_back(ModScope::scopeBuiltin());
+
+	for(const auto & settings : currentPreset["settings"].Struct())
+	{
+		if(!vstd::contains(modsToActivate, settings.first))
+			continue; // settings for inactive mod
+
+		for(const auto & submod : settings.second.Struct())
+			if(submod.second.Bool())
+				modsToActivate.push_back(settings.first + '.' + submod.first);
+	}
+	return modsToActivate;
+}
+
+ModsStorage::ModsStorage(const std::vector<TModID> & modsToLoad)
+{
+	JsonNode coreModConfig(JsonPath::builtin("config/gameConfig.json"));
+	coreModConfig.setModScope(ModScope::scopeBuiltin());
+	mods.try_emplace(ModScope::scopeBuiltin(), ModScope::scopeBuiltin(), coreModConfig);
+
+	for(auto modID : modsToLoad)
+	{
+		if(ModScope::isScopeReserved(modID))
+		{
+			logMod->error("Can not load mod %s - this name is reserved for internal use!", modID);
+			continue;
+		}
+
+		JsonNode modConfig(getModDescriptionFile(modID));
+		modConfig.setModScope(modID);
+
+		if(modConfig["modType"].isNull())
+		{
+			logMod->error("Can not load mod %s - invalid mod config file!", modID);
+			continue;
+		}
+
+		mods.try_emplace(modID, modID, modConfig);
+	}
+}
+
+const ModDescription & ModsStorage::getMod(const TModID & fullID) const
+{
+	return mods.at(fullID);
+}
+
+ModManager::ModManager()
+	: modsState(std::make_unique<ModsState>())
+	, modsPreset(std::make_unique<ModsPresetState>())
+{
+	std::vector<TModID> desiredModList = modsPreset->getActiveMods();
+	const std::vector<TModID> & installedModList = modsState->getAllMods();
+
+	vstd::erase_if(desiredModList, [&](const TModID & mod){
+		return !vstd::contains(installedModList, mod);
+	});
+
+	modsStorage = std::make_unique<ModsStorage>(desiredModList);
+
+	generateLoadOrder(desiredModList);
+}
+
+ModManager::~ModManager() = default;
+
+const ModDescription & ModManager::getModDescription(const TModID & modID) const
+{
+	return modsStorage->getMod(modID);
+}
+
+bool ModManager::isModActive(const TModID & modID) const
+{
+	return vstd::contains(activeMods, modID);
+}
+
+const TModList & ModManager::getActiveMods() const
+{
+	return activeMods;
+}
+
+void ModManager::generateLoadOrder(std::vector<TModID> modsToResolve)
+{
+	// Topological sort algorithm.
+	boost::range::sort(modsToResolve); // Sort mods per name
+	std::vector<TModID> sortedValidMods; // Vector keeps order of elements (LIFO)
+	sortedValidMods.reserve(modsToResolve.size()); // push_back calls won't cause memory reallocation
+	std::set<TModID> resolvedModIDs; // Use a set for validation for performance reason, but set does not keep order of elements
+	std::set<TModID> notResolvedModIDs(modsToResolve.begin(), modsToResolve.end()); // Use a set for validation for performance reason
+
+	// Mod is resolved if it has no dependencies or all its dependencies are already resolved
+	auto isResolved = [&](const ModDescription & mod) -> bool
+	{
+		if(mod.getDependencies().size() > resolvedModIDs.size())
+			return false;
+
+		for(const TModID & dependency : mod.getDependencies())
+			if(!vstd::contains(resolvedModIDs, dependency))
+				return false;
+
+		for(const TModID & softDependency : mod.getSoftDependencies())
+			if(vstd::contains(notResolvedModIDs, softDependency))
+				return false;
+
+		for(const TModID & conflict : mod.getConflicts())
+			if(vstd::contains(resolvedModIDs, conflict))
+				return false;
+
+		for(const TModID & reverseConflict : resolvedModIDs)
+			if(vstd::contains(modsStorage->getMod(reverseConflict).getConflicts(), mod.getID()))
+				return false;
+
+		return true;
+	};
+
+	while(true)
+	{
+		std::set<TModID> resolvedOnCurrentTreeLevel;
+		for(auto it = modsToResolve.begin(); it != modsToResolve.end();) // One iteration - one level of mods tree
+		{
+			if(isResolved(modsStorage->getMod(*it)))
+			{
+				resolvedOnCurrentTreeLevel.insert(*it); // Not to the resolvedModIDs, so current node children will be resolved on the next iteration
+				sortedValidMods.push_back(*it);
+				it = modsToResolve.erase(it);
+				continue;
+			}
+			it++;
+		}
+		if(!resolvedOnCurrentTreeLevel.empty())
+		{
+			resolvedModIDs.insert(resolvedOnCurrentTreeLevel.begin(), resolvedOnCurrentTreeLevel.end());
+			for(const auto & it : resolvedOnCurrentTreeLevel)
+				notResolvedModIDs.erase(it);
+			continue;
+		}
+		// If there are no valid mods on the current mods tree level, no more mod can be resolved, should be ended.
+		break;
+	}
+
+	activeMods = sortedValidMods;
+	brokenMods = modsToResolve;
+}
+
+//	modLoadErrors = std::make_unique<MetaString>();
+//
+//	auto addErrorMessage = [this](const std::string & textID, const std::string & brokenModID, const std::string & missingModID)
+//	{
+//		modLoadErrors->appendTextID(textID);
+//
+//		if (allMods.count(brokenModID))
+//			modLoadErrors->replaceRawString(allMods.at(brokenModID).getVerificationInfo().name);
+//		else
+//			modLoadErrors->replaceRawString(brokenModID);
+//
+//		if (allMods.count(missingModID))
+//			modLoadErrors->replaceRawString(allMods.at(missingModID).getVerificationInfo().name);
+//		else
+//			modLoadErrors->replaceRawString(missingModID);
+//
+//	};
+//
+//	// Left mods have unresolved dependencies, output all to log.
+//	for(const auto & brokenModID : modsToResolve)
+//	{
+//		const CModInfo & brokenMod = allMods.at(brokenModID);
+//		bool showErrorMessage = false;
+//		for(const TModID & dependency : brokenMod.dependencies)
+//		{
+//			if(!vstd::contains(resolvedModIDs, dependency) && brokenMod.config["modType"].String() != "Compatibility")
+//			{
+//				addErrorMessage("vcmi.server.errors.modNoDependency", brokenModID, dependency);
+//				showErrorMessage = true;
+//			}
+//		}
+//		for(const TModID & conflict : brokenMod.conflicts)
+//		{
+//			if(vstd::contains(resolvedModIDs, conflict))
+//			{
+//				addErrorMessage("vcmi.server.errors.modConflict", brokenModID, conflict);
+//				showErrorMessage = true;
+//			}
+//		}
+//		for(const TModID & reverseConflict : resolvedModIDs)
+//		{
+//			if (vstd::contains(allMods.at(reverseConflict).conflicts, brokenModID))
+//			{
+//				addErrorMessage("vcmi.server.errors.modConflict", brokenModID, reverseConflict);
+//				showErrorMessage = true;
+//			}
+//		}
+//
+//		// some mods may in a (soft) dependency loop.
+//		if(!showErrorMessage && brokenMod.config["modType"].String() != "Compatibility")
+//		{
+//			modLoadErrors->appendTextID("vcmi.server.errors.modDependencyLoop");
+//			if (allMods.count(brokenModID))
+//				modLoadErrors->replaceRawString(allMods.at(brokenModID).getVerificationInfo().name);
+//			else
+//				modLoadErrors->replaceRawString(brokenModID);
+//		}
+//
+//	}
+//	return sortedValidMods;
+//}
+
+VCMI_LIB_NAMESPACE_END

+ 95 - 0
lib/modding/ModManager.h

@@ -0,0 +1,95 @@
+/*
+ * ModManager.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../json/JsonNode.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class JsonNode;
+class ModDescription;
+struct CModVersion;
+
+using TModID = std::string;
+using TModList = std::vector<TModID>;
+using TModSet = std::set<TModID>;
+
+/// Provides interface to access list of locally installed mods
+class ModsState : boost::noncopyable
+{
+	TModList modList;
+
+	TModList scanModsDirectory(const std::string & modDir) const;
+
+public:
+	ModsState();
+
+	TModList getAllMods() const;
+};
+
+/// Provides interface to access or change current mod preset
+class ModsPresetState : boost::noncopyable
+{
+	JsonNode modConfig;
+
+	void saveConfiguration(const JsonNode & config);
+
+	void createInitialPreset();
+	void importInitialPreset();
+
+public:
+	ModsPresetState();
+
+	/// Returns true if mod is active in current preset
+	bool isModActive(const TModID & modName) const;
+
+	void activateModInPreset(const TModID & modName);
+	void dectivateModInAllPresets(const TModID & modName);
+
+	/// Returns list of all mods active in current preset. Mod order is unspecified
+	TModList getActiveMods() const;
+};
+
+/// Provides access to mod properties
+class ModsStorage : boost::noncopyable
+{
+	std::map<TModID, ModDescription> mods;
+
+public:
+	ModsStorage(const TModList & modsToLoad);
+
+	const ModDescription & getMod(const TModID & fullID) const;
+};
+
+/// Provides public interface to access mod state
+class ModManager : boost::noncopyable
+{
+	/// all currently active mods, in their load order
+	TModList activeMods;
+
+	/// Mods from current preset that failed to load due to invalid dependencies
+	TModList brokenMods;
+
+	std::unique_ptr<ModsState> modsState;
+	std::unique_ptr<ModsPresetState> modsPreset;
+	std::unique_ptr<ModsStorage> modsStorage;
+
+	void generateLoadOrder(TModList desiredModList);
+
+public:
+	ModManager();
+	~ModManager();
+
+	const ModDescription & getModDescription(const TModID & modID) const;
+	const TModList & getActiveMods() const;
+	bool isModActive(const TModID & modID) const;
+};
+
+VCMI_LIB_NAMESPACE_END

+ 5 - 5
lib/modding/ModVerificationInfo.cpp

@@ -10,8 +10,8 @@
 #include "StdInc.h"
 #include "ModVerificationInfo.h"
 
-#include "CModInfo.h"
 #include "CModHandler.h"
+#include "ModDescription.h"
 #include "ModIncompatibility.h"
 
 #include "../json/JsonNode.h"
@@ -68,7 +68,7 @@ ModListVerificationStatus ModVerificationInfo::verifyListAgainstLocalMods(const
 		if(modList.count(m))
 			continue;
 
-		if(VLC->modh->getModInfo(m).checkModGameplayAffecting())
+		if(VLC->modh->getModInfo(m).affectsGameplay())
 			result[m] = ModVerificationStatus::EXCESSIVE;
 	}
 
@@ -88,8 +88,8 @@ ModListVerificationStatus ModVerificationInfo::verifyListAgainstLocalMods(const
 			continue;
 		}
 
-		auto & localModInfo = VLC->modh->getModInfo(remoteModId).getVerificationInfo();
-		modAffectsGameplay |= VLC->modh->getModInfo(remoteModId).checkModGameplayAffecting();
+		const auto & localVersion = VLC->modh->getModInfo(remoteModId).getVersion();
+		modAffectsGameplay |= VLC->modh->getModInfo(remoteModId).affectsGameplay();
 
 		// skip it. Such mods should only be present in old saves or if mod changed and no longer affects gameplay
 		if (!modAffectsGameplay)
@@ -101,7 +101,7 @@ ModListVerificationStatus ModVerificationInfo::verifyListAgainstLocalMods(const
 			continue;
 		}
 
-		if(remoteModInfo.version != localModInfo.version)
+		if(remoteModInfo.version != localVersion)
 		{
 			result[remoteModId] = ModVerificationStatus::VERSION_MISMATCH;
 			continue;

+ 1 - 1
mapeditor/mapcontroller.cpp

@@ -23,7 +23,7 @@
 #include "../lib/mapping/CMapEditManager.h"
 #include "../lib/mapping/ObstacleProxy.h"
 #include "../lib/modding/CModHandler.h"
-#include "../lib/modding/CModInfo.h"
+#include "../lib/modding/ModDescription.h"
 #include "../lib/TerrainHandler.h"
 #include "../lib/CSkillHandler.h"
 #include "../lib/spells/CSpellHandler.h"

+ 1 - 2
mapeditor/mapcontroller.h

@@ -13,9 +13,8 @@
 #include "maphandler.h"
 #include "mapview.h"
 
-#include "../lib/modding/CModInfo.h"
-
 VCMI_LIB_NAMESPACE_BEGIN
+struct ModVerificationInfo;
 using ModCompatibilityInfo = std::map<std::string, ModVerificationInfo>;
 class EditorObstaclePlacer;
 VCMI_LIB_NAMESPACE_END

+ 5 - 5
mapeditor/mapsettings/modsettings.cpp

@@ -11,9 +11,9 @@
 #include "modsettings.h"
 #include "ui_modsettings.h"
 #include "../mapcontroller.h"
+#include "../../lib/modding/ModDescription.h"
 #include "../../lib/modding/CModHandler.h"
 #include "../../lib/mapping/CMapService.h"
-#include "../../lib/modding/CModInfo.h"
 
 void traverseNode(QTreeWidgetItem * item, std::function<void(QTreeWidgetItem*)> action)
 {
@@ -45,12 +45,12 @@ void ModSettings::initialize(MapController & c)
 	QSet<QString> modsToProcess;
 	ui->treeMods->blockSignals(true);
 
-	auto createModTreeWidgetItem = [&](QTreeWidgetItem * parent, const CModInfo & modInfo)
+	auto createModTreeWidgetItem = [&](QTreeWidgetItem * parent, const ModDescription & modInfo)
 	{
-		auto item = new QTreeWidgetItem(parent, {QString::fromStdString(modInfo.getVerificationInfo().name), QString::fromStdString(modInfo.getVerificationInfo().version.toString())});
-		item->setData(0, Qt::UserRole, QVariant(QString::fromStdString(modInfo.identifier)));
+		auto item = new QTreeWidgetItem(parent, {QString::fromStdString(modInfo.getName()), QString::fromStdString(modInfo.getVersion().toString())});
+		item->setData(0, Qt::UserRole, QVariant(QString::fromStdString(modInfo.getID())));
 		item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
-		item->setCheckState(0, controller->map()->mods.count(modInfo.identifier) ? Qt::Checked : Qt::Unchecked);
+		item->setCheckState(0, controller->map()->mods.count(modInfo.getID()) ? Qt::Checked : Qt::Unchecked);
 		//set parent check
 		if(parent && item->checkState(0) == Qt::Checked)
 			parent->setCheckState(0, Qt::Checked);

+ 1 - 1
mapeditor/validator.cpp

@@ -16,7 +16,7 @@
 #include "../lib/mapping/CMap.h"
 #include "../lib/mapObjects/MapObjects.h"
 #include "../lib/modding/CModHandler.h"
-#include "../lib/modding/CModInfo.h"
+#include "../lib/modding/ModDescription.h"
 #include "../lib/spells/CSpellHandler.h"
 
 Validator::Validator(const CMap * map, QWidget *parent) :

+ 3 - 2
server/GlobalLobbyProcessor.cpp

@@ -15,7 +15,8 @@
 #include "../lib/json/JsonUtils.h"
 #include "../lib/VCMI_Lib.h"
 #include "../lib/modding/CModHandler.h"
-#include "../lib/modding/CModInfo.h"
+#include "../lib/modding/ModDescription.h"
+#include "../lib/modding/ModVerificationInfo.h"
 
 GlobalLobbyProcessor::GlobalLobbyProcessor(CVCMIServer & owner)
 	: owner(owner)
@@ -161,7 +162,7 @@ JsonNode GlobalLobbyProcessor::getHostModList() const
 
 	for (auto const & modName : VLC->modh->getActiveMods())
 	{
-		if(VLC->modh->getModInfo(modName).checkModGameplayAffecting())
+		if(VLC->modh->getModInfo(modName).affectsGameplay())
 			info[modName] = VLC->modh->getModInfo(modName).getVerificationInfo();
 	}