From a05f8339aef8ba4ccb9a0a8b258e68b5b11f2642 Mon Sep 17 00:00:00 2001 From: nordsoft Date: Thu, 21 Sep 2023 04:31:08 +0200 Subject: [PATCH] Proper mod compatibility check system --- client/CServerHandler.cpp | 3 + lib/StartInfo.cpp | 2 +- lib/mapping/CMapHeader.h | 7 +-- lib/mapping/CMapService.cpp | 5 +- lib/mapping/CMapService.h | 5 +- lib/mapping/MapFormatJson.cpp | 21 +++++-- lib/modding/CModHandler.cpp | 82 ++++++++++++++++----------- lib/modding/CModHandler.h | 23 ++++---- lib/modding/CModInfo.cpp | 28 +++++---- lib/modding/CModInfo.h | 37 +++++++++--- lib/modding/ContentTypeHandler.cpp | 9 +-- mapeditor/mainwindow.cpp | 2 +- mapeditor/mapcontroller.cpp | 4 +- mapeditor/mapcontroller.h | 4 +- mapeditor/mapsettings/modsettings.cpp | 4 +- mapeditor/validator.cpp | 2 +- 16 files changed, 149 insertions(+), 89 deletions(-) diff --git a/client/CServerHandler.cpp b/client/CServerHandler.cpp index af1238c8c..06f5025b4 100644 --- a/client/CServerHandler.cpp +++ b/client/CServerHandler.cpp @@ -704,6 +704,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/lib/StartInfo.cpp b/lib/StartInfo.cpp index 7e3a5b91c..c8100987a 100644 --- a/lib/StartInfo.cpp +++ b/lib/StartInfo.cpp @@ -76,7 +76,7 @@ void LobbyInfo::verifyStateBeforeStart(bool ignoreNoHuman) const auto missingMods = CMapService::verifyMapHeaderMods(*mi->mapHeader); ModIncompatibility::ModList 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)); 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..5f5e9aff0 100644 --- a/lib/mapping/CMapService.cpp +++ b/lib/mapping/CMapService.cpp @@ -92,14 +92,15 @@ 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 modCompatibilityInfo; 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; } 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 c8aed2682..3e4c88c12 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"; @@ -953,7 +953,18 @@ 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.impactsGameplay = true; + + if(!mod["modId"].isNull()) + mapHeader->mods[mod["modId"].String()] = info; + else + mapHeader->mods[mod["name"].String()] = info; + } } //todo: multilevel map load support @@ -1294,8 +1305,10 @@ 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["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..ea8c7e681 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,47 +471,63 @@ 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 hasMod = [&modList](const TModID & m) -> bool + { + for(auto & i : modList) + if(i.first == m) + return true; + return false; + }; + for(const auto & m : activeMods) { - if (vstd::contains(saveActiveMods, m)) + if(hasMod(m)) continue; - auto & modInfo = allMods.at(m); - if(modInfo.checkModGameplayAffecting()) - missingMods.emplace_back(m, modInfo.version.toString()); + if(getModInfo(m).checkModGameplayAffecting()) + allMods[m].setEnabled(false); } - for(const auto & m : saveActiveMods) + std::vector newActiveMods; + ModIncompatibility::ModList missingMods; + + for(const auto & infoPair : modList) { - const CModVersion & mver = modList.at(m); - - if (allMods.count(m) == 0) + auto & remoteModId = infoPair.first; + auto & remoteModInfo = infoPair.second; + + if(!allMods.count(remoteModId)) { - missingMods.emplace_back(m, mver.toString()); + if(remoteModInfo.impactsGameplay) + missingMods.push_back({remoteModInfo.name, remoteModInfo.version.toString()}); //mod is not installed continue; } + + auto & localModInfo = getModInfo(remoteModId).getVerificationInfo(); - 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()); + bool 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) + { + newActiveMods.push_back(remoteModId); + continue; + } + + if(modAffectsGameplay || remoteModInfo.impactsGameplay) + missingMods.push_back({remoteModInfo.name, remoteModInfo.version.toString()}); //incompatible mod impacts gameplay } + + if(!missingMods.empty()) + throw ModIncompatibility(std::move(missingMods)); + + for(auto & m : newActiveMods) + allMods[m].setEnabled(true); std::swap(activeMods, newActiveMods); } 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..d0ea96546 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,17 @@ 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()); + if(!config["compatibility"].isNull()) { vcmiCompatibleMin = CModVersion::fromString(config["compatibility"]["min"].String()); @@ -61,7 +60,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 +82,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 +94,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 +103,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 +111,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 +126,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 +172,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..5fd687b75 100644 --- a/lib/modding/CModInfo.h +++ b/lib/modding/CModInfo.h @@ -29,17 +29,37 @@ 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; + + /// for serialization purposes + bool impactsGameplay = true; + + template + void serialize(Handler & h, const int v) + { + h & name; + h & version; + h & checksum; + 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 +72,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 +90,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 +99,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/mapeditor/mainwindow.cpp b/mapeditor/mainwindow.cpp index 14b6dfcba..3d23b923f 100644 --- a/mapeditor/mainwindow.cpp +++ b/mapeditor/mainwindow.cpp @@ -338,7 +338,7 @@ bool MainWindow::openMap(const QString & filenameSelect) auto missingMods = CMapService::verifyMapHeaderMods(*header); ModIncompatibility::ModList 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)); 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); } } }