From 2439d176a0b79794152a4aed413f800391f26059 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 2 Oct 2024 18:57:38 +0000 Subject: [PATCH 1/5] Analyze filesystem of mods to detect potential mod conflicts --- lib/modding/CModHandler.cpp | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 8d20639f2..a028c8da4 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -331,10 +331,32 @@ 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]); + + for(std::string & leftModName : activeMods) { - CModInfo & mod = allMods[modName]; - CResourceHandler::addFilesystem("data", modName, genModFilesystem(modName, mod.config)); + for(std::string & rightModName : activeMods) + { + if (leftModName == rightModName) + 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()); + } + } } } From d0aba56a5eadd308f6574e120da9ff9e6e711798 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Wed, 2 Oct 2024 18:58:03 +0000 Subject: [PATCH 2/5] Analyze json object modifications to detect mod conflicts --- lib/json/JsonUtils.cpp | 28 ++++++++++++++++++++++++++++ lib/json/JsonUtils.h | 4 ++++ lib/modding/ContentTypeHandler.cpp | 3 +++ 3 files changed, 35 insertions(+) diff --git a/lib/json/JsonUtils.cpp b/lib/json/JsonUtils.cpp index f1680e11f..2e0fdd829 100644 --- a/lib/json/JsonUtils.cpp +++ b/lib/json/JsonUtils.cpp @@ -275,4 +275,32 @@ 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) +{ + if (left == right) + return; + + 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 + { + 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()); + return; + } + case JsonNode::JsonType::DATA_STRUCT: + { + for(auto & node : left.Struct()) + if (!right[node.first].isNull()) + detectConflicts(node.second, right[node.first], entityName, keyName + "/" + node.first); + } + } +} + VCMI_LIB_NAMESPACE_END diff --git a/lib/json/JsonUtils.h b/lib/json/JsonUtils.h index 954ee0b16..bf14ce2d6 100644 --- a/lib/json/JsonUtils.h +++ b/lib/json/JsonUtils.h @@ -72,6 +72,10 @@ 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 + /// 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); } VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/ContentTypeHandler.cpp b/lib/modding/ContentTypeHandler.cpp index 83ae46c1b..43ece3409 100644 --- a/lib/modding/ContentTypeHandler.cpp +++ b/lib/modding/ContentTypeHandler.cpp @@ -79,6 +79,9 @@ bool ContentTypeHandler::preloadModData(const std::string & modName, const std:: logMod->trace("Patching object %s (%s) from %s", objectName, remoteName, modName); JsonNode & remoteConf = modData[remoteName].patches[objectName]; + if (!remoteConf.isNull()) + JsonUtils::detectConflicts(remoteConf, entry.second, objectName, ""); + JsonUtils::merge(remoteConf, entry.second); } } From d849e53499f35f50654ce3a3cfd34a50dab2bd36 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 6 Oct 2024 15:54:30 +0000 Subject: [PATCH 3/5] 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; From 3e3f842fbe80396f352e65d19f8f7cfd9c1d6e37 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 6 Oct 2024 17:20:58 +0000 Subject: [PATCH 4/5] Respect dependencies when checking for filesystem conflicts --- lib/modding/CModHandler.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 5e0e4c8d2..15c9d22e4 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -346,6 +346,9 @@ void CModHandler::loadModFilesystems() 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); From 66fdad145c00940542868ac5b1ae38430cdeddf9 Mon Sep 17 00:00:00 2001 From: Ivan Savenko Date: Sun, 6 Oct 2024 19:21:18 +0000 Subject: [PATCH 5/5] Added an option to configure validation level in launcher --- config/schemas/settings.json | 19 +- launcher/settingsView/csettingsview_moc.cpp | 25 + launcher/settingsView/csettingsview_moc.h | 13 +- launcher/settingsView/csettingsview_moc.ui | 2010 +++++++++-------- .../CRewardableConstructor.cpp | 4 +- lib/modding/CModHandler.cpp | 40 +- lib/modding/ContentTypeHandler.cpp | 122 +- lib/modding/ContentTypeHandler.h | 1 + 8 files changed, 1200 insertions(+), 1034 deletions(-) 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 8cb8c32a0..541a909de 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/mapObjectConstructors/CRewardableConstructor.cpp b/lib/mapObjectConstructors/CRewardableConstructor.cpp index 80d842a47..bbdff2bf2 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"].String()); - 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 15c9d22e4..ccd71e2a6 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" @@ -339,25 +340,28 @@ void CModHandler::loadModFilesystems() for(std::string & modName : activeMods) CResourceHandler::addFilesystem("data", modName, modFilesystems[modName]); - for(std::string & leftModName : activeMods) + if (settings["mods"]["validation"].String() == "full") { - for(std::string & rightModName : activeMods) + for(std::string & leftModName : 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) + for(std::string & rightModName : activeMods) { - if (rightResources.count(leftFile)) - logMod->warn("Potential confict detected between '%s' and '%s': both mods add file '%s'", leftModName, rightModName, leftFile.getOriginalName()); + 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()); + } } } } @@ -508,8 +512,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/ContentTypeHandler.cpp b/lib/modding/ContentTypeHandler.cpp index 60289ca6e..f2887a711 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" @@ -79,7 +80,7 @@ bool ContentTypeHandler::preloadModData(const std::string & modName, const std:: logMod->trace("Patching object %s (%s) from %s", objectName, remoteName, modName); JsonNode & remoteConf = modData[remoteName].patches[objectName]; - if (!remoteConf.isNull()) + if (!remoteConf.isNull() && settings["mods"]["validation"].String() != "off") JsonUtils::detectConflicts(conflictList, remoteConf, entry.second, objectName); JsonUtils::merge(remoteConf, entry.second); @@ -162,67 +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) + for (const auto& [conflictPath, conflictModData] : conflictList.Struct()) { - if (conflictModData[modID] != conflictModData[*conflictingMods.begin()]) + 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) { - allEqual = false; - break; + 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()); } - - 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(); @@ -288,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(); @@ -305,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; @@ -326,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 6c3f553c5..5c21bf182 100644 --- a/lib/modding/ContentTypeHandler.h +++ b/lib/modding/ContentTypeHandler.h @@ -58,6 +58,7 @@ class DLL_LINKAGE CContentHandler std::map handlers; + bool validateMod(const CModInfo & mod) const; public: void init();