modstatecontroller.cpp 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. /*
  2. * modstatecontroller.cpp, part of VCMI engine
  3. *
  4. * Authors: listed in file AUTHORS in main folder
  5. *
  6. * License: GNU General Public License v2.0 or later
  7. * Full text of license available in license.txt file, in main folder
  8. *
  9. */
  10. #include "StdInc.h"
  11. #include "modstatecontroller.h"
  12. #include "modstatemodel.h"
  13. #include "../../lib/VCMIDirs.h"
  14. #include "../../lib/filesystem/Filesystem.h"
  15. #include "../../lib/filesystem/CZipLoader.h"
  16. #include "../../lib/modding/CModHandler.h"
  17. #include "../../lib/modding/IdentifierStorage.h"
  18. #include "../../lib/json/JsonNode.h"
  19. #include "../vcmiqt/jsonutils.h"
  20. #include "../vcmiqt/launcherdirs.h"
  21. #include <future>
  22. namespace
  23. {
  24. QString detectModArchive(QString path, QString modName, std::vector<std::string> & filesToExtract)
  25. {
  26. try {
  27. ZipArchive archive(qstringToPath(path));
  28. filesToExtract = archive.listFiles();
  29. }
  30. catch (const std::runtime_error & e)
  31. {
  32. logGlobal->error("Failed to open zip archive. Reason: %s", e.what());
  33. return "";
  34. }
  35. QString modDirName;
  36. for(int folderLevel : {0, 1}) //search in subfolder if there is no mod.json in the root
  37. {
  38. for(auto file : filesToExtract)
  39. {
  40. QString filename = QString::fromUtf8(file.c_str());
  41. modDirName = filename.section('/', 0, folderLevel);
  42. if(filename == modDirName + "/mod.json")
  43. {
  44. return modDirName;
  45. }
  46. }
  47. }
  48. logGlobal->error("Failed to detect mod path in archive!");
  49. logGlobal->debug("List of file in archive:");
  50. for(auto file : filesToExtract)
  51. logGlobal->debug("%s", file.c_str());
  52. return "";
  53. }
  54. }
  55. ModStateController::ModStateController(std::shared_ptr<ModStateModel> modList)
  56. : modList(modList)
  57. {
  58. }
  59. ModStateController::~ModStateController() = default;
  60. void ModStateController::appendRepositories(const JsonNode & repomap)
  61. {
  62. modList->appendRepositories(repomap);
  63. }
  64. //void ModStateController::loadMods()
  65. //{
  66. // CModHandler handler;
  67. // auto installedMods = handler.getAllMods();
  68. // localMods.clear();
  69. //
  70. // for(auto modname : installedMods)
  71. // {
  72. // //calculate mod size
  73. // qint64 total = 0;
  74. // ResourcePath resDir(CModInfo::getModDir(modname), EResType::DIRECTORY);
  75. // if(CResourceHandler::get()->existsResource(resDir))
  76. // {
  77. // for(QDirIterator iter(QString::fromStdString(CResourceHandler::get()->getResourceName(resDir)->string()), QDirIterator::Subdirectories); iter.hasNext(); iter.next())
  78. // total += iter.fileInfo().size();
  79. // }
  80. //
  81. // boost::filesystem::path name = *CResourceHandler::get()->getResourceName(resID);
  82. // auto mod = JsonUtils::JsonFromFile(pathToQString(name));
  83. // auto json = JsonUtils::toJson(mod);
  84. // json["localSizeBytes"].Float() = total;
  85. // if(!name.is_absolute())
  86. // json["storedLocally"].Bool() = true;
  87. //
  88. // mod = JsonUtils::toVariant(json);
  89. // QString modNameQt = QString::fromUtf8(modname.c_str()).toLower();
  90. // localMods.insert(modNameQt, mod);
  91. // modSettings->registerNewMod(modNameQt, json["keepDisabled"].Bool());
  92. // }
  93. // modList->setLocalModList(localMods);
  94. //}
  95. bool ModStateController::addError(QString modname, QString message)
  96. {
  97. recentErrors.push_back(QString("%1: %2").arg(modname).arg(message));
  98. return false;
  99. }
  100. QStringList ModStateController::getErrors()
  101. {
  102. QStringList ret = recentErrors;
  103. recentErrors.clear();
  104. return ret;
  105. }
  106. bool ModStateController::installMod(QString modname, QString archivePath)
  107. {
  108. return canInstallMod(modname) && doInstallMod(modname, archivePath);
  109. }
  110. bool ModStateController::uninstallMod(QString modname)
  111. {
  112. return canUninstallMod(modname) && doUninstallMod(modname);
  113. }
  114. bool ModStateController::enableMod(QString modname)
  115. {
  116. return canEnableMod(modname) && doEnableMod(modname, true);
  117. }
  118. bool ModStateController::disableMod(QString modname)
  119. {
  120. return canDisableMod(modname) && doEnableMod(modname, false);
  121. }
  122. bool ModStateController::canInstallMod(QString modname)
  123. {
  124. auto mod = modList->getMod(modname);
  125. if(mod.isSubmod())
  126. return addError(modname, tr("Can not install submod"));
  127. if(mod.isInstalled())
  128. return addError(modname, tr("Mod is already installed"));
  129. return true;
  130. }
  131. bool ModStateController::canUninstallMod(QString modname)
  132. {
  133. auto mod = modList->getMod(modname);
  134. if(mod.isSubmod())
  135. return addError(modname, tr("Can not uninstall submod"));
  136. if(!mod.isInstalled())
  137. return addError(modname, tr("Mod is not installed"));
  138. return true;
  139. }
  140. bool ModStateController::canEnableMod(QString modname)
  141. {
  142. auto mod = modList->getMod(modname);
  143. if(modList->isModEnabled(modname))
  144. return addError(modname, tr("Mod is already enabled"));
  145. if(!mod.isInstalled())
  146. return addError(modname, tr("Mod must be installed first"));
  147. //check for compatibility
  148. if(!mod.isCompatible())
  149. return addError(modname, tr("Mod is not compatible, please update VCMI and checkout latest mod revisions"));
  150. for(auto modEntry : mod.getDependencies())
  151. {
  152. if(!modList->isModExists(modEntry)) // required mod is not available
  153. return addError(modname, tr("Required mod %1 is missing").arg(modEntry));
  154. ModState modData = modList->getMod(modEntry);
  155. if(!modData.isCompatibility() && !modList->isModEnabled(modEntry))
  156. return addError(modname, tr("Required mod %1 is not enabled").arg(modEntry));
  157. }
  158. for(QString modEntry : modList->getAllMods())
  159. {
  160. auto mod = modList->getMod(modEntry);
  161. // "reverse conflict" - enabled mod has this one as conflict
  162. if(modList->isModEnabled(modname) && mod.getConflicts().contains(modname))
  163. return addError(modname, tr("This mod conflicts with %1").arg(modEntry));
  164. }
  165. for(auto modEntry : mod.getConflicts())
  166. {
  167. // check if conflicting mod installed and enabled
  168. if(modList->isModExists(modEntry) && modList->isModEnabled(modEntry))
  169. return addError(modname, tr("This mod conflicts with %1").arg(modEntry));
  170. }
  171. return true;
  172. }
  173. bool ModStateController::canDisableMod(QString modname)
  174. {
  175. auto mod = modList->getMod(modname);
  176. if(!modList->isModEnabled(modname))
  177. return addError(modname, tr("Mod is already disabled"));
  178. if(!mod.isInstalled())
  179. return addError(modname, tr("Mod must be installed first"));
  180. for(QString modEntry : modList->getAllMods())
  181. {
  182. auto current = modList->getMod(modEntry);
  183. if(current.getDependencies().contains(modname) && modList->isModEnabled(modEntry))
  184. return addError(modname, tr("This mod is needed to run %1").arg(modEntry));
  185. }
  186. return true;
  187. }
  188. bool ModStateController::doEnableMod(QString mod, bool on)
  189. {
  190. //modSettings->setModActive(mod, on);
  191. //modList->modChanged(mod);
  192. return true;
  193. }
  194. bool ModStateController::doInstallMod(QString modname, QString archivePath)
  195. {
  196. const auto destDir = CLauncherDirs::modsPath() + QChar{'/'};
  197. if(!QFile(archivePath).exists())
  198. return addError(modname, tr("Mod archive is missing"));
  199. if(localMods.contains(modname))
  200. return addError(modname, tr("Mod with such name is already installed"));
  201. std::vector<std::string> filesToExtract;
  202. QString modDirName = ::detectModArchive(archivePath, modname, filesToExtract);
  203. if(!modDirName.size())
  204. return addError(modname, tr("Mod archive is invalid or corrupted"));
  205. std::atomic<int> filesCounter = 0;
  206. auto futureExtract = std::async(std::launch::async, [&archivePath, &destDir, &filesCounter, &filesToExtract]()
  207. {
  208. const auto destDirFsPath = qstringToPath(destDir);
  209. ZipArchive archive(qstringToPath(archivePath));
  210. for (auto const & file : filesToExtract)
  211. {
  212. if (!archive.extract(destDirFsPath, file))
  213. return false;
  214. ++filesCounter;
  215. }
  216. return true;
  217. });
  218. while(futureExtract.wait_for(std::chrono::milliseconds(10)) != std::future_status::ready)
  219. {
  220. emit extractionProgress(filesCounter, filesToExtract.size());
  221. qApp->processEvents();
  222. }
  223. if(!futureExtract.get())
  224. {
  225. removeModDir(destDir + modDirName);
  226. return addError(modname, tr("Failed to extract mod data"));
  227. }
  228. //rename folder and fix the path
  229. QDir extractedDir(destDir + modDirName);
  230. auto rc = QFile::rename(destDir + modDirName, destDir + modname);
  231. if (rc)
  232. extractedDir.setPath(destDir + modname);
  233. //there are possible excessive files - remove them
  234. QString upperLevel = modDirName.section('/', 0, 0);
  235. if(upperLevel != modDirName)
  236. removeModDir(destDir + upperLevel);
  237. CResourceHandler::get("initial")->updateFilteredFiles([](const std::string &) { return true; });
  238. //loadMods();
  239. //modList->reloadRepositories();
  240. return true;
  241. }
  242. bool ModStateController::doUninstallMod(QString modname)
  243. {
  244. ResourcePath resID(std::string("Mods/") + modname.toStdString(), EResType::DIRECTORY);
  245. // Get location of the mod, in case-insensitive way
  246. QString modDir = pathToQString(*CResourceHandler::get()->getResourceName(resID));
  247. if(!QDir(modDir).exists())
  248. return addError(modname, tr("Data with this mod was not found"));
  249. QDir modFullDir(modDir);
  250. if(!removeModDir(modDir))
  251. return addError(modname, tr("Mod is located in protected directory, please remove it manually:\n") + modFullDir.absolutePath());
  252. CResourceHandler::get("initial")->updateFilteredFiles([](const std::string &){ return true; });
  253. //loadMods();
  254. //modList->reloadRepositories();
  255. return true;
  256. }
  257. bool ModStateController::removeModDir(QString path)
  258. {
  259. // issues 2673 and 2680 its why you do not recursively remove without sanity check
  260. QDir checkDir(path);
  261. QDir dir(path);
  262. if(!checkDir.cdUp() || QString::compare("Mods", checkDir.dirName(), Qt::CaseInsensitive))
  263. return false;
  264. #ifndef VCMI_MOBILE // ios and android applications are stored in the isolated container
  265. if(!checkDir.cdUp() || QString::compare("vcmi", checkDir.dirName(), Qt::CaseInsensitive))
  266. return false;
  267. if(!dir.absolutePath().contains("vcmi", Qt::CaseInsensitive))
  268. return false;
  269. #endif
  270. if(!dir.absolutePath().contains("Mods", Qt::CaseInsensitive))
  271. return false;
  272. return dir.removeRecursively();
  273. }