From d849e53499f35f50654ce3a3cfd34a50dab2bd36 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 6 Oct 2024 15:54:30 +0000 Subject: [PATCH] Implement detection of mod compatibility patches --- lib/json/JsonUtils.cpp | 14 ++++----- lib/json/JsonUtils.h | 6 ++-- lib/modding/CModHandler.cpp | 10 +++++-- lib/modding/CModHandler.h | 1 + lib/modding/ContentTypeHandler.cpp | 46 ++++++++++++++++++++++++++---- lib/modding/ContentTypeHandler.h | 4 ++- 6 files changed, 62 insertions(+), 19 deletions(-) diff --git a/lib/json/JsonUtils.cpp b/lib/json/JsonUtils.cpp index 2e0fdd829..215a9bc6d 100644 --- a/lib/json/JsonUtils.cpp +++ b/lib/json/JsonUtils.cpp @@ -275,11 +275,8 @@ JsonNode JsonUtils::assembleFromFiles(const std::string & filename) return result; } -void JsonUtils::detectConflicts(const JsonNode & left, const JsonNode & right, const std::string & entityName, const std::string & keyName) +void JsonUtils::detectConflicts(JsonNode & result, const JsonNode & left, const JsonNode & right, const std::string & keyName) { - if (left == right) - return; - switch (left.getType()) { case JsonNode::JsonType::DATA_NULL: @@ -289,16 +286,15 @@ void JsonUtils::detectConflicts(const JsonNode & left, const JsonNode & right, c case JsonNode::JsonType::DATA_STRING: case JsonNode::JsonType::DATA_VECTOR: // NOTE: comparing vectors as whole - since merge will overwrite it in its entirety { - logMod->warn("Potential confict detected between '%s' and '%s' in object '%s'", left.getModScope(), right.getModScope(), entityName); - logMod->warn("Mod '%s' - value %s set to '%s'", left.getModScope(), keyName, left.toCompactString()); - logMod->warn("Mod '%s' - value %s set to '%s'", right.getModScope(), keyName, right.toCompactString()); + result[keyName][left.getModScope()] = left; + result[keyName][right.getModScope()] = right; return; } case JsonNode::JsonType::DATA_STRUCT: { - for(auto & node : left.Struct()) + for(const auto & node : left.Struct()) if (!right[node.first].isNull()) - detectConflicts(node.second, right[node.first], entityName, keyName + "/" + node.first); + detectConflicts(result, node.second, right[node.first], keyName + "/" + node.first); } } } diff --git a/lib/json/JsonUtils.h b/lib/json/JsonUtils.h index bf14ce2d6..5acab9fa0 100644 --- a/lib/json/JsonUtils.h +++ b/lib/json/JsonUtils.h @@ -74,8 +74,10 @@ namespace JsonUtils DLL_LINKAGE const JsonNode & getSchema(const std::string & URI); /// detects potential conflicts - json entries present in both nodes - /// any error messages will be printed to error log - DLL_LINKAGE void detectConflicts(const JsonNode & left, const JsonNode & right, const std::string & entityName, const std::string & keyName); + /// returns JsonNode that contains list of conflicting keys + /// For each conflict - list of conflicting mods and list of conflicting json values + /// result[pathToKey][modID] -> node that was conflicting + DLL_LINKAGE void detectConflicts(JsonNode & result, const JsonNode & left, const JsonNode & right, const std::string & keyName); } VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index a028c8da4..5e0e4c8d2 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -392,6 +392,12 @@ std::string CModHandler::getModLanguage(const TModID& modId) const return allMods.at(modId).baseLanguage; } +std::set CModHandler::getModDependencies(const TModID & modId) const +{ + bool isModFound; + return getModDependencies(modId, isModFound); +} + std::set CModHandler::getModDependencies(const TModID & modId, bool & isModFound) const { auto it = allMods.find(modId); @@ -499,8 +505,8 @@ void CModHandler::load() content->loadCustom(); - for(const TModID & modName : activeMods) - loadTranslation(modName); +// for(const TModID & modName : activeMods) +// loadTranslation(modName); #if 0 for(const TModID & modName : activeMods) diff --git a/lib/modding/CModHandler.h b/lib/modding/CModHandler.h index f88e1fe26..89b1554aa 100644 --- a/lib/modding/CModHandler.h +++ b/lib/modding/CModHandler.h @@ -66,6 +66,7 @@ public: std::string getModLanguage(const TModID & modId) const; + std::set getModDependencies(const TModID & modId) const; std::set getModDependencies(const TModID & modId, bool & isModFound) const; /// returns list of all (active) mods diff --git a/lib/modding/ContentTypeHandler.cpp b/lib/modding/ContentTypeHandler.cpp index 43ece3409..60289ca6e 100644 --- a/lib/modding/ContentTypeHandler.cpp +++ b/lib/modding/ContentTypeHandler.cpp @@ -39,9 +39,9 @@ VCMI_LIB_NAMESPACE_BEGIN -ContentTypeHandler::ContentTypeHandler(IHandlerBase * handler, const std::string & objectName): +ContentTypeHandler::ContentTypeHandler(IHandlerBase * handler, const std::string & entityName): handler(handler), - objectName(objectName), + entityName(entityName), originalData(handler->loadLegacyData()) { for(auto & node : originalData) @@ -80,7 +80,7 @@ bool ContentTypeHandler::preloadModData(const std::string & modName, const std:: JsonNode & remoteConf = modData[remoteName].patches[objectName]; if (!remoteConf.isNull()) - JsonUtils::detectConflicts(remoteConf, entry.second, objectName, ""); + JsonUtils::detectConflicts(conflictList, remoteConf, entry.second, objectName); JsonUtils::merge(remoteConf, entry.second); } @@ -96,7 +96,7 @@ bool ContentTypeHandler::loadMod(const std::string & modName, bool validate) auto performValidate = [&,this](JsonNode & data, const std::string & name){ handler->beforeValidate(data); if (validate) - result &= JsonUtils::validate(data, "vcmi:" + objectName, name); + result &= JsonUtils::validate(data, "vcmi:" + entityName, name); }; // apply patches @@ -116,7 +116,7 @@ bool ContentTypeHandler::loadMod(const std::string & modName, bool validate) // - another mod attempts to add object into this mod (technically can be supported, but might lead to weird edge cases) // - another mod attempts to edit object from this mod that no longer exist - DANGER since such patch likely has very incomplete data // so emit warning and skip such case - logMod->warn("Mod '%s' attempts to edit object '%s' of type '%s' from mod '%s' but no such object exist!", data.getModScope(), name, objectName, modName); + logMod->warn("Mod '%s' attempts to edit object '%s' of type '%s' from mod '%s' but no such object exist!", data.getModScope(), name, entityName, modName); continue; } @@ -189,6 +189,42 @@ void ContentTypeHandler::afterLoadFinalization() } } + for (const auto& [conflictPath, conflictModData] : conflictList.Struct()) + { + std::set conflictingMods; + std::set resolvedConflicts; + + for (auto const & conflictModData : conflictModData.Struct()) + conflictingMods.insert(conflictModData.first); + + for (auto const & modID : conflictingMods) + resolvedConflicts.merge(VLC->modh->getModDependencies(modID)); + + vstd::erase_if(conflictingMods, [&resolvedConflicts](const std::string & entry){ return resolvedConflicts.count(entry);}); + + if (conflictingMods.size() < 2) + continue; // all conflicts were resolved - either via compatibility patch (mod that depends on 2 conflicting mods) or simple mod that depends on another one + + bool allEqual = true; + + for (auto const & modID : conflictingMods) + { + if (conflictModData[modID] != conflictModData[*conflictingMods.begin()]) + { + allEqual = false; + break; + } + } + + if (allEqual) + continue; // conflict still present, but all mods use the same value for conflicting entry - permit it + + logMod->warn("Potential confict in '%s'", conflictPath); + + for (auto const & modID : conflictingMods) + logMod->warn("Mod '%s' - value set to %s", modID, conflictModData[modID].toCompactString()); + } + handler->afterLoadFinalization(); } diff --git a/lib/modding/ContentTypeHandler.h b/lib/modding/ContentTypeHandler.h index 7093c12d5..6c3f553c5 100644 --- a/lib/modding/ContentTypeHandler.h +++ b/lib/modding/ContentTypeHandler.h @@ -19,6 +19,8 @@ class CModInfo; /// internal type to handle loading of one data type (e.g. artifacts, creatures) class DLL_LINKAGE ContentTypeHandler { + JsonNode conflictList; + public: struct ModInfo { @@ -29,7 +31,7 @@ public: }; /// handler to which all data will be loaded IHandlerBase * handler; - std::string objectName; + std::string entityName; /// contains all loaded H3 data std::vector originalData;