CModHandler.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. /*
  2. * CModHandler.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 "CModHandler.h"
  12. #include "CModInfo.h"
  13. #include "ModScope.h"
  14. #include "ContentTypeHandler.h"
  15. #include "IdentifierStorage.h"
  16. #include "ModIncompatibility.h"
  17. #include "../CCreatureHandler.h"
  18. #include "../CGeneralTextHandler.h"
  19. #include "../CStopWatch.h"
  20. #include "../GameSettings.h"
  21. #include "../Languages.h"
  22. #include "../MetaString.h"
  23. #include "../ScriptHandler.h"
  24. #include "../constants/StringConstants.h"
  25. #include "../filesystem/Filesystem.h"
  26. #include "../json/JsonUtils.h"
  27. #include "../spells/CSpellHandler.h"
  28. #include "../VCMI_Lib.h"
  29. VCMI_LIB_NAMESPACE_BEGIN
  30. static JsonNode loadModSettings(const JsonPath & path)
  31. {
  32. if (CResourceHandler::get("local")->existsResource(ResourcePath(path)))
  33. {
  34. return JsonNode(path);
  35. }
  36. // Probably new install. Create initial configuration
  37. CResourceHandler::get("local")->createResource(path.getOriginalName() + ".json");
  38. return JsonNode();
  39. }
  40. CModHandler::CModHandler()
  41. : content(std::make_shared<CContentHandler>())
  42. , coreMod(std::make_unique<CModInfo>())
  43. {
  44. }
  45. CModHandler::~CModHandler() = default;
  46. // currentList is passed by value to get current list of depending mods
  47. bool CModHandler::hasCircularDependency(const TModID & modID, std::set<TModID> currentList) const
  48. {
  49. const CModInfo & mod = allMods.at(modID);
  50. // Mod already present? We found a loop
  51. if (vstd::contains(currentList, modID))
  52. {
  53. logMod->error("Error: Circular dependency detected! Printing dependency list:");
  54. logMod->error("\t%s -> ", mod.getVerificationInfo().name);
  55. return true;
  56. }
  57. currentList.insert(modID);
  58. // recursively check every dependency of this mod
  59. for(const TModID & dependency : mod.dependencies)
  60. {
  61. if (hasCircularDependency(dependency, currentList))
  62. {
  63. logMod->error("\t%s ->\n", mod.getVerificationInfo().name); // conflict detected, print dependency list
  64. return true;
  65. }
  66. }
  67. return false;
  68. }
  69. // Returned vector affects the resource loaders call order (see CFilesystemList::load).
  70. // The loaders call order matters when dependent mod overrides resources in its dependencies.
  71. std::vector <TModID> CModHandler::validateAndSortDependencies(std::vector <TModID> modsToResolve) const
  72. {
  73. // Topological sort algorithm.
  74. // TODO: Investigate possible ways to improve performance.
  75. boost::range::sort(modsToResolve); // Sort mods per name
  76. std::vector <TModID> sortedValidMods; // Vector keeps order of elements (LIFO)
  77. sortedValidMods.reserve(modsToResolve.size()); // push_back calls won't cause memory reallocation
  78. std::set <TModID> resolvedModIDs; // Use a set for validation for performance reason, but set does not keep order of elements
  79. // Mod is resolved if it has not dependencies or all its dependencies are already resolved
  80. auto isResolved = [&](const CModInfo & mod) -> bool
  81. {
  82. if(mod.dependencies.size() > resolvedModIDs.size())
  83. return false;
  84. for(const TModID & dependency : mod.dependencies)
  85. {
  86. if(!vstd::contains(resolvedModIDs, dependency))
  87. return false;
  88. }
  89. for(const TModID & conflict : mod.conflicts)
  90. {
  91. if(vstd::contains(resolvedModIDs, conflict))
  92. return false;
  93. }
  94. for(const TModID & reverseConflict : resolvedModIDs)
  95. {
  96. if (vstd::contains(allMods.at(reverseConflict).conflicts, mod.identifier))
  97. return false;
  98. }
  99. return true;
  100. };
  101. while(true)
  102. {
  103. std::set <TModID> resolvedOnCurrentTreeLevel;
  104. for(auto it = modsToResolve.begin(); it != modsToResolve.end();) // One iteration - one level of mods tree
  105. {
  106. if(isResolved(allMods.at(*it)))
  107. {
  108. resolvedOnCurrentTreeLevel.insert(*it); // Not to the resolvedModIDs, so current node childs will be resolved on the next iteration
  109. sortedValidMods.push_back(*it);
  110. it = modsToResolve.erase(it);
  111. continue;
  112. }
  113. it++;
  114. }
  115. if(!resolvedOnCurrentTreeLevel.empty())
  116. {
  117. resolvedModIDs.insert(resolvedOnCurrentTreeLevel.begin(), resolvedOnCurrentTreeLevel.end());
  118. continue;
  119. }
  120. // If there're no valid mods on the current mods tree level, no more mod can be resolved, should be end.
  121. break;
  122. }
  123. modLoadErrors = std::make_unique<MetaString>();
  124. auto addErrorMessage = [this](const std::string & textID, const std::string & brokenModID, const std::string & missingModID)
  125. {
  126. modLoadErrors->appendTextID(textID);
  127. if (allMods.count(brokenModID))
  128. modLoadErrors->replaceRawString(allMods.at(brokenModID).getVerificationInfo().name);
  129. else
  130. modLoadErrors->replaceRawString(brokenModID);
  131. if (allMods.count(missingModID))
  132. modLoadErrors->replaceRawString(allMods.at(missingModID).getVerificationInfo().name);
  133. else
  134. modLoadErrors->replaceRawString(missingModID);
  135. };
  136. // Left mods have unresolved dependencies, output all to log.
  137. for(const auto & brokenModID : modsToResolve)
  138. {
  139. const CModInfo & brokenMod = allMods.at(brokenModID);
  140. for(const TModID & dependency : brokenMod.dependencies)
  141. {
  142. if(!vstd::contains(resolvedModIDs, dependency) && brokenMod.config["modType"].String() != "Compatibility")
  143. addErrorMessage("vcmi.server.errors.modNoDependency", brokenModID, dependency);
  144. }
  145. for(const TModID & conflict : brokenMod.conflicts)
  146. {
  147. if(vstd::contains(resolvedModIDs, conflict))
  148. addErrorMessage("vcmi.server.errors.modConflict", brokenModID, conflict);
  149. }
  150. for(const TModID & reverseConflict : resolvedModIDs)
  151. {
  152. if (vstd::contains(allMods.at(reverseConflict).conflicts, brokenModID))
  153. addErrorMessage("vcmi.server.errors.modConflict", brokenModID, reverseConflict);
  154. }
  155. }
  156. return sortedValidMods;
  157. }
  158. std::vector<std::string> CModHandler::getModList(const std::string & path) const
  159. {
  160. std::string modDir = boost::to_upper_copy(path + "MODS/");
  161. size_t depth = boost::range::count(modDir, '/');
  162. auto list = CResourceHandler::get("initial")->getFilteredFiles([&](const ResourcePath & id) -> bool
  163. {
  164. if (id.getType() != EResType::DIRECTORY)
  165. return false;
  166. if (!boost::algorithm::starts_with(id.getName(), modDir))
  167. return false;
  168. if (boost::range::count(id.getName(), '/') != depth )
  169. return false;
  170. return true;
  171. });
  172. //storage for found mods
  173. std::vector<std::string> foundMods;
  174. for(const auto & entry : list)
  175. {
  176. std::string name = entry.getName();
  177. name.erase(0, modDir.size()); //Remove path prefix
  178. if (!name.empty())
  179. foundMods.push_back(name);
  180. }
  181. return foundMods;
  182. }
  183. void CModHandler::loadMods(const std::string & path, const std::string & parent, const JsonNode & modSettings, bool enableMods)
  184. {
  185. for(const std::string & modName : getModList(path))
  186. loadOneMod(modName, parent, modSettings, enableMods);
  187. }
  188. void CModHandler::loadOneMod(std::string modName, const std::string & parent, const JsonNode & modSettings, bool enableMods)
  189. {
  190. boost::to_lower(modName);
  191. std::string modFullName = parent.empty() ? modName : parent + '.' + modName;
  192. if ( ModScope::isScopeReserved(modFullName))
  193. {
  194. logMod->error("Can not load mod %s - this name is reserved for internal use!", modFullName);
  195. return;
  196. }
  197. if(CResourceHandler::get("initial")->existsResource(CModInfo::getModFile(modFullName)))
  198. {
  199. JsonParsingSettings settings;
  200. settings.mode = JsonParsingSettings::JsonFormatMode::JSON; // TODO: remove once Android launcher with its strict parser is gone
  201. CModInfo mod(modFullName, modSettings[modName], JsonNode(CModInfo::getModFile(modFullName), settings));
  202. if (!parent.empty()) // this is submod, add parent to dependencies
  203. mod.dependencies.insert(parent);
  204. allMods[modFullName] = mod;
  205. if (mod.isEnabled() && enableMods)
  206. activeMods.push_back(modFullName);
  207. loadMods(CModInfo::getModDir(modFullName) + '/', modFullName, modSettings[modName]["mods"], enableMods && mod.isEnabled());
  208. }
  209. }
  210. void CModHandler::loadMods()
  211. {
  212. JsonNode modConfig;
  213. modConfig = loadModSettings(JsonPath::builtin("config/modSettings.json"));
  214. loadMods("", "", modConfig["activeMods"], true);
  215. coreMod = std::make_unique<CModInfo>(ModScope::scopeBuiltin(), modConfig[ModScope::scopeBuiltin()], JsonNode(JsonPath::builtin("config/gameConfig.json")));
  216. }
  217. std::vector<std::string> CModHandler::getAllMods() const
  218. {
  219. std::vector<std::string> modlist;
  220. modlist.reserve(allMods.size());
  221. for (auto & entry : allMods)
  222. modlist.push_back(entry.first);
  223. return modlist;
  224. }
  225. std::vector<std::string> CModHandler::getActiveMods() const
  226. {
  227. return activeMods;
  228. }
  229. std::string CModHandler::getModLoadErrors() const
  230. {
  231. return modLoadErrors->toString();
  232. }
  233. const CModInfo & CModHandler::getModInfo(const TModID & modId) const
  234. {
  235. return allMods.at(modId);
  236. }
  237. static JsonNode genDefaultFS()
  238. {
  239. // default FS config for mods: directory "Content" that acts as H3 root directory
  240. JsonNode defaultFS;
  241. defaultFS[""].Vector().resize(2);
  242. defaultFS[""].Vector()[0]["type"].String() = "zip";
  243. defaultFS[""].Vector()[0]["path"].String() = "/Content.zip";
  244. defaultFS[""].Vector()[1]["type"].String() = "dir";
  245. defaultFS[""].Vector()[1]["path"].String() = "/Content";
  246. return defaultFS;
  247. }
  248. static ISimpleResourceLoader * genModFilesystem(const std::string & modName, const JsonNode & conf)
  249. {
  250. static const JsonNode defaultFS = genDefaultFS();
  251. if (!conf["filesystem"].isNull())
  252. return CResourceHandler::createFileSystem(CModInfo::getModDir(modName), conf["filesystem"]);
  253. else
  254. return CResourceHandler::createFileSystem(CModInfo::getModDir(modName), defaultFS);
  255. }
  256. static ui32 calculateModChecksum(const std::string & modName, ISimpleResourceLoader * filesystem)
  257. {
  258. boost::crc_32_type modChecksum;
  259. // first - add current VCMI version into checksum to force re-validation on VCMI updates
  260. modChecksum.process_bytes(reinterpret_cast<const void*>(GameConstants::VCMI_VERSION.data()), GameConstants::VCMI_VERSION.size());
  261. // second - add mod.json into checksum because filesystem does not contains this file
  262. // FIXME: remove workaround for core mod
  263. if (modName != ModScope::scopeBuiltin())
  264. {
  265. auto modConfFile = CModInfo::getModFile(modName);
  266. ui32 configChecksum = CResourceHandler::get("initial")->load(modConfFile)->calculateCRC32();
  267. modChecksum.process_bytes(reinterpret_cast<const void *>(&configChecksum), sizeof(configChecksum));
  268. }
  269. // third - add all detected text files from this mod into checksum
  270. auto files = filesystem->getFilteredFiles([](const ResourcePath & resID)
  271. {
  272. return (resID.getType() == EResType::TEXT || resID.getType() == EResType::JSON) &&
  273. ( boost::starts_with(resID.getName(), "DATA") || boost::starts_with(resID.getName(), "CONFIG"));
  274. });
  275. for (const ResourcePath & file : files)
  276. {
  277. ui32 fileChecksum = filesystem->load(file)->calculateCRC32();
  278. modChecksum.process_bytes(reinterpret_cast<const void *>(&fileChecksum), sizeof(fileChecksum));
  279. }
  280. return modChecksum.checksum();
  281. }
  282. void CModHandler::loadModFilesystems()
  283. {
  284. CGeneralTextHandler::detectInstallParameters();
  285. activeMods = validateAndSortDependencies(activeMods);
  286. coreMod->updateChecksum(calculateModChecksum(ModScope::scopeBuiltin(), CResourceHandler::get(ModScope::scopeBuiltin())));
  287. for(std::string & modName : activeMods)
  288. {
  289. CModInfo & mod = allMods[modName];
  290. CResourceHandler::addFilesystem("data", modName, genModFilesystem(modName, mod.config));
  291. }
  292. }
  293. TModID CModHandler::findResourceOrigin(const ResourcePath & name) const
  294. {
  295. try
  296. {
  297. for(const auto & modID : boost::adaptors::reverse(activeMods))
  298. {
  299. if(CResourceHandler::get(modID)->existsResource(name))
  300. return modID;
  301. }
  302. if(CResourceHandler::get("core")->existsResource(name))
  303. return "core";
  304. if(CResourceHandler::get("mapEditor")->existsResource(name))
  305. return "core"; // Workaround for loading maps via map editor
  306. }
  307. catch( const std::out_of_range & e)
  308. {
  309. // no-op
  310. }
  311. throw std::runtime_error("Resource with name " + name.getName() + " and type " + EResTypeHelper::getEResTypeAsString(name.getType()) + " wasn't found.");
  312. }
  313. std::string CModHandler::getModLanguage(const TModID& modId) const
  314. {
  315. if(modId == "core")
  316. return VLC->generaltexth->getInstalledLanguage();
  317. if(modId == "map")
  318. return VLC->generaltexth->getPreferredLanguage();
  319. return allMods.at(modId).baseLanguage;
  320. }
  321. std::set<TModID> CModHandler::getModDependencies(const TModID & modId, bool & isModFound) const
  322. {
  323. auto it = allMods.find(modId);
  324. isModFound = (it != allMods.end());
  325. if(isModFound)
  326. return it->second.dependencies;
  327. logMod->error("Mod not found: '%s'", modId);
  328. return {};
  329. }
  330. void CModHandler::initializeConfig()
  331. {
  332. VLC->settingsHandler->load(coreMod->config["settings"]);
  333. for(const TModID & modName : activeMods)
  334. {
  335. const auto & mod = allMods[modName];
  336. if (!mod.config["settings"].isNull())
  337. VLC->settingsHandler->load(mod.config["settings"]);
  338. }
  339. }
  340. CModVersion CModHandler::getModVersion(TModID modName) const
  341. {
  342. if (allMods.count(modName))
  343. return allMods.at(modName).getVerificationInfo().version;
  344. return {};
  345. }
  346. bool CModHandler::validateTranslations(TModID modName) const
  347. {
  348. bool result = true;
  349. const auto & mod = allMods.at(modName);
  350. {
  351. auto fileList = mod.config["translations"].convertTo<std::vector<std::string> >();
  352. JsonNode json = JsonUtils::assembleFromFiles(fileList);
  353. result |= VLC->generaltexth->validateTranslation(mod.baseLanguage, modName, json);
  354. }
  355. for(const auto & language : Languages::getLanguageList())
  356. {
  357. if (!language.hasTranslation)
  358. continue;
  359. if (mod.config[language.identifier].isNull())
  360. continue;
  361. if (mod.config[language.identifier]["skipValidation"].Bool())
  362. continue;
  363. auto fileList = mod.config[language.identifier]["translations"].convertTo<std::vector<std::string> >();
  364. JsonNode json = JsonUtils::assembleFromFiles(fileList);
  365. result |= VLC->generaltexth->validateTranslation(language.identifier, modName, json);
  366. }
  367. return result;
  368. }
  369. void CModHandler::loadTranslation(const TModID & modName)
  370. {
  371. const auto & mod = allMods[modName];
  372. std::string preferredLanguage = VLC->generaltexth->getPreferredLanguage();
  373. std::string modBaseLanguage = allMods[modName].baseLanguage;
  374. auto baseTranslationList = mod.config["translations"].convertTo<std::vector<std::string> >();
  375. auto extraTranslationList = mod.config[preferredLanguage]["translations"].convertTo<std::vector<std::string> >();
  376. JsonNode baseTranslation = JsonUtils::assembleFromFiles(baseTranslationList);
  377. JsonNode extraTranslation = JsonUtils::assembleFromFiles(extraTranslationList);
  378. VLC->generaltexth->loadTranslationOverrides(modBaseLanguage, modName, baseTranslation);
  379. VLC->generaltexth->loadTranslationOverrides(preferredLanguage, modName, extraTranslation);
  380. }
  381. void CModHandler::load()
  382. {
  383. CStopWatch totalTime;
  384. CStopWatch timer;
  385. logMod->info("\tInitializing content handler: %d ms", timer.getDiff());
  386. content->init();
  387. for(const TModID & modName : activeMods)
  388. {
  389. logMod->trace("Generating checksum for %s", modName);
  390. allMods[modName].updateChecksum(calculateModChecksum(modName, CResourceHandler::get(modName)));
  391. }
  392. // first - load virtual builtin mod that contains all data
  393. // TODO? move all data into real mods? RoE, AB, SoD, WoG
  394. content->preloadData(*coreMod);
  395. for(const TModID & modName : activeMods)
  396. content->preloadData(allMods[modName]);
  397. logMod->info("\tParsing mod data: %d ms", timer.getDiff());
  398. content->load(*coreMod);
  399. for(const TModID & modName : activeMods)
  400. content->load(allMods[modName]);
  401. #if SCRIPTING_ENABLED
  402. VLC->scriptHandler->performRegistration(VLC);//todo: this should be done before any other handlers load
  403. #endif
  404. content->loadCustom();
  405. for(const TModID & modName : activeMods)
  406. loadTranslation(modName);
  407. #if 0
  408. for(const TModID & modName : activeMods)
  409. if (!validateTranslations(modName))
  410. allMods[modName].validation = CModInfo::FAILED;
  411. #endif
  412. logMod->info("\tLoading mod data: %d ms", timer.getDiff());
  413. VLC->creh->loadCrExpMod();
  414. VLC->identifiersHandler->finalize();
  415. logMod->info("\tResolving identifiers: %d ms", timer.getDiff());
  416. content->afterLoadFinalization();
  417. logMod->info("\tHandlers post-load finalization: %d ms ", timer.getDiff());
  418. logMod->info("\tAll game content loaded in %d ms", totalTime.getDiff());
  419. }
  420. void CModHandler::afterLoad(bool onlyEssential)
  421. {
  422. JsonNode modSettings;
  423. for (auto & modEntry : allMods)
  424. {
  425. std::string pointer = "/" + boost::algorithm::replace_all_copy(modEntry.first, ".", "/mods/");
  426. modSettings["activeMods"].resolvePointer(pointer) = modEntry.second.saveLocalData();
  427. }
  428. modSettings[ModScope::scopeBuiltin()] = coreMod->saveLocalData();
  429. modSettings[ModScope::scopeBuiltin()]["name"].String() = "Original game files";
  430. if(!onlyEssential)
  431. {
  432. std::fstream file(CResourceHandler::get()->getResourceName(ResourcePath("config/modSettings.json"))->c_str(), std::ofstream::out | std::ofstream::trunc);
  433. file << modSettings.toString();
  434. }
  435. }
  436. VCMI_LIB_NAMESPACE_END