diff --git a/config/schemas/settings.json b/config/schemas/settings.json index da312e19d..276903873 100644 --- a/config/schemas/settings.json +++ b/config/schemas/settings.json @@ -3,7 +3,7 @@ { "type" : "object", "$schema" : "http://json-schema.org/draft-04/schema", - "required" : [ "general", "video", "adventure", "battle", "input", "server", "logging", "launcher", "lobby", "gameTweaks" ], + "required" : [ "general", "video", "adventure", "battle", "input", "server", "logging", "launcher", "lobby", "gameTweaks", "mods" ], "definitions" : { "logLevelEnum" : { "type" : "string", @@ -149,6 +149,23 @@ } } }, + + "mods" : { + "type" : "object", + "additionalProperties" : false, + "default" : {}, + "required" : [ + "validation" + ], + "properties" : { + "validation" : { + "type" : "string", + "enum" : [ "off", "basic", "full" ], + "default" : "basic" + } + } + }, + "video" : { "type" : "object", "additionalProperties" : false, diff --git a/launcher/settingsView/csettingsview_moc.cpp b/launcher/settingsView/csettingsview_moc.cpp index fd897d1f5..91788e7b6 100644 --- a/launcher/settingsView/csettingsview_moc.cpp +++ b/launcher/settingsView/csettingsview_moc.cpp @@ -182,6 +182,13 @@ void CSettingsView::loadSettings() else ui->buttonFontScalable->setChecked(true); + if (settings["mods"]["validation"].String() == "off") + ui->buttonValidationOff->setChecked(true); + else if (settings["mods"]["validation"].String() == "basic") + ui->buttonValidationBasic->setChecked(true); + else + ui->buttonValidationFull->setChecked(true); + loadToggleButtonSettings(); } @@ -791,3 +798,21 @@ void CSettingsView::on_buttonFontOriginal_clicked(bool checked) Settings node = settings.write["video"]["fontsType"]; node->String() = "original"; } + +void CSettingsView::on_buttonValidationOff_clicked(bool checked) +{ + Settings node = settings.write["mods"]["validation"]; + node->String() = "off"; +} + +void CSettingsView::on_buttonValidationBasic_clicked(bool checked) +{ + Settings node = settings.write["mods"]["validation"]; + node->String() = "basic"; +} + +void CSettingsView::on_buttonValidationFull_clicked(bool checked) +{ + Settings node = settings.write["mods"]["validation"]; + node->String() = "full"; +} diff --git a/launcher/settingsView/csettingsview_moc.h b/launcher/settingsView/csettingsview_moc.h index 1e76f6f2e..512762d0c 100644 --- a/launcher/settingsView/csettingsview_moc.h +++ b/launcher/settingsView/csettingsview_moc.h @@ -83,19 +83,20 @@ private slots: void on_sliderToleranceDistanceController_valueChanged(int value); void on_lineEditGameLobbyHost_textChanged(const QString &arg1); void on_spinBoxNetworkPortLobby_valueChanged(int arg1); - void on_sliderControllerSticksAcceleration_valueChanged(int value); - void on_sliderControllerSticksSensitivity_valueChanged(int value); - - //void on_buttonTtfFont_toggled(bool value); - void on_sliderScalingFont_valueChanged(int value); - void on_buttonFontAuto_clicked(bool checked); void on_buttonFontScalable_clicked(bool checked); void on_buttonFontOriginal_clicked(bool checked); + + void on_buttonValidationOff_clicked(bool checked); + + void on_buttonValidationBasic_clicked(bool checked); + + void on_buttonValidationFull_clicked(bool checked); + private: Ui::CSettingsView * ui; diff --git a/launcher/settingsView/csettingsview_moc.ui b/launcher/settingsView/csettingsview_moc.ui index 578fd55b4..c0082edd8 100644 --- a/launcher/settingsView/csettingsview_moc.ui +++ b/launcher/settingsView/csettingsview_moc.ui @@ -47,88 +47,19 @@ 0 - 0 + -800 729 - 1449 + 1506 - - - - false - - - BattleAI - - - - BattleAI - - - - - StupidAI - - - - - - - - - 0 - 0 - - + + - - true - - - - - Use Relative Pointer Mode - - - - - - - 25 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 5 - - - - - - - Additional repository - - - - - - - Music Volume - - - - - - @@ -157,374 +88,6 @@ - - - - Long Touch Duration - - - - - - - false - - - BattleAI - - - - BattleAI - - - - - StupidAI - - - - - - - - - true - - - - Video - - - 5 - - - - - - - - true - - - - General - - - 5 - - - - - - - Heroes III Translation - - - - - - - Show Tutorial again - - - - - - - Online Lobby port - - - - - - - Relative Pointer Speed - - - - - - - Touch Tap Tolerance - - - - - - - Select display mode for game - -Windowed - game will run inside a window that covers part of your screen - -Borderless Windowed Mode - game will run in a window that covers entirely of your screen, using same resolution as your screen. - -Fullscreen Exclusive Mode - game will cover entirety of your screen and will use selected resolution. - - - 0 - - - - Windowed - - - - - Borderless fullscreen - - - - - Exclusive fullscreen - - - - - - - - 500 - - - 2000 - - - 250 - - - 250 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 250 - - - - - - - Neutral AI in battles - - - - - - - - true - - - - Input - Mouse - - - 5 - - - - - - - - 0 - 0 - - - - Automatic - - - true - - - true - - - buttonGroup - - - - - - - Adventure Map Enemies - - - - - - - VCAI - - - - VCAI - - - - - Nullkiller - - - - - - - - 0 - - - 50 - - - 1 - - - 10 - - - 0 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 10 - - - - - - - Autosave - - - - - - - Renderer - - - - - - - Autosave limit (0 = off) - - - - - - - Default repository - - - - - - - 100 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 10 - - - - - - - Sticks Sensitivity - - - - - - - - true - - - - Audio - - - 5 - - - - - - - - - - Downscaling Filter - - - - - - - 1024 - - - 65535 - - - 3030 - - - - - - - 0 - - - 50 - - - 1 - - - 10 - - - 0 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 10 - - - - - - - Ignore SSL errors - - - @@ -532,120 +95,6 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - Refresh now - - - - - - - true - - - - 0 - 0 - - - - - - - true - - - false - - - - - - - VSync - - - - - - - Resolution - - - - - - - VCMI Language - - - - - - - Interface Scaling - - - - - - - - - - - - - - - - - Online Lobby address - - - - - - - VCAI - - - - VCAI - - - - - Nullkiller - - - - - - - - - 0 - 0 - - - - - - - true - - - - - - - - - - @@ -674,38 +123,37 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + + + false + + + BattleAI + + + + BattleAI + + + + + StupidAI + + + + + + - Autosave prefix + Additional repository - - + + - Reserved screen area - - - - - - - Sound Volume - - - - - - - Font Scaling (experimental) - - - - - - - Framerate Limit + Ignore SSL errors @@ -728,8 +176,130 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + + + VCMI Language + + + + + + + + 0 + 0 + + + + Automatic + + + true + + + true + + + buttonGroupFonts + + + + + + + + true + + + + Video + + + 5 + + + + + + + Show intro + + + + + + + Heroes III Translation + + + + + + + 0 + + + 50 + + + 1 + + + 10 + + + 0 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 10 + + + + + + + + 0 + 0 + + + + Scalable + + + true + + + true + + + buttonGroupFonts + + + + + + + Touch Tap Tolerance + + + + + + + + + + + + 100 @@ -744,56 +314,13 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - - Automatic - - - - - None - - - - - xBRZ x2 - - - - - xBRZ x3 - - - - - xBRZ x4 - - - + + - - + + - Autocombat AI in battles - - - - - - - Use scalable fonts - - - - - - - - - - true + Renderer @@ -812,87 +339,24 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + - Fullscreen + Neutral AI in battles - - - - 500 - - - 2500 - - - 25 - - - 250 - - - 500 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 250 - - - - - + + - Enemy AI in battles + VSync - - + + - Sticks Acceleration - - - - - - - 100 - - - 300 - - - Qt::Horizontal - - - QSlider::TicksAbove - - - 25 - - - - - - - - 0 - 0 - - - - - - - true + Framerate Limit @@ -903,8 +367,61 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + + + + true + + + + Input - Touchscreen + + + 5 + + + + + + + 0 + + + 50 + + + 1 + + + 10 + + + 0 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 10 + + + + + + + Relative Pointer Speed + + + + + + + + true @@ -925,29 +442,6 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - - 0 - 0 - - - - - - - true - - - - - - - - - - @@ -964,33 +458,20 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + - Adventure Map Allies + Long Touch Duration - - - - Haptic Feedback - - + + - - - - % - - - 50 - - - 400 - - - 10 + + + + Check on startup @@ -1013,72 +494,6 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - Mouse Click Tolerance - - - - - - - BattleAI - - - - BattleAI - - - - - StupidAI - - - - - - - - true - - - - 0 - 0 - - - - - - - true - - - false - - - - - - - Reset - - - - - - - - - - Network port - - - - - - @@ -1094,15 +509,8 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - Display index - - - - - + + 0 @@ -1117,128 +525,6 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - - - - true - - - - Network - - - 5 - - - - - - - - 0 - 0 - - - - Original - - - true - - - true - - - buttonGroup - - - - - - - - - - - - - - Show intro - - - - - - - - 0 - 0 - - - - - - - true - - - - - - - 1024 - - - 65535 - - - 3030 - - - - - - - - true - - - - Input - Touchscreen - - - 5 - - - - - - - 20 - - - 1000 - - - 10 - - - - - - - Controller Click Tolerance - - - - - - - Check on startup - - - @@ -1270,15 +556,53 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - - + + - Software Cursor + Downscaling Filter - - + + + + Select display mode for game + +Windowed - game will run inside a window that covers part of your screen + +Borderless Windowed Mode - game will run in a window that covers entirely of your screen, using same resolution as your screen. + +Fullscreen Exclusive Mode - game will cover entirety of your screen and will use selected resolution. + + + 0 + + + + Windowed + + + + + Borderless fullscreen + + + + + Exclusive fullscreen + + + + + + + + Use scalable fonts + + + + + 0 @@ -1286,7 +610,7 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - Scalable + Original true @@ -1295,13 +619,786 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use true - buttonGroup + buttonGroupFonts + + + + + + + + Automatic + + + + + None + + + + + xBRZ x2 + + + + + xBRZ x3 + + + + + xBRZ x4 + + + + + + + + Online Lobby port + + + + + + + + 0 + 0 + + + + + + + true + + + + + + + VCAI + + + + VCAI + + + + + Nullkiller + + + + + + + + + true + + + + Network + + + 5 + + + + + + + Refresh now + + + + + + + 500 + + + 2500 + + + 25 + + + 250 + + + 500 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 250 + + + + + + + Display index + + + + + + + 500 + + + 2000 + + + 250 + + + 250 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 250 + + + + + + + false + + + BattleAI + + + + BattleAI + + + + + StupidAI + + + + + + + + Adventure Map Enemies + + + + + + + + + + true + + + + + + + Reset + + + + + + + Show Tutorial again + + + + + + + Fullscreen + + + + + + + Use Relative Pointer Mode + + + + + + + + + + + + + + Sticks Acceleration + + + + + + + Autosave limit (0 = off) + + + + + + + Mouse Click Tolerance + + + + + + + + true + + + + General + + + 5 + + + + + + + Online Lobby address + + + + + + + + 0 + 0 + + + + + + + true + + + + + + + Interface Scaling + + + + + + + Default repository + + + + + + + + 0 + 0 + + + + + + + true + + + + + + + Haptic Feedback + + + + + + + + + + Autosave prefix + + + + + + + + true + + + + Input - Mouse + + + 5 + + + + + + + 25 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 5 + + + + + + + 20 + + + 1000 + + + 10 + + + + + + + 1024 + + + 65535 + + + 3030 + + + + + + + 100 + + + 300 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 25 + + + + + + + Enemy AI in battles + + + + + + + Reserved screen area + + + + + + + % + + + 50 + + + 400 + + + 10 + + + + + + + Music Volume + + + + + + + BattleAI + + + + BattleAI + + + + + StupidAI + + + + + + + + Autosave + + + + + + + 100 + + + Qt::Horizontal + + + QSlider::TicksAbove + + + 10 + + + + + + + Network port + + + + + + + true + + + + 0 + 0 + + + + + + + true + + + false + + + + + + + Resolution + + + + + + + + 0 + 0 + + + + + + + true + + + + + + + Sound Volume + + + + + + + Controller Click Tolerance + + + + + + + Adventure Map Allies + + + + + + + + true + + + + Miscellaneous + + + 5 + + + + + + + 1024 + + + 65535 + + + 3030 + + + + + + + VCAI + + + + VCAI + + + + + Nullkiller + + + + + + + + + true + + + + Audio + + + 5 + + + + + + + + 0 + 0 + + + + + + + true + + + + + + + Software Cursor + + + + + + + Autocombat AI in battles + + + + + + + + + + + + + + true + + + + 0 + 0 + + + + + + + true + + + false + + + + + + + Font Scaling (experimental) + + + + + + + Sticks Sensitivity + + + + + + + Mods Validation + + + + + + + true + + + + 0 + 0 + + + + Off + + + true + + + false + + + buttonGroupValidation + + + + + + + true + + + + 0 + 0 + + + + Basic + + + true + + + false + + + buttonGroupValidation + + + + + + + true + + + + 0 + 0 + + + + Full + + + true + + + false + + + buttonGroupValidation + + + @@ -1311,6 +1408,7 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use - + + diff --git a/lib/json/JsonUtils.cpp b/lib/json/JsonUtils.cpp index e1cd47678..6ede7be82 100644 --- a/lib/json/JsonUtils.cpp +++ b/lib/json/JsonUtils.cpp @@ -296,4 +296,28 @@ JsonNode JsonUtils::assembleFromFiles(const std::string & filename) return result; } +void JsonUtils::detectConflicts(JsonNode & result, const JsonNode & left, const JsonNode & right, const std::string & keyName) +{ + switch (left.getType()) + { + case JsonNode::JsonType::DATA_NULL: + case JsonNode::JsonType::DATA_BOOL: + case JsonNode::JsonType::DATA_FLOAT: + case JsonNode::JsonType::DATA_INTEGER: + case JsonNode::JsonType::DATA_STRING: + case JsonNode::JsonType::DATA_VECTOR: // NOTE: comparing vectors as whole - since merge will overwrite it in its entirety + { + result[keyName][left.getModScope()] = left; + result[keyName][right.getModScope()] = right; + return; + } + case JsonNode::JsonType::DATA_STRUCT: + { + for(const auto & node : left.Struct()) + if (!right[node.first].isNull()) + detectConflicts(result, node.second, right[node.first], keyName + "/" + node.first); + } + } +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/json/JsonUtils.h b/lib/json/JsonUtils.h index dcf03d30e..85c322bcb 100644 --- a/lib/json/JsonUtils.h +++ b/lib/json/JsonUtils.h @@ -74,6 +74,12 @@ namespace JsonUtils /// get schema by json URI: vcmi:# /// example: schema "vcmi:settings" is used to check user settings DLL_LINKAGE const JsonNode & getSchema(const std::string & URI); + + /// detects potential conflicts - json entries present in both nodes + /// 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/mapObjectConstructors/CRewardableConstructor.cpp b/lib/mapObjectConstructors/CRewardableConstructor.cpp index 20375961a..ed0c92142 100644 --- a/lib/mapObjectConstructors/CRewardableConstructor.cpp +++ b/lib/mapObjectConstructors/CRewardableConstructor.cpp @@ -14,6 +14,7 @@ #include "../mapObjects/CRewardableObject.h" #include "../texts/CGeneralTextHandler.h" #include "../IGameCallback.h" +#include "../CConfigHandler.h" VCMI_LIB_NAMESPACE_BEGIN @@ -25,7 +26,8 @@ void CRewardableConstructor::initTypeData(const JsonNode & config) if (!config["name"].isNull()) VLC->generaltexth->registerString( config.getModScope(), getNameTextID(), config["name"]); - JsonUtils::validate(config, "vcmi:rewardable", getJsonKey()); + if (settings["mods"]["validation"].String() != "off") + JsonUtils::validate(config, "vcmi:rewardable", getJsonKey()); } diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 8dbf84eda..2f2780c5b 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -17,6 +17,7 @@ #include "ModIncompatibility.h" #include "../CCreatureHandler.h" +#include "../CConfigHandler.h" #include "../CStopWatch.h" #include "../GameSettings.h" #include "../ScriptHandler.h" @@ -331,10 +332,38 @@ void CModHandler::loadModFilesystems() coreMod->updateChecksum(calculateModChecksum(ModScope::scopeBuiltin(), CResourceHandler::get(ModScope::scopeBuiltin()))); + std::map 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") { - CModInfo & mod = allMods[modName]; - CResourceHandler::addFilesystem("data", modName, genModFilesystem(modName, mod.config)); + for(std::string & leftModName : activeMods) + { + for(std::string & rightModName : activeMods) + { + if (leftModName == rightModName) + continue; + + if (getModDependencies(leftModName).count(rightModName) || getModDependencies(rightModName).count(leftModName)) + continue; + + const auto & filter = [](const ResourcePath &path){return path.getType() != EResType::DIRECTORY;}; + + std::unordered_set leftResources = modFilesystems[leftModName]->getFilteredFiles(filter); + std::unordered_set 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()); + } + } + } } } @@ -370,6 +399,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); diff --git a/lib/modding/CModHandler.h b/lib/modding/CModHandler.h index 358483c22..fab2d319b 100644 --- a/lib/modding/CModHandler.h +++ b/lib/modding/CModHandler.h @@ -64,6 +64,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 d5dfb8461..0a9acb0f4 100644 --- a/lib/modding/ContentTypeHandler.cpp +++ b/lib/modding/ContentTypeHandler.cpp @@ -17,6 +17,7 @@ #include "../BattleFieldHandler.h" #include "../CArtHandler.h" #include "../CCreatureHandler.h" +#include "../CConfigHandler.h" #include "../entities/faction/CTownHandler.h" #include "../texts/CGeneralTextHandler.h" #include "../CHeroHandler.h" @@ -39,9 +40,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) @@ -79,6 +80,9 @@ bool ContentTypeHandler::preloadModData(const std::string & modName, const JsonN logMod->trace("Patching object %s (%s) from %s", objectName, remoteName, modName); JsonNode & remoteConf = modData[remoteName].patches[objectName]; + if (!remoteConf.isNull() && settings["mods"]["validation"].String() != "off") + JsonUtils::detectConflicts(conflictList, remoteConf, entry.second, objectName); + JsonUtils::merge(remoteConf, entry.second); } } @@ -93,7 +97,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 @@ -113,7 +117,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; } @@ -159,31 +163,70 @@ void ContentTypeHandler::loadCustom() void ContentTypeHandler::afterLoadFinalization() { - for (auto const & data : modData) + if (settings["mods"]["validation"].String() != "off") { - if (data.second.modData.isNull()) + for (auto const & data : modData) { - for (auto node : data.second.patches.Struct()) - logMod->warn("Mod '%s' have added patch for object '%s' from mod '%s', but this mod was not loaded or has no new objects.", node.second.getModScope(), node.first, data.first); - } - - for(auto & otherMod : modData) - { - if (otherMod.first == data.first) - continue; - - if (otherMod.second.modData.isNull()) - continue; - - for(auto & otherObject : otherMod.second.modData.Struct()) + if (data.second.modData.isNull()) { - if (data.second.modData.Struct().count(otherObject.first)) + for (auto node : data.second.patches.Struct()) + logMod->warn("Mod '%s' have added patch for object '%s' from mod '%s', but this mod was not loaded or has no new objects.", node.second.getModScope(), node.first, data.first); + } + + for(auto & otherMod : modData) + { + if (otherMod.first == data.first) + continue; + + if (otherMod.second.modData.isNull()) + continue; + + for(auto & otherObject : otherMod.second.modData.Struct()) { - logMod->warn("Mod '%s' have added object with name '%s' that is also available in mod '%s'", data.first, otherObject.first, otherMod.first); - logMod->warn("Two objects with same name were loaded. Please use form '%s:%s' if mod '%s' needs to modify this object instead", otherMod.first, otherObject.first, data.first); + if (data.second.modData.Struct().count(otherObject.first)) + { + logMod->warn("Mod '%s' have added object with name '%s' that is also available in mod '%s'", data.first, otherObject.first, otherMod.first); + logMod->warn("Two objects with same name were loaded. Please use form '%s:%s' if mod '%s' needs to modify this object instead", otherMod.first, otherObject.first, data.first); + } } } } + + 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(); @@ -249,7 +292,7 @@ void CContentHandler::afterLoadFinalization() void CContentHandler::preloadData(CModInfo & mod) { - bool validate = (mod.validation != CModInfo::PASSED); + bool validate = validateMod(mod); // print message in format [<8-symbols checksum>] auto & info = mod.getVerificationInfo(); @@ -266,7 +309,7 @@ void CContentHandler::preloadData(CModInfo & mod) void CContentHandler::load(CModInfo & mod) { - bool validate = (mod.validation != CModInfo::PASSED); + bool validate = validateMod(mod); if (!loadMod(mod.identifier, validate)) mod.validation = CModInfo::FAILED; @@ -287,4 +330,18 @@ const ContentTypeHandler & CContentHandler::operator[](const std::string & name) return handlers.at(name); } +bool CContentHandler::validateMod(const CModInfo & mod) const +{ + if (settings["mods"]["validation"].String() == "full") + return true; + + if (mod.validation == CModInfo::PASSED) + return false; + + if (settings["mods"]["validation"].String() == "off") + return false; + + return true; +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ContentTypeHandler.h b/lib/modding/ContentTypeHandler.h index 60b29d689..a623cb226 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; @@ -56,6 +58,7 @@ class DLL_LINKAGE CContentHandler std::map handlers; + bool validateMod(const CModInfo & mod) const; public: void init();