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:
parent
c22471fd91
commit
ba9e3dca9d
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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();
|
||||
|
@ -212,6 +212,7 @@ ISimpleResourceLoader * CResourceHandler::get()
|
||||
|
||||
ISimpleResourceLoader * CResourceHandler::get(const std::string & identifier)
|
||||
{
|
||||
assert(knownLoaders.count(identifier));
|
||||
return knownLoaders.at(identifier);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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(const auto & [leftName, leftFilesystem] : modFilesystems)
|
||||
{
|
||||
for(std::string & leftModName : activeMods)
|
||||
for(const auto & [rightName, rightFilesystem] : modFilesystems)
|
||||
{
|
||||
for(std::string & rightModName : activeMods)
|
||||
if (leftName == rightName)
|
||||
continue;
|
||||
|
||||
if (getModDependencies(leftName).count(rightName) || getModDependencies(rightName).count(leftName))
|
||||
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;};
|
||||
|
||||
std::unordered_set<ResourcePath> leftResources = leftFilesystem->getFilteredFiles(filter);
|
||||
std::unordered_set<ResourcePath> rightResources = rightFilesystem->getFilteredFiles(filter);
|
||||
|
||||
for (auto const & leftFile : leftResources)
|
||||
{
|
||||
if (leftModName == rightModName)
|
||||
continue;
|
||||
|
||||
if (getModDependencies(leftModName).count(rightModName) || getModDependencies(rightModName).count(leftModName))
|
||||
continue;
|
||||
|
||||
if (getModSoftDependencies(leftModName).count(rightModName) || getModSoftDependencies(rightModName).count(leftModName))
|
||||
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);
|
||||
|
||||
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());
|
||||
}
|
||||
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/");
|
||||
//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
|
||||
|
@ -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();
|
||||
|
@ -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
|
@ -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
|
@ -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;
|
||||
|
@ -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
lib/modding/ModDescription.cpp
Normal file
114
lib/modding/ModDescription.cpp
Normal 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
|
56
lib/modding/ModDescription.h
Normal file
56
lib/modding/ModDescription.h
Normal 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
367
lib/modding/ModManager.cpp
Normal 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
95
lib/modding/ModManager.h
Normal 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
|
@ -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;
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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) :
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user