diff --git a/Mods/vcmi/config/vcmi/chinese.json b/Mods/vcmi/config/vcmi/chinese.json index 666b2168b..a62b3a606 100644 --- a/Mods/vcmi/config/vcmi/chinese.json +++ b/Mods/vcmi/config/vcmi/chinese.json @@ -30,9 +30,9 @@ "vcmi.capitalColors.6" : "褐色", "vcmi.capitalColors.7" : "粉色", - "vcmi.server.errors.existingProcess" : "一个VCMI进程已经在运行,启动新进程前请结束它。", - "vcmi.server.errors.modsIncompatibility" : "需要加载的MOD列表:", - "vcmi.server.confirmReconnect" : "您想要重连上一个会话么?", + "vcmi.server.errors.existingProcess" : "一个VCMI进程已经在运行,启动新进程前请结束它。", + "vcmi.server.errors.modsToEnable" : "{需要加载的MOD列表}", + "vcmi.server.confirmReconnect" : "您想要重连上一个会话么?", "vcmi.settingsMainWindow.generalTab.hover" : "常规", "vcmi.settingsMainWindow.generalTab.help" : "切换到“常规”选项卡 - 设置游戏客户端呈现", diff --git a/Mods/vcmi/config/vcmi/czech.json b/Mods/vcmi/config/vcmi/czech.json index dcbb8ccaf..04aa13f30 100644 --- a/Mods/vcmi/config/vcmi/czech.json +++ b/Mods/vcmi/config/vcmi/czech.json @@ -48,9 +48,9 @@ "vcmi.lobby.filename" : "Název souboru", "vcmi.lobby.creationDate" : "Datum vytvoření", - "vcmi.server.errors.existingProcess" : "Již běží jiný server VCMI. Prosím, ukončete ho před startem nové hry.", - "vcmi.server.errors.modsIncompatibility" : "Následující modifikace jsou nutné pro načtení hry:", - "vcmi.server.confirmReconnect" : "Chcete se připojit k poslední relaci?", + "vcmi.server.errors.existingProcess" : "Již běží jiný server VCMI. Prosím, ukončete ho před startem nové hry.", + "vcmi.server.errors.modsToEnable" : "{Následující modifikace jsou nutné pro načtení hry}", + "vcmi.server.confirmReconnect" : "Chcete se připojit k poslední relaci?", "vcmi.settingsMainWindow.generalTab.hover" : "Obecné", "vcmi.settingsMainWindow.generalTab.help" : "Přepne na kartu obecných nastavení, která obsahuje nastavení související s obecným chováním klienta hry", diff --git a/Mods/vcmi/config/vcmi/english.json b/Mods/vcmi/config/vcmi/english.json index cc95f3e30..5e2279dfe 100644 --- a/Mods/vcmi/config/vcmi/english.json +++ b/Mods/vcmi/config/vcmi/english.json @@ -53,9 +53,10 @@ "vcmi.lobby.filename" : "Filename", "vcmi.lobby.creationDate" : "Creation date", - "vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.", - "vcmi.server.errors.modsIncompatibility" : "The following mods are required to load the game:", - "vcmi.server.confirmReconnect" : "Do you want to reconnect to the last session?", + "vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.", + "vcmi.server.errors.modsToEnable" : "{Following mods are required}", + "vcmi.server.errors.modsToDisable" : "{Following mods must be disabled}", + "vcmi.server.confirmReconnect" : "Do you want to reconnect to the last session?", "vcmi.settingsMainWindow.generalTab.hover" : "General", "vcmi.settingsMainWindow.generalTab.help" : "Switches to General Options tab, which contains settings related to general game client behavior.", diff --git a/Mods/vcmi/config/vcmi/french.json b/Mods/vcmi/config/vcmi/french.json index 5d3e0a532..69112035d 100644 --- a/Mods/vcmi/config/vcmi/french.json +++ b/Mods/vcmi/config/vcmi/french.json @@ -38,9 +38,9 @@ "vcmi.mainMenu.joinTCP" : "Rejoindre TCP/IP jeu", "vcmi.mainMenu.playerName" : "Joueur", - "vcmi.server.errors.existingProcess" : "Un autre processus de serveur VCMI est en cours d'exécution. Veuillez l'arrêter' avant de démarrer un nouveau jeu.", - "vcmi.server.errors.modsIncompatibility" : "Les mods suivants sont nécessaires pour charger le jeu :", - "vcmi.server.confirmReconnect" : "Voulez-vous vous reconnecter à la dernière session ?", + "vcmi.server.errors.existingProcess" : "Un autre processus de serveur VCMI est en cours d'exécution. Veuillez l'arrêter' avant de démarrer un nouveau jeu.", + "vcmi.server.errors.modsToEnable" : "{Les mods suivants sont nécessaires pour charger le jeu}", + "vcmi.server.confirmReconnect" : "Voulez-vous vous reconnecter à la dernière session ?", "vcmi.settingsMainWindow.generalTab.hover" : "Général", "vcmi.settingsMainWindow.generalTab.help" : "Passe à l'onglet Options générales, qui contient des paramètres liés au comportement général du client de jeu", diff --git a/Mods/vcmi/config/vcmi/german.json b/Mods/vcmi/config/vcmi/german.json index 926501565..fe6f63828 100644 --- a/Mods/vcmi/config/vcmi/german.json +++ b/Mods/vcmi/config/vcmi/german.json @@ -52,9 +52,9 @@ "vcmi.lobby.filename" : "Dateiname", "vcmi.lobby.creationDate" : "Erstellungsdatum", - "vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst", - "vcmi.server.errors.modsIncompatibility" : "Erforderliche Mods um das Spiel zu laden:", - "vcmi.server.confirmReconnect" : "Mit der letzten Sitzung verbinden?", + "vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst", + "vcmi.server.errors.modsToEnable" : "{Erforderliche Mods um das Spiel zu laden}", + "vcmi.server.confirmReconnect" : "Mit der letzten Sitzung verbinden?", "vcmi.settingsMainWindow.generalTab.hover" : "Allgemein", "vcmi.settingsMainWindow.generalTab.help" : "Wechselt zur Registerkarte Allgemeine Optionen, die Einstellungen zum allgemeinen Verhalten des Spielclients enthält.", diff --git a/Mods/vcmi/config/vcmi/polish.json b/Mods/vcmi/config/vcmi/polish.json index f382a9578..af4eac0c6 100644 --- a/Mods/vcmi/config/vcmi/polish.json +++ b/Mods/vcmi/config/vcmi/polish.json @@ -47,9 +47,9 @@ "vcmi.lobby.filename" : "Nazwa pliku", "vcmi.lobby.creationDate" : "Data utworzenia", - "vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej", - "vcmi.server.errors.modsIncompatibility" : "Następujące mody są wymagane do wczytania gry:", - "vcmi.server.confirmReconnect" : "Połączyć ponownie z ostatnią sesją?", + "vcmi.server.errors.existingProcess" : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej", + "vcmi.server.errors.modsToEnable" : "{Następujące mody są wymagane do wczytania gry}", + "vcmi.server.confirmReconnect" : "Połączyć ponownie z ostatnią sesją?", "vcmi.settingsMainWindow.generalTab.hover" : "Ogólne", "vcmi.settingsMainWindow.generalTab.help" : "Przełącza do zakładki opcji ogólnych, która zawiera ustawienia związane z ogólnym działaniem gry", diff --git a/Mods/vcmi/config/vcmi/russian.json b/Mods/vcmi/config/vcmi/russian.json index 51d13ac0d..63d1a2d48 100644 --- a/Mods/vcmi/config/vcmi/russian.json +++ b/Mods/vcmi/config/vcmi/russian.json @@ -21,9 +21,9 @@ "vcmi.adventureMap.moveCostDetails" : "Очки движения - Стоимость: %TURNS ходов + %POINTS очков, Останется: %REMAINING очков", "vcmi.adventureMap.moveCostDetailsNoTurns" : "Очки движения - Стоимость: %POINTS очков, Останется: %REMAINING очков", - "vcmi.server.errors.existingProcess" : "Запущен другой процесс vcmiserver, сначала завершите его.", - "vcmi.server.errors.modsIncompatibility" : "Требуемые моды для загрузки игры:", - "vcmi.server.confirmReconnect" : "Подключиться к предыдущей сессии?", + "vcmi.server.errors.existingProcess" : "Запущен другой процесс vcmiserver, сначала завершите его.", + "vcmi.server.errors.modsToEnable" : "{Требуемые моды для загрузки игры}", + "vcmi.server.confirmReconnect" : "Подключиться к предыдущей сессии?", "vcmi.settingsMainWindow.generalTab.hover" : "Общее", "vcmi.settingsMainWindow.generalTab.help" : "Переключиться на вкладку \"Общее\", содержащее общие настройки клиента игры", diff --git a/Mods/vcmi/config/vcmi/spanish.json b/Mods/vcmi/config/vcmi/spanish.json index ffc43609b..e18986ebc 100644 --- a/Mods/vcmi/config/vcmi/spanish.json +++ b/Mods/vcmi/config/vcmi/spanish.json @@ -30,9 +30,9 @@ "vcmi.capitalColors.6" : "Turquesa", "vcmi.capitalColors.7" : "Rosa", - "vcmi.server.errors.existingProcess" : "Otro proceso de vcmiserver está en ejecución, por favor termínalo primero", - "vcmi.server.errors.modsIncompatibility" : "Mods necesarios para cargar el juego:", - "vcmi.server.confirmReconnect" : "¿Conectar a la última sesión?", + "vcmi.server.errors.existingProcess" : "Otro proceso de vcmiserver está en ejecución, por favor termínalo primero", + "vcmi.server.errors.modsToEnable" : "{Mods necesarios para cargar el juego}", + "vcmi.server.confirmReconnect" : "¿Conectar a la última sesión?", "vcmi.settingsMainWindow.generalTab.hover" : "General", "vcmi.settingsMainWindow.generalTab.help" : "Cambiar a la pestaña de opciones generales, que contiene ajustes relacionados con el comportamiento general del juego", diff --git a/Mods/vcmi/config/vcmi/ukrainian.json b/Mods/vcmi/config/vcmi/ukrainian.json index 78f16b9d4..315a837ad 100644 --- a/Mods/vcmi/config/vcmi/ukrainian.json +++ b/Mods/vcmi/config/vcmi/ukrainian.json @@ -48,9 +48,9 @@ "vcmi.lobby.filename" : "Назва файлу", "vcmi.lobby.creationDate" : "Дата створення", - "vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його", - "vcmi.server.errors.modsIncompatibility" : "Потрібні модифікації для завантаження гри:", - "vcmi.server.confirmReconnect" : "Підключитися до минулої сесії?", + "vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його", + "vcmi.server.errors.modsToEnable" : "{Потрібні модифікації для завантаження гри}", + "vcmi.server.confirmReconnect" : "Підключитися до минулої сесії?", "vcmi.settingsMainWindow.generalTab.hover" : "Загальні", "vcmi.settingsMainWindow.generalTab.help" : "Перемикає на вкладку загальних параметрів, яка містить налаштування, пов'язані із загальною поведінкою ігрового клієнта", diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index 291fe5b70..737afd23a 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -537,6 +537,8 @@ void CServerHandler::sendGuiAction(ui8 action) const void CServerHandler::sendRestartGame() const { + GH.windows().createAndPushWindow(); + LobbyEndGame endGame; endGame.closeConnection = false; endGame.restart = true; @@ -552,10 +554,17 @@ bool CServerHandler::validateGameStart(bool allowOnlyAI) const catch(ModIncompatibility & e) { logGlobal->warn("Incompatibility exception during start scenario: %s", e.what()); - - auto errorMsg = CGI->generaltexth->translate("vcmi.server.errors.modsIncompatibility") + '\n'; - errorMsg += e.what(); - + std::string errorMsg; + if(!e.whatMissing().empty()) + { + errorMsg += VLC->generaltexth->translate("vcmi.server.errors.modsToEnable") + '\n'; + errorMsg += e.whatMissing(); + } + if(!e.whatExcessive().empty()) + { + errorMsg += VLC->generaltexth->translate("vcmi.server.errors.modsToDisable") + '\n'; + errorMsg += e.whatExcessive(); + } showServerError(errorMsg); return false; } @@ -572,7 +581,8 @@ bool CServerHandler::validateGameStart(bool allowOnlyAI) const void CServerHandler::sendStartGame(bool allowOnlyAI) const { verifyStateBeforeStart(allowOnlyAI ? true : settings["session"]["onlyai"].Bool()); - + GH.windows().createAndPushWindow(); + LobbyStartGame lsg; if(client) { @@ -711,6 +721,9 @@ void CServerHandler::startCampaignScenario(std::shared_ptr cs) void CServerHandler::showServerError(const std::string & txt) const { + if(auto w = GH.windows().topWindow()) + GH.windows().popWindow(w); + CInfoWindow::showInfoDialog(txt, {}); } diff --git a/client/NetPacksLobbyClient.cpp b/client/NetPacksLobbyClient.cpp index b5356ccf5..7dd44cc88 100644 --- a/client/NetPacksLobbyClient.cpp +++ b/client/NetPacksLobbyClient.cpp @@ -149,8 +149,6 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyLoadProgress(LobbyLoadProgress w->tick(0); w->redraw(); } - else - GH.windows().createAndPushWindow(); } void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState & pack) diff --git a/client/lobby/CLobbyScreen.cpp b/client/lobby/CLobbyScreen.cpp index dba561388..f2256f91f 100644 --- a/client/lobby/CLobbyScreen.cpp +++ b/client/lobby/CLobbyScreen.cpp @@ -29,7 +29,6 @@ #include "../../lib/CGeneralTextHandler.h" #include "../../lib/campaign/CampaignHandler.h" #include "../../lib/mapping/CMapInfo.h" -#include "../../lib/modding/ModIncompatibility.h" #include "../../lib/rmg/CMapGenOptions.h" CLobbyScreen::CLobbyScreen(ESelectionScreen screenType) diff --git a/lib/StartInfo.cpp b/lib/StartInfo.cpp index 7e3a5b91c..f3abd2d92 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -74,12 +74,12 @@ void LobbyInfo::verifyStateBeforeStart(bool ignoreNoHuman) const throw std::domain_error(VLC->generaltexth->translate("core.genrltxt.529")); auto missingMods = CMapService::verifyMapHeaderMods(*mi->mapHeader); - ModIncompatibility::ModList modList; + ModIncompatibility::ModListWithVersion modList; for(const auto & m : missingMods) - modList.push_back({m.first, m.second.toString()}); + modList.push_back({m.second.name, m.second.version.toString()}); if(!modList.empty()) - throw ModIncompatibility(std::move(modList)); + throw ModIncompatibility(modList); //there must be at least one human player before game can be started std::map::const_iterator i; diff --git a/lib/mapping/CMapHeader.h b/lib/mapping/CMapHeader.h index 8b04c1f2e..64ea09b7b 100644 --- a/lib/mapping/CMapHeader.h +++ b/lib/mapping/CMapHeader.h @@ -10,7 +10,7 @@ #pragma once -#include "../modding/CModVersion.h" +#include "../modding/CModInfo.h" #include "../LogicalExpression.h" #include "../int3.h" #include "../MetaString.h" @@ -19,7 +19,7 @@ VCMI_LIB_NAMESPACE_BEGIN class CGObjectInstance; enum class EMapFormat : uint8_t; -using ModCompatibilityInfo = std::map; +using ModCompatibilityInfo = std::map; /// The hero name struct consists of the hero id and the hero name. struct DLL_LINKAGE SHeroName @@ -249,8 +249,7 @@ public: void serialize(Handler & h, const int Version) { h & version; - if(Version >= 821) - h & mods; + h & mods; h & name; h & description; h & width; diff --git a/lib/mapping/CMapService.cpp b/lib/mapping/CMapService.cpp index 8b52b0a72..0d119bd5a 100644 --- a/lib/mapping/CMapService.cpp +++ b/lib/mapping/CMapService.cpp @@ -92,20 +92,29 @@ void CMapService::saveMap(const std::unique_ptr & map, boost::filesystem:: ModCompatibilityInfo CMapService::verifyMapHeaderMods(const CMapHeader & map) { - ModCompatibilityInfo modCompatibilityInfo; const auto & activeMods = VLC->modh->getActiveMods(); + + ModCompatibilityInfo missingMods, missingModsFiltered; for(const auto & mapMod : map.mods) { if(vstd::contains(activeMods, mapMod.first)) { const auto & modInfo = VLC->modh->getModInfo(mapMod.first); - if(modInfo.version.compatible(mapMod.second)) + if(modInfo.getVerificationInfo().version.compatible(mapMod.second.version)) continue; } - - modCompatibilityInfo[mapMod.first] = mapMod.second; - } - return modCompatibilityInfo; + missingMods[mapMod.first] = mapMod.second; + } + + //filter child mods + for(const auto & mapMod : missingMods) + { + if(!mapMod.second.parent.empty() && missingMods.count(mapMod.second.parent)) + continue; + missingModsFiltered.insert(mapMod); + } + + return missingModsFiltered; } std::unique_ptr CMapService::getStreamFromFS(const ResourcePath & name) diff --git a/lib/mapping/CMapService.h b/lib/mapping/CMapService.h index 9a8a33c7f..ed791197b 100644 --- a/lib/mapping/CMapService.h +++ b/lib/mapping/CMapService.h @@ -10,6 +10,8 @@ #pragma once +#include "../modding/CModInfo.h" + VCMI_LIB_NAMESPACE_BEGIN class ResourcePath; @@ -17,12 +19,11 @@ class ResourcePath; class CMap; class CMapHeader; class CInputStream; -struct CModVersion; class IMapLoader; class IMapPatcher; -using ModCompatibilityInfo = std::map; +using ModCompatibilityInfo = std::map; /** * The map service provides loading of VCMI/H3 map files. It can diff --git a/lib/mapping/MapFormatJson.cpp b/lib/mapping/MapFormatJson.cpp index 42c76415b..66de3989e 100644 --- a/lib/mapping/MapFormatJson.cpp +++ b/lib/mapping/MapFormatJson.cpp @@ -342,7 +342,7 @@ namespace TerrainDetail ///CMapFormatJson const int CMapFormatJson::VERSION_MAJOR = 1; -const int CMapFormatJson::VERSION_MINOR = 2; +const int CMapFormatJson::VERSION_MINOR = 3; const std::string CMapFormatJson::HEADER_FILE_NAME = "header.json"; const std::string CMapFormatJson::OBJECTS_FILE_NAME = "objects.json"; @@ -958,7 +958,19 @@ void CMapLoaderJson::readHeader(const bool complete) if(!header["mods"].isNull()) { for(auto & mod : header["mods"].Vector()) - mapHeader->mods[mod["name"].String()] = CModVersion::fromString(mod["version"].String()); + { + CModInfo::VerificationInfo info; + info.version = CModVersion::fromString(mod["version"].String()); + info.checksum = mod["checksum"].Integer(); + info.name = mod["name"].String(); + info.parent = mod["parent"].String(); + info.impactsGameplay = true; + + if(!mod["modId"].isNull()) + mapHeader->mods[mod["modId"].String()] = info; + else + mapHeader->mods[mod["name"].String()] = info; + } } //todo: multilevel map load support @@ -1299,8 +1311,11 @@ void CMapSaverJson::writeHeader() for(const auto & mod : mapHeader->mods) { JsonNode modWriter; - modWriter["name"].String() = mod.first; - modWriter["version"].String() = mod.second.toString(); + modWriter["modId"].String() = mod.first; + modWriter["name"].String() = mod.second.name; + modWriter["parent"].String() = mod.second.parent; + modWriter["version"].String() = mod.second.version.toString(); + modWriter["checksum"].Integer() = mod.second.checksum; mods.Vector().push_back(modWriter); } diff --git a/lib/modding/CModHandler.cpp b/lib/modding/CModHandler.cpp index 19049e1ee..12187990a 100644 --- a/lib/modding/CModHandler.cpp +++ b/lib/modding/CModHandler.cpp @@ -56,7 +56,7 @@ bool CModHandler::hasCircularDependency(const TModID & modID, std::set c if (vstd::contains(currentList, modID)) { logMod->error("Error: Circular dependency detected! Printing dependency list:"); - logMod->error("\t%s -> ", mod.name); + logMod->error("\t%s -> ", mod.getVerificationInfo().name); return true; } @@ -67,7 +67,7 @@ bool CModHandler::hasCircularDependency(const TModID & modID, std::set c { if (hasCircularDependency(dependency, currentList)) { - logMod->error("\t%s ->\n", mod.name); // conflict detected, print dependency list + logMod->error("\t%s ->\n", mod.getVerificationInfo().name); // conflict detected, print dependency list return true; } } @@ -129,7 +129,7 @@ std::vector CModHandler::validateAndSortDependencies(std::vector error("Mod '%s' will not work: it depends on mod '%s', which is not installed.", brokenMod.name, dependency); + logMod->error("Mod '%s' will not work: it depends on mod '%s', which is not installed.", brokenMod.getVerificationInfo().name, dependency); } } return sortedValidMods; @@ -212,7 +212,6 @@ void CModHandler::loadMods(bool onlyEssential) } coreMod = std::make_unique(ModScope::scopeBuiltin(), modConfig[ModScope::scopeBuiltin()], JsonNode(JsonPath::builtin("config/gameConfig.json"))); - coreMod->name = "Original game files"; } std::vector CModHandler::getAllMods() @@ -352,7 +351,7 @@ void CModHandler::initializeConfig() CModVersion CModHandler::getModVersion(TModID modName) const { if (allMods.count(modName)) - return allMods.at(modName).version; + return allMods.at(modName).getVerificationInfo().version; return {}; } @@ -462,6 +461,7 @@ void CModHandler::afterLoad(bool onlyEssential) modSettings["activeMods"].resolvePointer(pointer) = modEntry.second.saveLocalData(); } modSettings[ModScope::scopeBuiltin()] = coreMod->saveLocalData(); + modSettings[ModScope::scopeBuiltin()]["name"].String() = "Original game files"; if(!onlyEssential) { @@ -471,49 +471,85 @@ void CModHandler::afterLoad(bool onlyEssential) } -void CModHandler::trySetActiveMods(std::vector saveActiveMods, const std::map & modList) +void CModHandler::trySetActiveMods(const std::vector> & modList) { - std::vector newActiveMods; - - ModIncompatibility::ModList missingMods; - + auto searchVerificationInfo = [&modList](const TModID & m) -> const CModInfo::VerificationInfo* + { + for(auto & i : modList) + if(i.first == m) + return &i.second; + return nullptr; + }; + + std::vector missingMods, excessiveMods; + ModIncompatibility::ModListWithVersion missingModsResult; + ModIncompatibility::ModList excessiveModsResult; + for(const auto & m : activeMods) { - if (vstd::contains(saveActiveMods, m)) + if(searchVerificationInfo(m)) continue; - auto & modInfo = allMods.at(m); - if(modInfo.checkModGameplayAffecting()) - missingMods.emplace_back(m, modInfo.version.toString()); + //TODO: support actual disabling of these mods + if(getModInfo(m).checkModGameplayAffecting()) + excessiveMods.push_back(m); } - - for(const auto & m : saveActiveMods) + + for(const auto & infoPair : modList) { - const CModVersion & mver = modList.at(m); - - if (allMods.count(m) == 0) + auto & remoteModId = infoPair.first; + auto & remoteModInfo = infoPair.second; + + bool modAffectsGameplay = remoteModInfo.impactsGameplay; + //parent mod affects gameplay if child affects too + for(const auto & subInfoPair : modList) + modAffectsGameplay |= (subInfoPair.second.impactsGameplay && subInfoPair.second.parent == remoteModId); + + if(!allMods.count(remoteModId)) { - missingMods.emplace_back(m, mver.toString()); + if(modAffectsGameplay) + missingMods.push_back(remoteModId); //mod is not installed continue; } - - auto & modInfo = allMods.at(m); - - bool modAffectsGameplay = modInfo.checkModGameplayAffecting(); - bool modVersionCompatible = modInfo.version.isNull() || mver.isNull() || modInfo.version.compatible(mver); - bool modEnabledLocally = vstd::contains(activeMods, m); - bool modCanBeEnabled = modEnabledLocally && modVersionCompatible; - - allMods[m].setEnabled(modCanBeEnabled); - - if (modCanBeEnabled) - newActiveMods.push_back(m); - - if (!modCanBeEnabled && modAffectsGameplay) - missingMods.emplace_back(m, mver.toString()); + + auto & localModInfo = getModInfo(remoteModId).getVerificationInfo(); + modAffectsGameplay |= getModInfo(remoteModId).checkModGameplayAffecting(); + bool modVersionCompatible = localModInfo.version.isNull() + || remoteModInfo.version.isNull() + || localModInfo.version.compatible(remoteModInfo.version); + bool modLocalyEnabled = vstd::contains(activeMods, remoteModId); + + if(modVersionCompatible && modAffectsGameplay && modLocalyEnabled) + continue; + + if(modAffectsGameplay) + missingMods.push_back(remoteModId); //incompatible mod impacts gameplay } - - std::swap(activeMods, newActiveMods); + + //filter mods + for(auto & m : missingMods) + { + if(auto * vInfo = searchVerificationInfo(m)) + { + assert(vInfo->parent != m); + if(!vInfo->parent.empty() && vstd::contains(missingMods, vInfo->parent)) + continue; + missingModsResult.push_back({vInfo->name, vInfo->version.toString()}); + } + } + for(auto & m : excessiveMods) + { + auto & vInfo = getModInfo(m).getVerificationInfo(); + assert(vInfo.parent != m); + if(!vInfo.parent.empty() && vstd::contains(excessiveMods, vInfo.parent)) + continue; + excessiveModsResult.push_back(vInfo.name); + } + + if(!missingModsResult.empty() || !excessiveModsResult.empty()) + throw ModIncompatibility(missingModsResult, excessiveModsResult); + + //TODO: support actual enabling of required mods } VCMI_LIB_NAMESPACE_END diff --git a/lib/modding/CModHandler.h b/lib/modding/CModHandler.h index 93ec72775..2062909bf 100644 --- a/lib/modding/CModHandler.h +++ b/lib/modding/CModHandler.h @@ -9,13 +9,12 @@ */ #pragma once -#include "CModVersion.h" +#include "CModInfo.h" VCMI_LIB_NAMESPACE_BEGIN class CModHandler; class CModIndentifier; -class CModInfo; class JsonNode; class IHandlerBase; class CIdentifierStorage; @@ -52,7 +51,7 @@ class DLL_LINKAGE CModHandler : boost::noncopyable CModVersion getModVersion(TModID modName) const; /// Attempt to set active mods according to provided list of mods from save, throws on failure - void trySetActiveMods(std::vector saveActiveMods, const std::map & modList); + void trySetActiveMods(const std::vector> & modList); public: std::shared_ptr content; //(!)Do not serialize FIXME: make private @@ -88,22 +87,22 @@ public: { h & activeMods; for(const auto & m : activeMods) - { - CModVersion version = getModVersion(m); - h & version; - } + h & getModInfo(m).getVerificationInfo(); } else { loadMods(); std::vector saveActiveMods; - std::map modVersions; h & saveActiveMods; + + std::vector> saveModInfos(saveActiveMods.size()); + for(int i = 0; i < saveActiveMods.size(); ++i) + { + saveModInfos[i].first = saveActiveMods[i]; + h & saveModInfos[i].second; + } - for(const auto & m : saveActiveMods) - h & modVersions[m]; - - trySetActiveMods(saveActiveMods, modVersions); + trySetActiveMods(saveModInfos); } } }; diff --git a/lib/modding/CModInfo.cpp b/lib/modding/CModInfo.cpp index 1901e8923..d90ce08ae 100644 --- a/lib/modding/CModInfo.cpp +++ b/lib/modding/CModInfo.cpp @@ -23,7 +23,6 @@ static JsonNode addMeta(JsonNode config, const std::string & meta) } CModInfo::CModInfo(): - checksum(0), explicitlyEnabled(false), implicitlyEnabled(true), validation(PENDING) @@ -33,17 +32,20 @@ CModInfo::CModInfo(): CModInfo::CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode & config): identifier(identifier), - name(config["name"].String()), description(config["description"].String()), dependencies(config["depends"].convertTo>()), conflicts(config["conflicts"].convertTo>()), - checksum(0), explicitlyEnabled(false), implicitlyEnabled(true), validation(PENDING), config(addMeta(config, identifier)) { - version = CModVersion::fromString(config["version"].String()); + verificationInfo.name = config["name"].String(); + verificationInfo.version = CModVersion::fromString(config["version"].String()); + verificationInfo.parent = identifier.substr(0, identifier.find_last_of('.')); + if(verificationInfo.parent == identifier) + verificationInfo.parent.clear(); + if(!config["compatibility"].isNull()) { vcmiCompatibleMin = CModVersion::fromString(config["compatibility"]["min"].String()); @@ -61,7 +63,7 @@ CModInfo::CModInfo(const std::string & identifier, const JsonNode & local, const JsonNode CModInfo::saveLocalData() const { std::ostringstream stream; - stream << std::noshowbase << std::hex << std::setw(8) << std::setfill('0') << checksum; + stream << std::noshowbase << std::hex << std::setw(8) << std::setfill('0') << verificationInfo.checksum; JsonNode conf; conf["active"].Bool() = explicitlyEnabled; @@ -83,9 +85,9 @@ JsonPath CModInfo::getModFile(const std::string & name) void CModInfo::updateChecksum(ui32 newChecksum) { // comment-out next line to force validation of all mods ignoring checksum - if (newChecksum != checksum) + if (newChecksum != verificationInfo.checksum) { - checksum = newChecksum; + verificationInfo.checksum = newChecksum; validation = PENDING; } } @@ -95,7 +97,7 @@ void CModInfo::loadLocalData(const JsonNode & data) bool validated = false; implicitlyEnabled = true; explicitlyEnabled = !config["keepDisabled"].Bool(); - checksum = 0; + verificationInfo.checksum = 0; if (data.getType() == JsonNode::JsonType::DATA_BOOL) { explicitlyEnabled = data.Bool(); @@ -104,7 +106,7 @@ void CModInfo::loadLocalData(const JsonNode & data) { explicitlyEnabled = data["active"].Bool(); validated = data["validated"].Bool(); - checksum = strtol(data["checksum"].String().c_str(), nullptr, 16); + updateChecksum(strtol(data["checksum"].String().c_str(), nullptr, 16)); } //check compatibility @@ -112,13 +114,13 @@ void CModInfo::loadLocalData(const JsonNode & data) implicitlyEnabled &= (vcmiCompatibleMax.isNull() || vcmiCompatibleMax.compatible(CModVersion::GameVersion(), true, true)); if(!implicitlyEnabled) - logGlobal->warn("Mod %s is incompatible with current version of VCMI and cannot be enabled", name); + logGlobal->warn("Mod %s is incompatible with current version of VCMI and cannot be enabled", verificationInfo.name); if (boost::iequals(config["modType"].String(), "translation")) // compatibility code - mods use "Translation" type at the moment { if (baseLanguage != VLC->generaltexth->getPreferredLanguage()) { - logGlobal->warn("Translation mod %s was not loaded: language mismatch!", name); + logGlobal->warn("Translation mod %s was not loaded: language mismatch!", verificationInfo.name); implicitlyEnabled = false; } } @@ -127,6 +129,8 @@ void CModInfo::loadLocalData(const JsonNode & data) validation = validated ? PASSED : PENDING; else validation = validated ? PASSED : FAILED; + + verificationInfo.impactsGameplay = checkModGameplayAffecting(); } bool CModInfo::checkModGameplayAffecting() const @@ -171,6 +175,11 @@ bool CModInfo::checkModGameplayAffecting() const return *modGameplayAffecting; } +const CModInfo::VerificationInfo & CModInfo::getVerificationInfo() const +{ + return verificationInfo; +} + bool CModInfo::isEnabled() const { return implicitlyEnabled && explicitlyEnabled; diff --git a/lib/modding/CModInfo.h b/lib/modding/CModInfo.h index 6e8a5012d..7469e1f19 100644 --- a/lib/modding/CModInfo.h +++ b/lib/modding/CModInfo.h @@ -29,17 +29,41 @@ public: FAILED, PASSED }; + + struct VerificationInfo + { + /// human-readable mod name + std::string name; + + /// version of the mod + CModVersion version; + + /// CRC-32 checksum of the mod + ui32 checksum = 0; + + /// parent mod ID, empty if root-level mod + TModID parent; + + /// for serialization purposes + bool impactsGameplay = true; + + template + void serialize(Handler & h, const int v) + { + h & name; + h & version; + h & checksum; + h & parent; + h & impactsGameplay; + } + }; /// identifier, identical to name of folder with mod std::string identifier; - /// human-readable strings - std::string name; + /// detailed mod description std::string description; - /// version of the mod - CModVersion version; - /// Base language of mod, all mod strings are assumed to be in this language std::string baseLanguage; @@ -52,9 +76,6 @@ public: /// list of mods that can't be used in the same time as this one std::set conflicts; - /// CRC-32 checksum of the mod - ui32 checksum; - EValidationStatus validation; JsonNode config; @@ -73,6 +94,8 @@ public: /// return true if this mod can affect gameplay, e.g. adds or modifies any game objects bool checkModGameplayAffecting() const; + + const VerificationInfo & getVerificationInfo() const; private: /// true if mod is enabled by user, e.g. in Launcher UI @@ -80,6 +103,8 @@ private: /// true if mod can be loaded - compatible and has no missing deps bool implicitlyEnabled; + + VerificationInfo verificationInfo; void loadLocalData(const JsonNode & data); }; diff --git a/lib/modding/ContentTypeHandler.cpp b/lib/modding/ContentTypeHandler.cpp index afcfd0ff9..9d018f3e9 100644 --- a/lib/modding/ContentTypeHandler.cpp +++ b/lib/modding/ContentTypeHandler.cpp @@ -212,7 +212,8 @@ void CContentHandler::preloadData(CModInfo & mod) bool validate = (mod.validation != CModInfo::PASSED); // print message in format [<8-symbols checksum>] - logMod->info("\t\t[%08x]%s", mod.checksum, mod.name); + auto & info = mod.getVerificationInfo(); + logMod->info("\t\t[%08x]%s", info.checksum, info.name); if (validate && mod.identifier != ModScope::scopeBuiltin()) { @@ -233,12 +234,12 @@ void CContentHandler::load(CModInfo & mod) if (validate) { if (mod.validation != CModInfo::FAILED) - logMod->info("\t\t[DONE] %s", mod.name); + logMod->info("\t\t[DONE] %s", mod.getVerificationInfo().name); else - logMod->error("\t\t[FAIL] %s", mod.name); + logMod->error("\t\t[FAIL] %s", mod.getVerificationInfo().name); } else - logMod->info("\t\t[SKIP] %s", mod.name); + logMod->info("\t\t[SKIP] %s", mod.getVerificationInfo().name); } const ContentTypeHandler & CContentHandler::operator[](const std::string & name) const diff --git a/lib/modding/ModIncompatibility.h b/lib/modding/ModIncompatibility.h index ee7e2e2c4..23af7deaf 100644 --- a/lib/modding/ModIncompatibility.h +++ b/lib/modding/ModIncompatibility.h @@ -14,29 +14,44 @@ VCMI_LIB_NAMESPACE_BEGIN class DLL_LINKAGE ModIncompatibility: public std::exception { public: - using StringPair = std::pair; - using ModList = std::list; + using ModListWithVersion = std::vector>; + using ModList = std::vector; - ModIncompatibility(ModList && _missingMods): - missingMods(std::move(_missingMods)) + ModIncompatibility(const ModListWithVersion & _missingMods) { std::ostringstream _ss; - for(const auto & m : missingMods) + for(const auto & m : _missingMods) _ss << m.first << ' ' << m.second << std::endl; - message = _ss.str(); + messageMissingMods = _ss.str(); } - + + ModIncompatibility(const ModListWithVersion & _missingMods, ModList & _excessiveMods) + : ModIncompatibility(_missingMods) + { + std::ostringstream _ss; + for(const auto & m : _excessiveMods) + _ss << m << std::endl; + messageExcessiveMods = _ss.str(); + } + const char * what() const noexcept override { - return message.c_str(); + static const std::string w("Mod incompatibility exception"); + return w.c_str(); + } + + const std::string & whatMissing() const noexcept + { + return messageMissingMods; + } + + const std::string & whatExcessive() const noexcept + { + return messageExcessiveMods; } private: - //list of mods required to load the game - // first: mod name - // second: mod version - const ModList missingMods; - std::string message; + std::string messageMissingMods, messageExcessiveMods; }; VCMI_LIB_NAMESPACE_END diff --git a/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 14b6dfcba..8242ee5ec 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -336,19 +336,20 @@ bool MainWindow::openMap(const QString & filenameSelect) if(auto header = mapService.loadMapHeader(resId)) { auto missingMods = CMapService::verifyMapHeaderMods(*header); - ModIncompatibility::ModList modList; + ModIncompatibility::ModListWithVersion modList; for(const auto & m : missingMods) - modList.push_back({m.first, m.second.toString()}); + modList.push_back({m.second.name, m.second.version.toString()}); if(!modList.empty()) - throw ModIncompatibility(std::move(modList)); + throw ModIncompatibility(modList); controller.setMap(mapService.loadMap(resId)); } } catch(const ModIncompatibility & e) { - QMessageBox::warning(this, "Mods requiered", e.what()); + assert(e.whatExcessive().empty()); + QMessageBox::warning(this, "Mods are requiered", QString::fromStdString(e.whatMissing())); return false; } catch(const std::exception & e) diff --git a/mapeditor/mapcontroller.cpp b/mapeditor/mapcontroller.cpp index f0eea1725..b1b86212a 100644 --- a/mapeditor/mapcontroller.cpp +++ b/mapeditor/mapcontroller.cpp @@ -588,7 +588,7 @@ ModCompatibilityInfo MapController::modAssessmentAll() auto handler = VLC->objtypeh->getHandlerFor(primaryID, secondaryID); auto modName = QString::fromStdString(handler->getJsonKey()).split(":").at(0).toStdString(); if(modName != "core") - result[modName] = VLC->modh->getModInfo(modName).version; + result[modName] = VLC->modh->getModInfo(modName).getVerificationInfo(); } } return result; @@ -605,7 +605,7 @@ ModCompatibilityInfo MapController::modAssessmentMap(const CMap & map) auto handler = VLC->objtypeh->getHandlerFor(obj->ID, obj->subID); auto modName = QString::fromStdString(handler->getJsonKey()).split(":").at(0).toStdString(); if(modName != "core") - result[modName] = VLC->modh->getModInfo(modName).version; + result[modName] = VLC->modh->getModInfo(modName).getVerificationInfo(); } //TODO: terrains? return result; diff --git a/mapeditor/mapcontroller.h b/mapeditor/mapcontroller.h index ad2cecad0..979c2c6d2 100644 --- a/mapeditor/mapcontroller.h +++ b/mapeditor/mapcontroller.h @@ -13,10 +13,10 @@ #include "maphandler.h" #include "mapview.h" -#include "../lib/modding/CModVersion.h" +#include "../lib/modding/CModInfo.h" VCMI_LIB_NAMESPACE_BEGIN -using ModCompatibilityInfo = std::map; +using ModCompatibilityInfo = std::map; class EditorObstaclePlacer; VCMI_LIB_NAMESPACE_END diff --git a/mapeditor/mapsettings/modsettings.cpp b/mapeditor/mapsettings/modsettings.cpp index f8121d918..5926542e5 100644 --- a/mapeditor/mapsettings/modsettings.cpp +++ b/mapeditor/mapsettings/modsettings.cpp @@ -47,7 +47,7 @@ void ModSettings::initialize(MapController & c) auto createModTreeWidgetItem = [&](QTreeWidgetItem * parent, const CModInfo & modInfo) { - auto item = new QTreeWidgetItem(parent, {QString::fromStdString(modInfo.name), QString::fromStdString(modInfo.version.toString())}); + auto item = new QTreeWidgetItem(parent, {QString::fromStdString(modInfo.getVerificationInfo().name), QString::fromStdString(modInfo.getVerificationInfo().version.toString())}); item->setData(0, Qt::UserRole, QVariant(QString::fromStdString(modInfo.identifier))); item->setFlags(item->flags() | Qt::ItemIsUserCheckable); item->setCheckState(0, controller->map()->mods.count(modInfo.identifier) ? Qt::Checked : Qt::Unchecked); @@ -104,7 +104,7 @@ void ModSettings::update() if(item->checkState(0) == Qt::Checked) { auto modName = item->data(0, Qt::UserRole).toString().toStdString(); - controller->map()->mods[modName] = VLC->modh->getModInfo(modName).version; + controller->map()->mods[modName] = VLC->modh->getModInfo(modName).getVerificationInfo(); } }; diff --git a/mapeditor/validator.cpp b/mapeditor/validator.cpp index e5cabf608..25783058d 100644 --- a/mapeditor/validator.cpp +++ b/mapeditor/validator.cpp @@ -174,7 +174,7 @@ std::list Validator::validate(const CMap * map) { if(!map->mods.count(mod.first)) { - issues.emplace_back(QString(tr("Map contains object from mod \"%1\", but doesn't require it")).arg(QString::fromStdString(VLC->modh->getModInfo(mod.first).name)), true); + issues.emplace_back(QString(tr("Map contains object from mod \"%1\", but doesn't require it")).arg(QString::fromStdString(VLC->modh->getModInfo(mod.first).getVerificationInfo().name)), true); } } } diff --git a/server/CGameHandler.cpp b/server/CGameHandler.cpp index 43dee22d4..f0b62bf52 100644 --- a/server/CGameHandler.cpp +++ b/server/CGameHandler.cpp @@ -1768,8 +1768,17 @@ bool CGameHandler::load(const std::string & filename) catch(const ModIncompatibility & e) { logGlobal->error("Failed to load game: %s", e.what()); - auto errorMsg = VLC->generaltexth->translate("vcmi.server.errors.modsIncompatibility") + '\n'; - errorMsg += e.what(); + std::string errorMsg; + if(!e.whatMissing().empty()) + { + errorMsg += VLC->generaltexth->translate("vcmi.server.errors.modsToEnable") + '\n'; + errorMsg += e.whatMissing(); + } + if(!e.whatExcessive().empty()) + { + errorMsg += VLC->generaltexth->translate("vcmi.server.errors.modsToDisable") + '\n'; + errorMsg += e.whatExcessive(); + } lobby->announceMessage(errorMsg); return false; }