game_list_worker.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
  2. // SPDX-License-Identifier: GPL-2.0-or-later
  3. #include <memory>
  4. #include <string>
  5. #include <utility>
  6. #include <vector>
  7. #include <QDir>
  8. #include <QFile>
  9. #include <QFileInfo>
  10. #include <QSettings>
  11. #include "common/fs/fs.h"
  12. #include "common/fs/path_util.h"
  13. #include "core/core.h"
  14. #include "core/file_sys/card_image.h"
  15. #include "core/file_sys/content_archive.h"
  16. #include "core/file_sys/control_metadata.h"
  17. #include "core/file_sys/mode.h"
  18. #include "core/file_sys/nca_metadata.h"
  19. #include "core/file_sys/patch_manager.h"
  20. #include "core/file_sys/registered_cache.h"
  21. #include "core/file_sys/submission_package.h"
  22. #include "core/loader/loader.h"
  23. #include "yuzu/compatibility_list.h"
  24. #include "yuzu/game_list.h"
  25. #include "yuzu/game_list_p.h"
  26. #include "yuzu/game_list_worker.h"
  27. #include "yuzu/uisettings.h"
  28. namespace {
  29. QString GetGameListCachedObject(const std::string& filename, const std::string& ext,
  30. const std::function<QString()>& generator) {
  31. if (!UISettings::values.cache_game_list || filename == "0000000000000000") {
  32. return generator();
  33. }
  34. const auto path =
  35. Common::FS::PathToUTF8String(Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) /
  36. "game_list" / fmt::format("{}.{}", filename, ext));
  37. void(Common::FS::CreateParentDirs(path));
  38. if (!Common::FS::Exists(path)) {
  39. const auto str = generator();
  40. QFile file{QString::fromStdString(path)};
  41. if (file.open(QFile::WriteOnly)) {
  42. file.write(str.toUtf8());
  43. }
  44. return str;
  45. }
  46. QFile file{QString::fromStdString(path)};
  47. if (file.open(QFile::ReadOnly)) {
  48. return QString::fromUtf8(file.readAll());
  49. }
  50. return generator();
  51. }
  52. std::pair<std::vector<u8>, std::string> GetGameListCachedObject(
  53. const std::string& filename, const std::string& ext,
  54. const std::function<std::pair<std::vector<u8>, std::string>()>& generator) {
  55. if (!UISettings::values.cache_game_list || filename == "0000000000000000") {
  56. return generator();
  57. }
  58. const auto game_list_dir =
  59. Common::FS::GetYuzuPath(Common::FS::YuzuPath::CacheDir) / "game_list";
  60. const auto jpeg_name = fmt::format("{}.jpeg", filename);
  61. const auto app_name = fmt::format("{}.appname.txt", filename);
  62. const auto path1 = Common::FS::PathToUTF8String(game_list_dir / jpeg_name);
  63. const auto path2 = Common::FS::PathToUTF8String(game_list_dir / app_name);
  64. void(Common::FS::CreateParentDirs(path1));
  65. if (!Common::FS::Exists(path1) || !Common::FS::Exists(path2)) {
  66. const auto [icon, nacp] = generator();
  67. QFile file1{QString::fromStdString(path1)};
  68. if (!file1.open(QFile::WriteOnly)) {
  69. LOG_ERROR(Frontend, "Failed to open cache file.");
  70. return generator();
  71. }
  72. if (!file1.resize(icon.size())) {
  73. LOG_ERROR(Frontend, "Failed to resize cache file to necessary size.");
  74. return generator();
  75. }
  76. if (file1.write(reinterpret_cast<const char*>(icon.data()), icon.size()) !=
  77. s64(icon.size())) {
  78. LOG_ERROR(Frontend, "Failed to write data to cache file.");
  79. return generator();
  80. }
  81. QFile file2{QString::fromStdString(path2)};
  82. if (file2.open(QFile::WriteOnly)) {
  83. file2.write(nacp.data(), nacp.size());
  84. }
  85. return std::make_pair(icon, nacp);
  86. }
  87. QFile file1(QString::fromStdString(path1));
  88. QFile file2(QString::fromStdString(path2));
  89. if (!file1.open(QFile::ReadOnly)) {
  90. LOG_ERROR(Frontend, "Failed to open cache file for reading.");
  91. return generator();
  92. }
  93. if (!file2.open(QFile::ReadOnly)) {
  94. LOG_ERROR(Frontend, "Failed to open cache file for reading.");
  95. return generator();
  96. }
  97. std::vector<u8> vec(file1.size());
  98. if (file1.read(reinterpret_cast<char*>(vec.data()), vec.size()) !=
  99. static_cast<s64>(vec.size())) {
  100. return generator();
  101. }
  102. const auto data = file2.readAll();
  103. return std::make_pair(vec, data.toStdString());
  104. }
  105. void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager, const FileSys::NCA& nca,
  106. std::vector<u8>& icon, std::string& name) {
  107. std::tie(icon, name) = GetGameListCachedObject(
  108. fmt::format("{:016X}", patch_manager.GetTitleID()), {}, [&patch_manager, &nca] {
  109. const auto [nacp, icon_f] = patch_manager.ParseControlNCA(nca);
  110. return std::make_pair(icon_f->ReadAllBytes(), nacp->GetApplicationName());
  111. });
  112. }
  113. bool HasSupportedFileExtension(const std::string& file_name) {
  114. const QFileInfo file = QFileInfo(QString::fromStdString(file_name));
  115. return GameList::supported_file_extensions.contains(file.suffix(), Qt::CaseInsensitive);
  116. }
  117. bool IsExtractedNCAMain(const std::string& file_name) {
  118. return QFileInfo(QString::fromStdString(file_name)).fileName() == QStringLiteral("main");
  119. }
  120. QString FormatGameName(const std::string& physical_name) {
  121. const QString physical_name_as_qstring = QString::fromStdString(physical_name);
  122. const QFileInfo file_info(physical_name_as_qstring);
  123. if (IsExtractedNCAMain(physical_name)) {
  124. return file_info.dir().path();
  125. }
  126. return physical_name_as_qstring;
  127. }
  128. QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager,
  129. Loader::AppLoader& loader, bool updatable = true) {
  130. QString out;
  131. FileSys::VirtualFile update_raw;
  132. loader.ReadUpdateRaw(update_raw);
  133. for (const auto& kv : patch_manager.GetPatchVersionNames(update_raw)) {
  134. const bool is_update = kv.first == "Update" || kv.first == "[D] Update";
  135. if (!updatable && is_update) {
  136. continue;
  137. }
  138. const QString type = QString::fromStdString(kv.first);
  139. if (kv.second.empty()) {
  140. out.append(QStringLiteral("%1\n").arg(type));
  141. } else {
  142. auto ver = kv.second;
  143. // Display container name for packed updates
  144. if (is_update && ver == "PACKED") {
  145. ver = Loader::GetFileTypeString(loader.GetFileType());
  146. }
  147. out.append(QStringLiteral("%1 (%2)\n").arg(type, QString::fromStdString(ver)));
  148. }
  149. }
  150. out.chop(1);
  151. return out;
  152. }
  153. QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::string& name,
  154. const std::size_t size, const std::vector<u8>& icon,
  155. Loader::AppLoader& loader, u64 program_id,
  156. const CompatibilityList& compatibility_list,
  157. const PlayTime::PlayTimeManager& play_time_manager,
  158. const FileSys::PatchManager& patch) {
  159. const auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
  160. // The game list uses this as compatibility number for untested games
  161. QString compatibility{QStringLiteral("99")};
  162. if (it != compatibility_list.end()) {
  163. compatibility = it->second.first;
  164. }
  165. const auto file_type = loader.GetFileType();
  166. const auto file_type_string = QString::fromStdString(Loader::GetFileTypeString(file_type));
  167. QList<QStandardItem*> list{
  168. new GameListItemPath(FormatGameName(path), icon, QString::fromStdString(name),
  169. file_type_string, program_id),
  170. new GameListItemCompat(compatibility),
  171. new GameListItem(file_type_string),
  172. new GameListItemSize(size),
  173. new GameListItemPlayTime(play_time_manager.GetPlayTime(program_id)),
  174. };
  175. const auto patch_versions = GetGameListCachedObject(
  176. fmt::format("{:016X}", patch.GetTitleID()), "pv.txt", [&patch, &loader] {
  177. return FormatPatchNameVersions(patch, loader, loader.IsRomFSUpdatable());
  178. });
  179. list.insert(2, new GameListItem(patch_versions));
  180. return list;
  181. }
  182. } // Anonymous namespace
  183. GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs_,
  184. FileSys::ManualContentProvider* provider_,
  185. QVector<UISettings::GameDir>& game_dirs_,
  186. const CompatibilityList& compatibility_list_,
  187. const PlayTime::PlayTimeManager& play_time_manager_,
  188. Core::System& system_)
  189. : vfs{std::move(vfs_)}, provider{provider_}, game_dirs{game_dirs_},
  190. compatibility_list{compatibility_list_}, play_time_manager{play_time_manager_}, system{
  191. system_} {
  192. // We want the game list to manage our lifetime.
  193. setAutoDelete(false);
  194. }
  195. GameListWorker::~GameListWorker() {
  196. this->disconnect();
  197. stop_requested.store(true);
  198. processing_completed.Wait();
  199. }
  200. void GameListWorker::ProcessEvents(GameList* game_list) {
  201. while (true) {
  202. std::function<void(GameList*)> func;
  203. {
  204. // Lock queue to protect concurrent modification.
  205. std::scoped_lock lk(lock);
  206. // If we can't pop a function, return.
  207. if (queued_events.empty()) {
  208. return;
  209. }
  210. // Pop a function.
  211. func = std::move(queued_events.back());
  212. queued_events.pop_back();
  213. }
  214. // Run the function.
  215. func(game_list);
  216. }
  217. }
  218. template <typename F>
  219. void GameListWorker::RecordEvent(F&& func) {
  220. {
  221. // Lock queue to protect concurrent modification.
  222. std::scoped_lock lk(lock);
  223. // Add the function into the front of the queue.
  224. queued_events.emplace_front(std::move(func));
  225. }
  226. // Data now available.
  227. emit DataAvailable();
  228. }
  229. void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
  230. using namespace FileSys;
  231. const auto& cache = system.GetContentProviderUnion();
  232. auto installed_games = cache.ListEntriesFilterOrigin(std::nullopt, TitleType::Application,
  233. ContentRecordType::Program);
  234. if (parent_dir->type() == static_cast<int>(GameListItemType::SdmcDir)) {
  235. installed_games = cache.ListEntriesFilterOrigin(
  236. ContentProviderUnionSlot::SDMC, TitleType::Application, ContentRecordType::Program);
  237. } else if (parent_dir->type() == static_cast<int>(GameListItemType::UserNandDir)) {
  238. installed_games = cache.ListEntriesFilterOrigin(
  239. ContentProviderUnionSlot::UserNAND, TitleType::Application, ContentRecordType::Program);
  240. } else if (parent_dir->type() == static_cast<int>(GameListItemType::SysNandDir)) {
  241. installed_games = cache.ListEntriesFilterOrigin(
  242. ContentProviderUnionSlot::SysNAND, TitleType::Application, ContentRecordType::Program);
  243. }
  244. for (const auto& [slot, game] : installed_games) {
  245. if (slot == ContentProviderUnionSlot::FrontendManual) {
  246. continue;
  247. }
  248. const auto file = cache.GetEntryUnparsed(game.title_id, game.type);
  249. std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(system, file);
  250. if (!loader) {
  251. continue;
  252. }
  253. std::vector<u8> icon;
  254. std::string name;
  255. u64 program_id = 0;
  256. const auto result = loader->ReadProgramId(program_id);
  257. if (result != Loader::ResultStatus::Success) {
  258. continue;
  259. }
  260. const PatchManager patch{program_id, system.GetFileSystemController(),
  261. system.GetContentProvider()};
  262. const auto control = cache.GetEntry(game.title_id, ContentRecordType::Control);
  263. if (control != nullptr) {
  264. GetMetadataFromControlNCA(patch, *control, icon, name);
  265. }
  266. auto entry = MakeGameListEntry(file->GetFullPath(), name, file->GetSize(), icon, *loader,
  267. program_id, compatibility_list, play_time_manager, patch);
  268. RecordEvent([=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
  269. }
  270. }
  271. void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
  272. GameListDir* parent_dir) {
  273. const auto callback = [this, target, parent_dir](const std::filesystem::path& path) -> bool {
  274. if (stop_requested) {
  275. // Breaks the callback loop.
  276. return false;
  277. }
  278. const auto physical_name = Common::FS::PathToUTF8String(path);
  279. const auto is_dir = Common::FS::IsDir(path);
  280. if (!is_dir &&
  281. (HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name))) {
  282. const auto file = vfs->OpenFile(physical_name, FileSys::Mode::Read);
  283. if (!file) {
  284. return true;
  285. }
  286. auto loader = Loader::GetLoader(system, file);
  287. if (!loader) {
  288. return true;
  289. }
  290. const auto file_type = loader->GetFileType();
  291. if (file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) {
  292. return true;
  293. }
  294. u64 program_id = 0;
  295. const auto res2 = loader->ReadProgramId(program_id);
  296. if (target == ScanTarget::FillManualContentProvider) {
  297. if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) {
  298. provider->AddEntry(FileSys::TitleType::Application,
  299. FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
  300. program_id, file);
  301. } else if (res2 == Loader::ResultStatus::Success &&
  302. (file_type == Loader::FileType::XCI ||
  303. file_type == Loader::FileType::NSP)) {
  304. const auto nsp = file_type == Loader::FileType::NSP
  305. ? std::make_shared<FileSys::NSP>(file)
  306. : FileSys::XCI{file}.GetSecurePartitionNSP();
  307. for (const auto& title : nsp->GetNCAs()) {
  308. for (const auto& entry : title.second) {
  309. provider->AddEntry(entry.first.first, entry.first.second, title.first,
  310. entry.second->GetBaseFile());
  311. }
  312. }
  313. }
  314. } else {
  315. std::vector<u64> program_ids;
  316. loader->ReadProgramIds(program_ids);
  317. if (res2 == Loader::ResultStatus::Success && program_ids.size() > 1 &&
  318. (file_type == Loader::FileType::XCI || file_type == Loader::FileType::NSP)) {
  319. for (const auto id : program_ids) {
  320. loader = Loader::GetLoader(system, file, id);
  321. if (!loader) {
  322. continue;
  323. }
  324. std::vector<u8> icon;
  325. [[maybe_unused]] const auto res1 = loader->ReadIcon(icon);
  326. std::string name = " ";
  327. [[maybe_unused]] const auto res3 = loader->ReadTitle(name);
  328. const FileSys::PatchManager patch{id, system.GetFileSystemController(),
  329. system.GetContentProvider()};
  330. auto entry = MakeGameListEntry(
  331. physical_name, name, Common::FS::GetSize(physical_name), icon, *loader,
  332. id, compatibility_list, play_time_manager, patch);
  333. RecordEvent(
  334. [=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
  335. }
  336. } else {
  337. std::vector<u8> icon;
  338. [[maybe_unused]] const auto res1 = loader->ReadIcon(icon);
  339. std::string name = " ";
  340. [[maybe_unused]] const auto res3 = loader->ReadTitle(name);
  341. const FileSys::PatchManager patch{program_id, system.GetFileSystemController(),
  342. system.GetContentProvider()};
  343. auto entry = MakeGameListEntry(
  344. physical_name, name, Common::FS::GetSize(physical_name), icon, *loader,
  345. program_id, compatibility_list, play_time_manager, patch);
  346. RecordEvent(
  347. [=](GameList* game_list) { game_list->AddEntry(entry, parent_dir); });
  348. }
  349. }
  350. } else if (is_dir) {
  351. watch_list.append(QString::fromStdString(physical_name));
  352. }
  353. return true;
  354. };
  355. if (deep_scan) {
  356. Common::FS::IterateDirEntriesRecursively(dir_path, callback,
  357. Common::FS::DirEntryFilter::All);
  358. } else {
  359. Common::FS::IterateDirEntries(dir_path, callback, Common::FS::DirEntryFilter::File);
  360. }
  361. }
  362. void GameListWorker::run() {
  363. watch_list.clear();
  364. provider->ClearAllEntries();
  365. const auto DirEntryReady = [&](GameListDir* game_list_dir) {
  366. RecordEvent([=](GameList* game_list) { game_list->AddDirEntry(game_list_dir); });
  367. };
  368. for (UISettings::GameDir& game_dir : game_dirs) {
  369. if (stop_requested) {
  370. break;
  371. }
  372. if (game_dir.path == QStringLiteral("SDMC")) {
  373. auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir);
  374. DirEntryReady(game_list_dir);
  375. AddTitlesToGameList(game_list_dir);
  376. } else if (game_dir.path == QStringLiteral("UserNAND")) {
  377. auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir);
  378. DirEntryReady(game_list_dir);
  379. AddTitlesToGameList(game_list_dir);
  380. } else if (game_dir.path == QStringLiteral("SysNAND")) {
  381. auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir);
  382. DirEntryReady(game_list_dir);
  383. AddTitlesToGameList(game_list_dir);
  384. } else {
  385. watch_list.append(game_dir.path);
  386. auto* const game_list_dir = new GameListDir(game_dir);
  387. DirEntryReady(game_list_dir);
  388. ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path.toStdString(),
  389. game_dir.deep_scan, game_list_dir);
  390. ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path.toStdString(),
  391. game_dir.deep_scan, game_list_dir);
  392. }
  393. }
  394. RecordEvent([this](GameList* game_list) { game_list->DonePopulating(watch_list); });
  395. processing_completed.Set();
  396. }