addon_manager.cpp 28 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022
  1. // SuperTux - Add-on Manager
  2. // Copyright (C) 2007 Christoph Sommer <christoph.sommer@2007.expires.deltadevelopment.de>
  3. // 2014 Ingo Ruhnke <grumbel@gmail.com>
  4. //
  5. // This program is free software: you can redistribute it and/or modify
  6. // it under the terms of the GNU General Public License as published by
  7. // the Free Software Foundation, either version 3 of the License, or
  8. // (at your option) any later version.
  9. //
  10. // This program is distributed in the hope that it will be useful,
  11. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. // GNU General Public License for more details.
  14. //
  15. // You should have received a copy of the GNU General Public License
  16. // along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. #include "addon/addon_manager.hpp"
  18. #include <physfs.h>
  19. #include <fmt/format.h>
  20. #include <sstream>
  21. #include "addon/addon.hpp"
  22. #include "addon/md5.hpp"
  23. #include "gui/dialog.hpp"
  24. #include "physfs/util.hpp"
  25. #include "supertux/globals.hpp"
  26. #include "supertux/menu/addon_menu.hpp"
  27. #include "supertux/menu/menu_storage.hpp"
  28. #include "util/file_system.hpp"
  29. #include "util/gettext.hpp"
  30. #include "util/log.hpp"
  31. #include "util/reader.hpp"
  32. #include "util/reader_collection.hpp"
  33. #include "util/reader_document.hpp"
  34. #include "util/reader_mapping.hpp"
  35. #include "util/string_util.hpp"
  36. namespace {
  37. static const char* ADDON_INFO_PATH = "/addons/repository.nfo";
  38. static const char* ADDON_REPOSITORY_URL = "https://raw.githubusercontent.com/SuperTux/addons/master/index-0_6.nfo";
  39. MD5 md5_from_file(const std::string& filename)
  40. {
  41. // TODO: This does not work as expected for some files -- IFileStream seems to not always behave like an ifstream.
  42. //IFileStream ifs(installed_physfs_filename);
  43. //std::string md5 = MD5(ifs).hex_digest();
  44. MD5 md5;
  45. auto file = PHYSFS_openRead(filename.c_str());
  46. if (!file)
  47. {
  48. std::ostringstream out;
  49. out << "PHYSFS_openRead() failed: " << physfsutil::get_last_error();
  50. throw std::runtime_error(out.str());
  51. }
  52. else
  53. {
  54. while (true)
  55. {
  56. unsigned char buffer[1024];
  57. PHYSFS_sint64 len = PHYSFS_readBytes(file, buffer, sizeof(buffer));
  58. if (len <= 0) break;
  59. md5.update(buffer, static_cast<unsigned int>(len));
  60. }
  61. PHYSFS_close(file);
  62. return md5;
  63. }
  64. }
  65. MD5 md5_from_archive(const std::string& filename)
  66. {
  67. if (physfsutil::is_directory(filename)) {
  68. return MD5();
  69. } else {
  70. return md5_from_file(filename);
  71. }
  72. }
  73. static Addon& get_addon(const AddonManager::AddonMap& list, const AddonId& id,
  74. bool installed)
  75. {
  76. auto it = list.find(id);
  77. if (it != list.end())
  78. {
  79. return *(it->second);
  80. }
  81. else
  82. {
  83. std::string type = installed ? "installed" : "repository";
  84. throw std::runtime_error("Couldn't find " + type + " addon with id: " + id);
  85. }
  86. }
  87. static std::vector<AddonId> get_addons(const AddonManager::AddonMap& list)
  88. {
  89. // Use a map for storing sorted add-on titles with their respective IDs.
  90. std::map<std::string, AddonId> sorted_titles;
  91. for (const auto& [id, addon] : list)
  92. {
  93. sorted_titles.insert({addon->get_title(), id});
  94. }
  95. std::vector<AddonId> results;
  96. results.reserve(sorted_titles.size());
  97. std::transform(sorted_titles.begin(), sorted_titles.end(),
  98. std::back_inserter(results),
  99. [](const auto& title_and_id)
  100. {
  101. return title_and_id.second;
  102. });
  103. return results;
  104. }
  105. static PHYSFS_EnumerateCallbackResult add_to_dictionary_path(void *data, const char *origdir, const char *fname)
  106. {
  107. std::string full_path = FileSystem::join(origdir, fname);
  108. if (physfsutil::is_directory(full_path))
  109. {
  110. log_debug << "Adding \"" << full_path << "\" to dictionary search path" << std::endl;
  111. // We want translations from add-ons to have precedence.
  112. g_dictionary_manager->add_directory(full_path, true);
  113. }
  114. return PHYSFS_ENUM_OK;
  115. }
  116. static PHYSFS_EnumerateCallbackResult remove_from_dictionary_path(void *data, const char *origdir, const char *fname)
  117. {
  118. std::string full_path = FileSystem::join(origdir, fname);
  119. if (physfsutil::is_directory(full_path))
  120. {
  121. g_dictionary_manager->remove_directory(full_path);
  122. }
  123. return PHYSFS_ENUM_OK;
  124. }
  125. } // namespace
  126. AddonManager::AddonManager(const std::string& addon_directory,
  127. std::vector<Config::Addon>& addon_config) :
  128. m_downloader(),
  129. m_addon_directory(addon_directory),
  130. m_cache_directory(FileSystem::join(m_addon_directory, "cache")),
  131. m_screenshots_cache_directory(FileSystem::join(m_cache_directory, "screenshots")),
  132. m_repository_url(ADDON_REPOSITORY_URL),
  133. m_addon_config(addon_config),
  134. m_installed_addons(),
  135. m_repository_addons(),
  136. m_initialized(false),
  137. m_has_been_updated(false),
  138. m_transfer_statuses(new TransferStatusList)
  139. {
  140. if (!PHYSFS_mkdir(m_addon_directory.c_str()))
  141. {
  142. std::ostringstream msg;
  143. msg << "Couldn't create directory for addons '"
  144. << m_addon_directory << "': " << physfsutil::get_last_error();
  145. throw std::runtime_error(msg.str());
  146. }
  147. add_installed_addons();
  148. // FIXME: We should also restore the order here.
  149. for (auto& addon : m_addon_config)
  150. {
  151. if (addon.enabled)
  152. {
  153. try
  154. {
  155. enable_addon(addon.id);
  156. }
  157. catch(const std::exception& err)
  158. {
  159. log_warning << "Failed to enable addon '" << addon.id << "' from config: " << err.what() << std::endl;
  160. }
  161. }
  162. }
  163. if (PHYSFS_exists(ADDON_INFO_PATH))
  164. {
  165. try
  166. {
  167. m_repository_addons = parse_addon_infos(ADDON_INFO_PATH);
  168. }
  169. catch(const std::exception& err)
  170. {
  171. log_warning << "parsing repository.nfo failed: " << err.what() << std::endl;
  172. }
  173. }
  174. else
  175. {
  176. log_info << "repository.nfo doesn't exist, not loading" << std::endl;
  177. }
  178. if (!g_config->repository_url.empty() &&
  179. g_config->repository_url != m_repository_url)
  180. {
  181. m_repository_url = g_config->repository_url;
  182. }
  183. // Create the add-on cache directory, if it doesn't exist.
  184. if (!PHYSFS_exists(m_cache_directory.c_str()))
  185. {
  186. PHYSFS_mkdir(m_cache_directory.c_str());
  187. }
  188. else
  189. {
  190. empty_cache_directory();
  191. }
  192. m_initialized = true;
  193. }
  194. AddonManager::~AddonManager()
  195. {
  196. // Sync enabled/disabled add-ons into the config for saving.
  197. m_addon_config.clear();
  198. for (const auto& [id, addon] : m_installed_addons)
  199. {
  200. m_addon_config.push_back({id, addon->is_enabled()});
  201. }
  202. // Delete the add-on cache directory, if it exists.
  203. physfsutil::remove_with_content(m_cache_directory);
  204. }
  205. void
  206. AddonManager::empty_cache_directory()
  207. {
  208. physfsutil::remove_content(m_cache_directory);
  209. }
  210. Addon&
  211. AddonManager::get_repository_addon(const AddonId& id) const
  212. {
  213. return get_addon(m_repository_addons, id, false);
  214. }
  215. Addon&
  216. AddonManager::get_installed_addon(const AddonId& id) const
  217. {
  218. return get_addon(m_installed_addons, id, true);
  219. }
  220. std::vector<AddonId>
  221. AddonManager::get_repository_addons() const
  222. {
  223. return get_addons(m_repository_addons);
  224. }
  225. std::vector<AddonId>
  226. AddonManager::get_installed_addons() const
  227. {
  228. return get_addons(m_installed_addons);
  229. }
  230. bool
  231. AddonManager::has_online_support() const
  232. {
  233. return true;
  234. }
  235. bool
  236. AddonManager::has_been_updated() const
  237. {
  238. return m_has_been_updated;
  239. }
  240. TransferStatusPtr
  241. AddonManager::request_check_online()
  242. {
  243. empty_cache_directory();
  244. TransferStatusPtr status = m_downloader.request_download(m_repository_url, ADDON_INFO_PATH);
  245. status->then(
  246. [this](bool success)
  247. {
  248. if (success)
  249. {
  250. m_repository_addons = parse_addon_infos(ADDON_INFO_PATH);
  251. m_has_been_updated = true;
  252. }
  253. });
  254. return status;
  255. }
  256. void
  257. AddonManager::check_online()
  258. {
  259. empty_cache_directory();
  260. m_downloader.download(m_repository_url, ADDON_INFO_PATH);
  261. m_repository_addons = parse_addon_infos(ADDON_INFO_PATH);
  262. m_has_been_updated = true;
  263. }
  264. TransferStatusListPtr
  265. AddonManager::request_install_addon(const AddonId& addon_id)
  266. {
  267. // Remove add-on if it already exists.
  268. auto it = m_installed_addons.find(addon_id);
  269. if (it != m_installed_addons.end())
  270. {
  271. log_debug << "Reinstalling add-on " << addon_id << std::endl;
  272. if (it->second->is_enabled())
  273. {
  274. disable_addon(it->first);
  275. }
  276. m_installed_addons.erase(it);
  277. }
  278. else
  279. {
  280. log_debug << "Installing add-on " << addon_id << std::endl;
  281. }
  282. auto& addon = get_repository_addon(addon_id);
  283. std::string install_filename = FileSystem::join(m_addon_directory, addon.get_filename());
  284. // Install add-on dependencies, if any.
  285. request_install_addon_dependencies(addon);
  286. // Install the add-on.
  287. TransferStatusPtr status = m_downloader.request_download(addon.get_url(), install_filename);
  288. status->then(
  289. [this, install_filename, addon_id](bool success)
  290. {
  291. if (success)
  292. {
  293. // Complete the add-on installation.
  294. Addon& repository_addon = get_repository_addon(addon_id);
  295. MD5 md5 = md5_from_file(install_filename);
  296. if (repository_addon.get_md5() != md5.hex_digest())
  297. {
  298. if (PHYSFS_delete(install_filename.c_str()) == 0)
  299. {
  300. log_warning << "PHYSFS_delete failed: " << physfsutil::get_last_error() << std::endl;
  301. }
  302. throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
  303. }
  304. else
  305. {
  306. const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
  307. if (!realdir)
  308. {
  309. throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
  310. }
  311. add_installed_archive(install_filename, md5.hex_digest());
  312. // Attempt to enable the add-on.
  313. try
  314. {
  315. enable_addon(repository_addon.get_id());
  316. }
  317. catch (const std::exception& err)
  318. {
  319. log_warning << "Enabling add-on failed: " << err.what() << std::endl;
  320. }
  321. }
  322. }
  323. });
  324. m_transfer_statuses->push(status);
  325. return m_transfer_statuses;
  326. }
  327. TransferStatusListPtr
  328. AddonManager::request_install_addon_dependencies(const Addon& addon)
  329. {
  330. for (const std::string& id : addon.get_dependencies())
  331. {
  332. if (is_addon_installed(id))
  333. continue; // Don't attempt to install add-ons that are already installed.
  334. try
  335. {
  336. get_repository_addon(id);
  337. }
  338. catch (...)
  339. {
  340. continue; // Don't attempt to install add-ons that are not available.
  341. }
  342. request_install_addon(id);
  343. }
  344. return m_transfer_statuses;
  345. }
  346. TransferStatusListPtr
  347. AddonManager::request_install_addon_dependencies(const AddonId& addon_id)
  348. {
  349. return request_install_addon_dependencies(get_repository_addon(addon_id));
  350. }
  351. void
  352. AddonManager::install_addon(const AddonId& addon_id)
  353. {
  354. { // remove addon if it already exists.
  355. auto it = m_installed_addons.find(addon_id);
  356. if (it != m_installed_addons.end())
  357. {
  358. log_debug << "Reinstalling add-on " << addon_id << std::endl;
  359. if (it->second->is_enabled())
  360. {
  361. disable_addon(it->first);
  362. }
  363. m_installed_addons.erase(it);
  364. }
  365. else
  366. {
  367. log_debug << "Installing add-on " << addon_id << std::endl;
  368. }
  369. }
  370. auto& repository_addon = get_repository_addon(addon_id);
  371. std::string install_filename = FileSystem::join(m_addon_directory, repository_addon.get_filename());
  372. m_downloader.download(repository_addon.get_url(), install_filename);
  373. MD5 md5 = md5_from_file(install_filename);
  374. if (repository_addon.get_md5() != md5.hex_digest())
  375. {
  376. if (PHYSFS_delete(install_filename.c_str()) == 0)
  377. {
  378. log_warning << "PHYSFS_delete failed: " << physfsutil::get_last_error() << std::endl;
  379. }
  380. throw std::runtime_error("Downloading Add-on failed: MD5 checksums differ");
  381. }
  382. else
  383. {
  384. const char* realdir = PHYSFS_getRealDir(install_filename.c_str());
  385. if (!realdir)
  386. {
  387. throw std::runtime_error("PHYSFS_getRealDir failed: " + install_filename);
  388. }
  389. else
  390. {
  391. add_installed_archive(install_filename, md5.hex_digest());
  392. }
  393. }
  394. }
  395. void
  396. AddonManager::install_addon_from_local_file(const std::string& filename)
  397. {
  398. const std::string& source_filename = FileSystem::basename(filename);
  399. if(!StringUtil::has_suffix(source_filename, ".zip"))
  400. return;
  401. const std::string& target_directory = FileSystem::join(PHYSFS_getRealDir(m_addon_directory.c_str()), m_addon_directory);
  402. const std::string& target_filename = FileSystem::join(target_directory, source_filename);
  403. const std::string& physfs_target_filename = FileSystem::join(m_addon_directory, source_filename);
  404. FileSystem::copy(filename, target_filename);
  405. MD5 target_md5 = md5_from_file(physfs_target_filename);
  406. add_installed_archive(physfs_target_filename, target_md5.hex_digest(), true);
  407. }
  408. void
  409. AddonManager::uninstall_addon(const AddonId& addon_id)
  410. {
  411. log_debug << "Uninstalling add-on " << addon_id << std::endl;
  412. auto& addon = get_installed_addon(addon_id);
  413. if (addon.is_enabled())
  414. {
  415. disable_addon(addon_id);
  416. }
  417. log_debug << "Deleting file \"" << addon.get_install_filename() << "\"" << std::endl;
  418. const auto it = m_installed_addons.find(addon.get_id());
  419. if (it != m_installed_addons.end())
  420. {
  421. if (PHYSFS_delete(FileSystem::join(m_addon_directory, addon.get_filename()).c_str()) == 0)
  422. {
  423. throw std::runtime_error(_("Error deleting addon .zip file: \"PHYSFS_delete\" failed: ") + std::string(physfsutil::get_last_error()));
  424. }
  425. m_installed_addons.erase(it);
  426. }
  427. else
  428. {
  429. throw std::runtime_error("Error uninstalling add-on: Addon with id " + addon_id + " not found.");
  430. }
  431. }
  432. TransferStatusListPtr
  433. AddonManager::request_download_addon_screenshots(const AddonId& addon_id)
  434. {
  435. // Create the add-on screenshots cache directory, if it doesn't exist.
  436. if (!PHYSFS_exists(m_screenshots_cache_directory.c_str()))
  437. {
  438. PHYSFS_mkdir(m_screenshots_cache_directory.c_str());
  439. }
  440. const auto& screenshots = get_repository_addon(addon_id).get_screenshots();
  441. for (size_t i = 0; i < screenshots.size(); i++)
  442. {
  443. const std::string filename = addon_id + "_" + std::to_string(i + 1) + FileSystem::extension(screenshots[i]);
  444. const std::string filepath = FileSystem::join(m_screenshots_cache_directory, filename);
  445. if (PHYSFS_exists(filepath.c_str())) continue; // Do not re-download existing screenshots.
  446. TransferStatusPtr status;
  447. try
  448. {
  449. status = m_downloader.request_download(screenshots[i], filepath);
  450. }
  451. catch (std::exception& err)
  452. {
  453. log_warning << "Error downloading add-on screenshot from URL '" << screenshots[i]
  454. << "' to file '" << filename << "': " << err.what() << std::endl;
  455. continue;
  456. }
  457. m_transfer_statuses->push(status);
  458. }
  459. return m_transfer_statuses;
  460. }
  461. std::vector<std::string>
  462. AddonManager::get_local_addon_screenshots(const AddonId& addon_id)
  463. {
  464. std::vector<std::string> screenshots;
  465. physfsutil::enumerate_files(m_screenshots_cache_directory, [&screenshots, &addon_id, this](const std::string& filename) {
  466. // Push any files from the cache directory, starting with the ID of the add-on.
  467. if (StringUtil::starts_with(filename, addon_id))
  468. {
  469. screenshots.push_back(FileSystem::join(m_screenshots_cache_directory, filename));
  470. }
  471. });
  472. return screenshots;
  473. }
  474. void
  475. AddonManager::enable_addon(const AddonId& addon_id)
  476. {
  477. log_debug << "enabling addon " << addon_id << std::endl;
  478. auto& addon = get_installed_addon(addon_id);
  479. if (addon.is_enabled())
  480. {
  481. throw std::runtime_error("Tried enabling already enabled add-on.");
  482. }
  483. else
  484. {
  485. if (addon.get_type() == Addon::RESOURCEPACK)
  486. {
  487. for (const auto& [id, installed_addon] : m_installed_addons)
  488. {
  489. if (installed_addon->get_type() == Addon::RESOURCEPACK &&
  490. installed_addon->is_enabled())
  491. {
  492. throw std::runtime_error(_("Only one resource pack is allowed to be enabled at a time."));
  493. }
  494. }
  495. }
  496. std::string mountpoint;
  497. switch (addon.get_format()) {
  498. case Addon::ORIGINAL:
  499. mountpoint = "";
  500. break;
  501. default:
  502. mountpoint = "custom/" + addon_id;
  503. break;
  504. }
  505. // Only mount resource packs on startup (AddonManager initialization).
  506. if (addon.get_type() == Addon::RESOURCEPACK && m_initialized)
  507. {
  508. addon.set_enabled(true);
  509. return;
  510. }
  511. log_debug << "Adding archive \"" << addon.get_install_filename() << "\" to search path" << std::endl;
  512. if (PHYSFS_mount(addon.get_install_filename().c_str(), mountpoint.c_str(), !addon.overrides_data()) == 0)
  513. {
  514. std::stringstream err;
  515. err << "Could not add " << addon.get_install_filename() << " to search path: "
  516. << physfsutil::get_last_error() << std::endl;
  517. throw std::runtime_error(err.str());
  518. }
  519. else
  520. {
  521. if (addon.get_type() == Addon::LANGUAGEPACK)
  522. {
  523. PHYSFS_enumerate(addon.get_id().c_str(), add_to_dictionary_path, nullptr);
  524. }
  525. addon.set_enabled(true);
  526. }
  527. }
  528. }
  529. void
  530. AddonManager::disable_addon(const AddonId& addon_id)
  531. {
  532. log_debug << "disabling addon " << addon_id << std::endl;
  533. auto& addon = get_installed_addon(addon_id);
  534. if (!addon.is_enabled())
  535. {
  536. throw std::runtime_error("Tried disabling already disabled add-on.");
  537. }
  538. else
  539. {
  540. // Don't unmount resource packs. Disabled resource packs will not be mounted on next startup.
  541. if (addon.get_type() == Addon::RESOURCEPACK)
  542. {
  543. addon.set_enabled(false);
  544. return;
  545. }
  546. log_debug << "Removing archive \"" << addon.get_install_filename() << "\" from search path" << std::endl;
  547. if (PHYSFS_unmount(addon.get_install_filename().c_str()) == 0)
  548. {
  549. std::stringstream err;
  550. err << "Could not remove " << addon.get_install_filename() << " from search path: "
  551. << physfsutil::get_last_error() << std::endl;
  552. throw std::runtime_error(err.str());
  553. }
  554. else
  555. {
  556. if (addon.get_type() == Addon::LANGUAGEPACK)
  557. {
  558. PHYSFS_enumerate(addon.get_id().c_str(), remove_from_dictionary_path, nullptr);
  559. }
  560. addon.set_enabled(false);
  561. }
  562. }
  563. }
  564. bool
  565. AddonManager::is_old_enabled_addon(const std::unique_ptr<Addon>& addon) const
  566. {
  567. return addon->get_format() == Addon::ORIGINAL &&
  568. addon->get_type() != Addon::LANGUAGEPACK &&
  569. addon->is_enabled();
  570. }
  571. bool
  572. AddonManager::is_old_addon_enabled() const {
  573. auto it = std::find_if(m_installed_addons.begin(), m_installed_addons.end(),
  574. [this](const auto& addon)
  575. {
  576. return is_old_enabled_addon(addon.second);
  577. });
  578. return it != m_installed_addons.end();
  579. }
  580. void
  581. AddonManager::disable_old_addons()
  582. {
  583. for (auto& [id, addon] : m_installed_addons) {
  584. if (is_old_enabled_addon(addon)) {
  585. disable_addon(id);
  586. }
  587. }
  588. }
  589. void
  590. AddonManager::mount_old_addons()
  591. {
  592. std::string mountpoint;
  593. for (auto& [id, addon] : m_installed_addons) {
  594. if (is_old_enabled_addon(addon)) {
  595. if (PHYSFS_mount(addon->get_install_filename().c_str(), mountpoint.c_str(), !addon->overrides_data()) == 0)
  596. {
  597. log_warning << "Could not add " << addon->get_install_filename() << " to search path: "
  598. << physfsutil::get_last_error() << std::endl;
  599. }
  600. }
  601. }
  602. }
  603. void
  604. AddonManager::unmount_old_addons()
  605. {
  606. for (auto& [id, addon] : m_installed_addons) {
  607. if (is_old_enabled_addon(addon)) {
  608. if (PHYSFS_unmount(addon->get_install_filename().c_str()) == 0)
  609. {
  610. log_warning << "Could not remove " << addon->get_install_filename() << " from search path: "
  611. << physfsutil::get_last_error() << std::endl;
  612. }
  613. }
  614. }
  615. }
  616. bool
  617. AddonManager::is_from_old_addon(const std::string& filename) const
  618. {
  619. std::string real_path = PHYSFS_getRealDir(filename.c_str());
  620. for (auto& [id, addon] : m_installed_addons) {
  621. if (is_old_enabled_addon(addon) &&
  622. addon->get_install_filename() == real_path) {
  623. return true;
  624. }
  625. }
  626. return false;
  627. }
  628. bool
  629. AddonManager::is_addon_installed(const std::string& id) const
  630. {
  631. const auto installed_addons = get_installed_addons();
  632. return std::any_of(installed_addons.begin(), installed_addons.end(),
  633. [id] (const auto& installed_addon) {
  634. return installed_addon == id;
  635. });
  636. }
  637. std::vector<AddonId>
  638. AddonManager::get_depending_addons(const std::string& id) const
  639. {
  640. std::vector<AddonId> addons;
  641. for (auto& [addon_id, addon] : m_installed_addons)
  642. {
  643. const auto& dependencies = addon->get_dependencies();
  644. if (std::find(dependencies.begin(), dependencies.end(), addon_id) != dependencies.end())
  645. addons.push_back(addon_id);
  646. }
  647. return addons;
  648. }
  649. std::vector<std::string>
  650. AddonManager::scan_for_archives() const
  651. {
  652. std::vector<std::string> archives;
  653. // Search for archives and add them to the search path.
  654. physfsutil::enumerate_files(m_addon_directory, [this, &archives](const std::string& filename) {
  655. const std::string fullpath = FileSystem::join(m_addon_directory, filename);
  656. if (physfsutil::is_directory(fullpath))
  657. {
  658. // ignore dot files (e.g. '.git/'), as well as the addon cache directory.
  659. if (filename[0] != '.' && fullpath != m_cache_directory) {
  660. archives.push_back(fullpath);
  661. }
  662. }
  663. else
  664. {
  665. if (StringUtil::has_suffix(StringUtil::tolower(filename), ".zip")) {
  666. if (PHYSFS_exists(fullpath.c_str())) {
  667. archives.push_back(fullpath);
  668. }
  669. }
  670. }
  671. });
  672. return archives;
  673. }
  674. std::string
  675. AddonManager::scan_for_info(const std::string& archive_os_path) const
  676. {
  677. std::string nfoFilename = "";
  678. physfsutil::enumerate_files("/", [archive_os_path, &nfoFilename](const std::string& file) {
  679. if (StringUtil::has_suffix(file, ".nfo"))
  680. {
  681. std::string nfo_filename = FileSystem::join("/", file);
  682. // Make sure it's in the current archive_os_path.
  683. const char* realdir = PHYSFS_getRealDir(nfo_filename.c_str());
  684. if (!realdir)
  685. {
  686. log_warning << "PHYSFS_getRealDir() failed for " << nfo_filename << ": " << physfsutil::get_last_error() << std::endl;
  687. }
  688. else
  689. {
  690. if (realdir == archive_os_path)
  691. {
  692. nfoFilename = nfo_filename;
  693. }
  694. }
  695. }
  696. });
  697. return nfoFilename;
  698. }
  699. void
  700. AddonManager::add_installed_archive(const std::string& archive, const std::string& md5, bool user_install)
  701. {
  702. const char* realdir = PHYSFS_getRealDir(archive.c_str());
  703. if (!realdir)
  704. {
  705. log_warning << "PHYSFS_getRealDir() failed for " << archive << ": "
  706. << physfsutil::get_last_error() << std::endl;
  707. }
  708. else
  709. {
  710. bool has_error = false;
  711. std::string os_path = FileSystem::join(realdir, archive);
  712. PHYSFS_mount(os_path.c_str(), nullptr, 1);
  713. std::string nfo_filename = scan_for_info(os_path);
  714. if (nfo_filename.empty())
  715. {
  716. log_warning << "Couldn't find .nfo file for " << os_path << std::endl;
  717. has_error = true;
  718. }
  719. else
  720. {
  721. try
  722. {
  723. std::unique_ptr<Addon> addon = Addon::parse(nfo_filename);
  724. addon->set_install_filename(os_path, md5);
  725. const auto& addon_id = addon->get_id();
  726. try
  727. {
  728. get_installed_addon(addon_id);
  729. if(user_install)
  730. {
  731. Dialog::show_message(fmt::format(_("Add-on {} by {} is already installed."),
  732. addon->get_title(), addon->get_author()));
  733. }
  734. }
  735. catch(...)
  736. {
  737. // Save add-on title and author on stack before std::move.
  738. const std::string addon_title = addon->get_title();
  739. const std::string addon_author = addon->get_author();
  740. m_installed_addons[addon_id] = std::move(addon);
  741. if(user_install)
  742. {
  743. try
  744. {
  745. enable_addon(addon_id);
  746. }
  747. catch(const std::exception& err)
  748. {
  749. log_warning << "Failed to enable add-on archive '" << addon_id << "': " << err.what() << std::endl;
  750. }
  751. Dialog::show_message(fmt::format(_("Add-on {} by {} successfully installed."),
  752. addon_title, addon_author));
  753. // If currently opened menu is add-ons menu refresh it.
  754. AddonMenu* addon_menu = dynamic_cast<AddonMenu*>(MenuManager::instance().current_menu());
  755. if (addon_menu)
  756. addon_menu->refresh();
  757. }
  758. }
  759. }
  760. catch (const std::runtime_error& e)
  761. {
  762. log_warning << "Could not load add-on info for " << archive << ": " << e.what() << std::endl;
  763. has_error = true;
  764. }
  765. }
  766. if(!user_install || has_error)
  767. {
  768. PHYSFS_unmount(os_path.c_str());
  769. }
  770. }
  771. }
  772. void
  773. AddonManager::add_installed_addons()
  774. {
  775. auto archives = scan_for_archives();
  776. for (const auto& archive : archives)
  777. {
  778. MD5 md5 = md5_from_archive(archive);
  779. add_installed_archive(archive, md5.hex_digest());
  780. }
  781. }
  782. AddonManager::AddonMap
  783. AddonManager::parse_addon_infos(const std::string& filename) const
  784. {
  785. AddonMap m_addons;
  786. try
  787. {
  788. register_translation_directory(filename);
  789. auto doc = ReaderDocument::from_file(filename);
  790. auto root = doc.get_root();
  791. if (root.get_name() != "supertux-addons")
  792. {
  793. throw std::runtime_error("Downloaded file is not an Add-on list");
  794. }
  795. else
  796. {
  797. auto addon_collection = root.get_collection();
  798. for (auto const& addon_node : addon_collection.get_objects())
  799. {
  800. if (addon_node.get_name() != "supertux-addoninfo")
  801. {
  802. log_warning << "Unknown token '" << addon_node.get_name() << "' in Add-on list" << std::endl;
  803. }
  804. else
  805. {
  806. try
  807. {
  808. std::unique_ptr<Addon> addon = Addon::parse(addon_node.get_mapping());
  809. m_addons[addon->get_id()] = std::move(addon);
  810. }
  811. catch(const std::exception& e)
  812. {
  813. log_warning << "Problem when reading Add-on entry: " << e.what() << std::endl;
  814. }
  815. }
  816. }
  817. return m_addons;
  818. }
  819. }
  820. catch(const std::exception& e)
  821. {
  822. std::stringstream msg;
  823. msg << "Problem when reading Add-on list: " << e.what();
  824. throw std::runtime_error(msg.str());
  825. }
  826. }
  827. void
  828. AddonManager::update()
  829. {
  830. m_downloader.update();
  831. }
  832. void
  833. AddonManager::check_for_langpack_updates()
  834. {
  835. const std::string& language = g_dictionary_manager->get_language().get_language();
  836. if (language == "en")
  837. return;
  838. try
  839. {
  840. check_online();
  841. try
  842. {
  843. const std::string& addon_id = "language-pack";
  844. log_debug << "Looking for language add-on with ID " << addon_id << "..." << std::endl;
  845. Addon& langpack = get_repository_addon(addon_id);
  846. try
  847. {
  848. auto& installed_langpack = get_installed_addon(addon_id);
  849. if (installed_langpack.get_md5() == langpack.get_md5() ||
  850. installed_langpack.get_version() > langpack.get_version())
  851. {
  852. log_debug << "Language add-on " << addon_id << " is already the latest version." << std::endl;
  853. return;
  854. }
  855. // Langpack update available. Let's install it!
  856. install_addon(addon_id);
  857. try
  858. {
  859. enable_addon(addon_id);
  860. }
  861. catch(const std::exception& err)
  862. {
  863. log_warning << "Failed to enable language pack '" << addon_id << "' after update: " << err.what() << std::endl;
  864. }
  865. }
  866. catch(const std::exception&)
  867. {
  868. log_debug << "Language addon " << addon_id << " is not installed. Installing..." << std::endl;
  869. install_addon(addon_id);
  870. try
  871. {
  872. enable_addon(addon_id);
  873. }
  874. catch(const std::exception& err)
  875. {
  876. log_warning << "Failed to enable language pack '" << addon_id << "' after install: " << err.what() << std::endl;
  877. }
  878. }
  879. }
  880. catch(std::exception&)
  881. {
  882. log_debug << "Language add-on for current locale not found." << std::endl;
  883. }
  884. }
  885. catch(...)
  886. {
  887. // If anything fails here, just silently ignore.
  888. }
  889. }
  890. #ifdef EMSCRIPTEN
  891. void
  892. AddonManager::onDownloadProgress(int id, int loaded, int total)
  893. {
  894. m_downloader.onDownloadProgress(id, loaded, total);
  895. }
  896. void
  897. AddonManager::onDownloadFinished(int id)
  898. {
  899. m_downloader.onDownloadFinished(id);
  900. }
  901. void
  902. AddonManager::onDownloadError(int id)
  903. {
  904. m_downloader.onDownloadError(id);
  905. }
  906. void
  907. AddonManager::onDownloadAborted(int id)
  908. {
  909. m_downloader.onDownloadAborted(id);
  910. }
  911. #endif
  912. /* EOF */