1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-01-28 03:57:02 +02:00
vcmi/lib/modding/CModHandler.cpp
Ivan Savenko 6056d385ed Always load json configs from mod that references it
This should fix rather common problem with mods, where two unrelated mods
accidentally use same file name for a config file, leading to very unclear
conflict since this result in a file override.

Now all config files referenced in mod.json are loaded specifically from
filesystem of mod that referenced it. In other words, it is no longer
possible for one mod to override config from another mod.

As a side effect, this allows mods to use shorter directory layout, e.g.
`config/modName/xxx.json` can now be safely replaced with `config/
xxx.json` without fear of broken mod if there is another mod with same
path to config. Similarly, now all mods can use `config/translation/
language.json` scheme for translation files

Since this is no longer a problem, I've also simplified directory layout
of our built-in 'vcmi' mod, by moving all files from `config/vcmi`
directory directly to `config` directory.

- Overrides for miscellaneous configs like mainmenu.json should works as
before
- Images / animations (png's or def's) work as before (and may still
result in confict)
- Rebalance mods work as before and can modify another mod via standard
`modName:objectName` syntax
2024-10-31 14:49:11 +00:00

603 lines
19 KiB
C++

/*
* CModHandler.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 "CModHandler.h"
#include "CModInfo.h"
#include "ModScope.h"
#include "ContentTypeHandler.h"
#include "IdentifierStorage.h"
#include "ModIncompatibility.h"
#include "../CCreatureHandler.h"
#include "../CConfigHandler.h"
#include "../CStopWatch.h"
#include "../GameSettings.h"
#include "../ScriptHandler.h"
#include "../constants/StringConstants.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>())
{
}
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, bool enableMods)
{
for(const std::string & modName : getModList(path))
loadOneMod(modName, parent, modSettings, enableMods);
}
void CModHandler::loadOneMod(std::string modName, const std::string & parent, const JsonNode & modSettings, 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)))
{
CModInfo mod(modFullName, modSettings[modName], JsonNode(CModInfo::getModFile(modFullName)));
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"], enableMods && mod.isEnabled());
}
}
void CModHandler::loadMods()
{
JsonNode modConfig;
modConfig = loadModSettings(JsonPath::builtin("config/modSettings.json"));
loadMods("", "", modConfig["activeMods"], true);
coreMod = std::make_unique<CModInfo>(ModScope::scopeBuiltin(), modConfig[ModScope::scopeBuiltin()], JsonNode(JsonPath::builtin("config/gameConfig.json")));
}
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;
}
std::vector<std::string> CModHandler::getActiveMods() const
{
return activeMods;
}
std::string CModHandler::getModLoadErrors() const
{
return modLoadErrors->toString();
}
const CModInfo & CModHandler::getModInfo(const TModID & modId) const
{
return allMods.at(modId);
}
static JsonNode genDefaultFS()
{
// default FS config for mods: directory "Content" that acts as H3 root directory
JsonNode defaultFS;
defaultFS[""].Vector().resize(2);
defaultFS[""].Vector()[0]["type"].String() = "zip";
defaultFS[""].Vector()[0]["path"].String() = "/Content.zip";
defaultFS[""].Vector()[1]["type"].String() = "dir";
defaultFS[""].Vector()[1]["path"].String() = "/Content";
return defaultFS;
}
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"]);
else
return CResourceHandler::createFileSystem(CModInfo::getModDir(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();
}
void CModHandler::loadModFilesystems()
{
CGeneralTextHandler::detectInstallParameters();
activeMods = validateAndSortDependencies(activeMods);
coreMod->updateChecksum(calculateModChecksum(ModScope::scopeBuiltin(), CResourceHandler::get(ModScope::scopeBuiltin())));
std::map<std::string, ISimpleResourceLoader *> modFilesystems;
for(std::string & modName : activeMods)
modFilesystems[modName] = genModFilesystem(modName, allMods[modName].config);
for(std::string & modName : activeMods)
CResourceHandler::addFilesystem("data", modName, modFilesystems[modName]);
if (settings["mods"]["validation"].String() == "full")
{
for(std::string & leftModName : activeMods)
{
for(std::string & rightModName : activeMods)
{
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());
}
}
}
}
}
TModID CModHandler::findResourceOrigin(const ResourcePath & name) const
{
try
{
for(const auto & modID : boost::adaptors::reverse(activeMods))
{
if(CResourceHandler::get(modID)->existsResource(name))
return modID;
}
if(CResourceHandler::get("core")->existsResource(name))
return "core";
if(CResourceHandler::get("mapEditor")->existsResource(name))
return "core"; // Workaround for loading maps via map editor
}
catch( const std::out_of_range & e)
{
// no-op
}
throw std::runtime_error("Resource with name " + name.getName() + " and type " + EResTypeHelper::getEResTypeAsString(name.getType()) + " wasn't found.");
}
std::string CModHandler::findResourceLanguage(const ResourcePath & name) const
{
std::string modName = findResourceOrigin(name);
std::string modLanguage = getModLanguage(modName);
return modLanguage;
}
std::string CModHandler::findResourceEncoding(const ResourcePath & resource) const
{
std::string modName = findResourceOrigin(resource);
std::string modLanguage = findResourceLanguage(resource);
bool potentiallyUserMadeContent = resource.getType() == EResType::MAP || resource.getType() == EResType::CAMPAIGN;
if (potentiallyUserMadeContent && modName == ModScope::scopeBuiltin() && modLanguage == "english")
{
// this might be a map or campaign that player downloaded manually and placed in Maps/ directory
// in this case, this file may be in user-preferred language, and not in same language as the rest of H3 data
// however at the moment we have no way to detect that for sure - file can be either in English or in user-preferred language
// but since all known H3 encodings (Win125X or GBK) are supersets of ASCII, we can safely load English data using encoding of user-preferred language
std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage();
std::string fileEncoding = Languages::getLanguageOptions(modLanguage).encoding;
return fileEncoding;
}
else
{
std::string fileEncoding = Languages::getLanguageOptions(modLanguage).encoding;
return fileEncoding;
}
}
std::string CModHandler::getModLanguage(const TModID& modId) const
{
if(modId == "core")
return VLC->generaltexth->getInstalledLanguage();
if(modId == "map")
return VLC->generaltexth->getPreferredLanguage();
return allMods.at(modId).baseLanguage;
}
std::set<TModID> CModHandler::getModDependencies(const TModID & modId) const
{
bool isModFound;
return getModDependencies(modId, isModFound);
}
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;
logMod->error("Mod not found: '%s'", modId);
return {};
}
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 {};
}
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++;
}
return softDependencies;
}
void CModHandler::initializeConfig()
{
VLC->settingsHandler->loadBase(JsonUtils::assembleFromFiles(coreMod->config["settings"]));
for(const TModID & modName : activeMods)
{
const auto & mod = allMods[modName];
if (!mod.config["settings"].isNull())
VLC->settingsHandler->loadBase(mod.config["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];
std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage();
std::string modBaseLanguage = allMods[modName].baseLanguage;
JsonNode baseTranslation = JsonUtils::assembleFromFiles(mod.config["translations"]);
JsonNode extraTranslation = JsonUtils::assembleFromFiles(mod.config[preferredLanguage]["translations"]);
VLC->generaltexth->loadTranslationOverrides(modName, modBaseLanguage, baseTranslation);
VLC->generaltexth->loadTranslationOverrides(modName, preferredLanguage, extraTranslation);
}
void CModHandler::load()
{
CStopWatch totalTime;
CStopWatch timer;
logMod->info("\tInitializing content handler: %d ms", timer.getDiff());
content->init();
for(const TModID & modName : activeMods)
{
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());
content->load(*coreMod);
for(const TModID & modName : activeMods)
content->load(allMods[modName]);
#if SCRIPTING_ENABLED
VLC->scriptHandler->performRegistration(VLC);//todo: this should be done before any other handlers load
#endif
content->loadCustom();
for(const TModID & modName : activeMods)
loadTranslation(modName);
logMod->info("\tLoading mod data: %d ms", timer.getDiff());
VLC->creh->loadCrExpMod();
VLC->identifiersHandler->finalize();
logMod->info("\tResolving identifiers: %d ms", timer.getDiff());
content->afterLoadFinalization();
logMod->info("\tHandlers post-load finalization: %d ms ", timer.getDiff());
logMod->info("\tAll game content loaded in %d ms", totalTime.getDiff());
}
void CModHandler::afterLoad(bool onlyEssential)
{
JsonNode modSettings;
for (auto & modEntry : allMods)
{
std::string pointer = "/" + boost::algorithm::replace_all_copy(modEntry.first, ".", "/mods/");
modSettings["activeMods"].resolvePointer(pointer) = modEntry.second.saveLocalData();
}
modSettings[ModScope::scopeBuiltin()] = coreMod->saveLocalData();
modSettings[ModScope::scopeBuiltin()]["name"].String() = "Original game files";
if(!onlyEssential)
{
std::fstream file(CResourceHandler::get()->getResourceName(ResourcePath("config/modSettings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc);
file << modSettings.toString();
}
}
VCMI_LIB_NAMESPACE_END