1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-04 09:42:40 +02:00

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
This commit is contained in:
Ivan Savenko 2024-11-09 20:29:07 +00:00
parent c22471fd91
commit ba9e3dca9d
25 changed files with 864 additions and 827 deletions

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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

View File

@ -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"

View File

@ -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();

View File

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

View File

@ -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;

View File

@ -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())

View File

@ -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
{

View File

@ -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);
const auto & activeMods = modManager->getActiveMods();
coreMod->updateChecksum(calculateModChecksum(ModScope::scopeBuiltin(), CResourceHandler::get(ModScope::scopeBuiltin())));
std::map<TModID, ISimpleResourceLoader *> modFilesystems;
std::map<std::string, ISimpleResourceLoader *> modFilesystems;
for(const TModID & modName : activeMods)
modFilesystems[modName] = genModFilesystem(modName, getModInfo(modName).getFilesystemConfig());
for(std::string & modName : activeMods)
modFilesystems[modName] = genModFilesystem(modName, allMods[modName].config);
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(std::string & leftModName : activeMods)
for(const auto & [leftName, leftFilesystem] : modFilesystems)
{
for(std::string & rightModName : activeMods)
for(const auto & [rightName, rightFilesystem] : modFilesystems)
{
if (leftModName == rightModName)
if (leftName == rightName)
continue;
if (getModDependencies(leftModName).count(rightModName) || getModDependencies(rightModName).count(leftModName))
if (getModDependencies(leftName).count(rightName) || getModDependencies(rightName).count(leftName))
continue;
if (getModSoftDependencies(leftModName).count(rightModName) || getModSoftDependencies(rightModName).count(leftModName))
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;};
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());
}
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());
isModFound = modManager->isModActive(modId);
if (isModFound)
return it->second.dependencies;
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/");
//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";
// 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();
}
//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

View File

@ -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;
std::unique_ptr<ModManager> modManager;
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);
CModVersion getModVersion(TModID modName) const;
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();

View File

@ -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

View File

@ -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

View File

@ -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);
preloadModData(mod.getID(), mod.getConfig(), false);
// 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;
// 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);
loadMod(mod.getID(), false);
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);
// 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;

View File

@ -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();

View File

@ -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

View File

@ -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
lib/modding/ModManager.cpp Normal file
View File

@ -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
lib/modding/ModManager.h Normal file
View File

@ -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

View File

@ -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;

View File

@ -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"

View File

@ -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

View File

@ -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);

View File

@ -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) :

View File

@ -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();
}