game_list_worker.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. // Copyright 2018 yuzu emulator team
  2. // Licensed under GPLv2 or any later version
  3. // Refer to the license.txt file included.
  4. #include <memory>
  5. #include <string>
  6. #include <utility>
  7. #include <vector>
  8. #include <QDir>
  9. #include <QFile>
  10. #include <QFileInfo>
  11. #include <QSettings>
  12. #include "common/common_paths.h"
  13. #include "common/file_util.h"
  14. #include "core/core.h"
  15. #include "core/file_sys/card_image.h"
  16. #include "core/file_sys/content_archive.h"
  17. #include "core/file_sys/control_metadata.h"
  18. #include "core/file_sys/mode.h"
  19. #include "core/file_sys/nca_metadata.h"
  20. #include "core/file_sys/patch_manager.h"
  21. #include "core/file_sys/registered_cache.h"
  22. #include "core/file_sys/submission_package.h"
  23. #include "core/hle/service/filesystem/filesystem.h"
  24. #include "core/loader/loader.h"
  25. #include "yuzu/compatibility_list.h"
  26. #include "yuzu/game_list.h"
  27. #include "yuzu/game_list_p.h"
  28. #include "yuzu/game_list_worker.h"
  29. #include "yuzu/uisettings.h"
  30. namespace {
  31. QString GetGameListCachedObject(const std::string& filename, const std::string& ext,
  32. const std::function<QString()>& generator) {
  33. if (!UISettings::values.cache_game_list || filename == "0000000000000000") {
  34. return generator();
  35. }
  36. const auto path = FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) + DIR_SEP + "game_list" +
  37. DIR_SEP + filename + '.' + ext;
  38. FileUtil::CreateFullPath(path);
  39. if (!FileUtil::Exists(path)) {
  40. const auto str = generator();
  41. QFile file{QString::fromStdString(path)};
  42. if (file.open(QFile::WriteOnly)) {
  43. file.write(str.toUtf8());
  44. }
  45. return str;
  46. }
  47. QFile file{QString::fromStdString(path)};
  48. if (file.open(QFile::ReadOnly)) {
  49. return QString::fromUtf8(file.readAll());
  50. }
  51. return generator();
  52. }
  53. std::pair<std::vector<u8>, std::string> GetGameListCachedObject(
  54. const std::string& filename, const std::string& ext,
  55. const std::function<std::pair<std::vector<u8>, std::string>()>& generator) {
  56. if (!UISettings::values.cache_game_list || filename == "0000000000000000") {
  57. return generator();
  58. }
  59. const auto path1 = FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) + DIR_SEP + "game_list" +
  60. DIR_SEP + filename + ".jpeg";
  61. const auto path2 = FileUtil::GetUserPath(FileUtil::UserPath::CacheDir) + DIR_SEP + "game_list" +
  62. DIR_SEP + filename + ".appname.txt";
  63. FileUtil::CreateFullPath(path1);
  64. if (!FileUtil::Exists(path1) || !FileUtil::Exists(path2)) {
  65. const auto [icon, nacp] = generator();
  66. QFile file1{QString::fromStdString(path1)};
  67. if (!file1.open(QFile::WriteOnly)) {
  68. LOG_ERROR(Frontend, "Failed to open cache file.");
  69. return generator();
  70. }
  71. if (!file1.resize(icon.size())) {
  72. LOG_ERROR(Frontend, "Failed to resize cache file to necessary size.");
  73. return generator();
  74. }
  75. if (file1.write(reinterpret_cast<const char*>(icon.data()), icon.size()) != icon.size()) {
  76. LOG_ERROR(Frontend, "Failed to write data to cache file.");
  77. return generator();
  78. }
  79. QFile file2{QString::fromStdString(path2)};
  80. if (file2.open(QFile::WriteOnly)) {
  81. file2.write(nacp.data(), nacp.size());
  82. }
  83. return std::make_pair(icon, nacp);
  84. }
  85. QFile file1(QString::fromStdString(path1));
  86. QFile file2(QString::fromStdString(path2));
  87. if (!file1.open(QFile::ReadOnly)) {
  88. LOG_ERROR(Frontend, "Failed to open cache file for reading.");
  89. return generator();
  90. }
  91. if (!file2.open(QFile::ReadOnly)) {
  92. LOG_ERROR(Frontend, "Failed to open cache file for reading.");
  93. return generator();
  94. }
  95. std::vector<u8> vec(file1.size());
  96. if (file1.read(reinterpret_cast<char*>(vec.data()), vec.size()) !=
  97. static_cast<s64>(vec.size())) {
  98. return generator();
  99. }
  100. const auto data = file2.readAll();
  101. return std::make_pair(vec, data.toStdString());
  102. }
  103. void GetMetadataFromControlNCA(const FileSys::PatchManager& patch_manager, const FileSys::NCA& nca,
  104. std::vector<u8>& icon, std::string& name) {
  105. std::tie(icon, name) = GetGameListCachedObject(
  106. fmt::format("{:016X}", patch_manager.GetTitleID()), {}, [&patch_manager, &nca] {
  107. const auto [nacp, icon_f] = patch_manager.ParseControlNCA(nca);
  108. return std::make_pair(icon_f->ReadAllBytes(), nacp->GetApplicationName());
  109. });
  110. }
  111. bool HasSupportedFileExtension(const std::string& file_name) {
  112. const QFileInfo file = QFileInfo(QString::fromStdString(file_name));
  113. return GameList::supported_file_extensions.contains(file.suffix(), Qt::CaseInsensitive);
  114. }
  115. bool IsExtractedNCAMain(const std::string& file_name) {
  116. return QFileInfo(QString::fromStdString(file_name)).fileName() == QStringLiteral("main");
  117. }
  118. QString FormatGameName(const std::string& physical_name) {
  119. const QString physical_name_as_qstring = QString::fromStdString(physical_name);
  120. const QFileInfo file_info(physical_name_as_qstring);
  121. if (IsExtractedNCAMain(physical_name)) {
  122. return file_info.dir().path();
  123. }
  124. return physical_name_as_qstring;
  125. }
  126. QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager,
  127. Loader::AppLoader& loader, bool updatable = true) {
  128. QString out;
  129. FileSys::VirtualFile update_raw;
  130. loader.ReadUpdateRaw(update_raw);
  131. for (const auto& kv : patch_manager.GetPatchVersionNames(update_raw)) {
  132. const bool is_update = kv.first == "Update" || kv.first == "[D] Update";
  133. if (!updatable && is_update) {
  134. continue;
  135. }
  136. const QString type = QString::fromStdString(kv.first);
  137. if (kv.second.empty()) {
  138. out.append(QStringLiteral("%1\n").arg(type));
  139. } else {
  140. auto ver = kv.second;
  141. // Display container name for packed updates
  142. if (is_update && ver == "PACKED") {
  143. ver = Loader::GetFileTypeString(loader.GetFileType());
  144. }
  145. out.append(QStringLiteral("%1 (%2)\n").arg(type, QString::fromStdString(ver)));
  146. }
  147. }
  148. out.chop(1);
  149. return out;
  150. }
  151. QList<QStandardItem*> MakeGameListEntry(const std::string& path, const std::string& name,
  152. const std::vector<u8>& icon, Loader::AppLoader& loader,
  153. u64 program_id, const CompatibilityList& compatibility_list,
  154. const FileSys::PatchManager& patch) {
  155. const auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id);
  156. // The game list uses this as compatibility number for untested games
  157. QString compatibility{QStringLiteral("99")};
  158. if (it != compatibility_list.end()) {
  159. compatibility = it->second.first;
  160. }
  161. const auto file_type = loader.GetFileType();
  162. const auto file_type_string = QString::fromStdString(Loader::GetFileTypeString(file_type));
  163. QList<QStandardItem*> list{
  164. new GameListItemPath(FormatGameName(path), icon, QString::fromStdString(name),
  165. file_type_string, program_id),
  166. new GameListItemCompat(compatibility),
  167. new GameListItem(file_type_string),
  168. new GameListItemSize(FileUtil::GetSize(path)),
  169. };
  170. if (UISettings::values.show_add_ons) {
  171. const auto patch_versions = GetGameListCachedObject(
  172. fmt::format("{:016X}", patch.GetTitleID()), "pv.txt", [&patch, &loader] {
  173. return FormatPatchNameVersions(patch, loader, loader.IsRomFSUpdatable());
  174. });
  175. list.insert(2, new GameListItem(patch_versions));
  176. }
  177. return list;
  178. }
  179. } // Anonymous namespace
  180. GameListWorker::GameListWorker(FileSys::VirtualFilesystem vfs,
  181. FileSys::ManualContentProvider* provider,
  182. QVector<UISettings::GameDir>& game_dirs,
  183. const CompatibilityList& compatibility_list)
  184. : vfs(std::move(vfs)), provider(provider), game_dirs(game_dirs),
  185. compatibility_list(compatibility_list) {}
  186. GameListWorker::~GameListWorker() = default;
  187. void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
  188. using namespace FileSys;
  189. const auto& cache =
  190. dynamic_cast<ContentProviderUnion&>(Core::System::GetInstance().GetContentProvider());
  191. std::vector<std::pair<ContentProviderUnionSlot, ContentProviderEntry>> installed_games;
  192. installed_games = cache.ListEntriesFilterOrigin(std::nullopt, TitleType::Application,
  193. ContentRecordType::Program);
  194. if (parent_dir->type() == static_cast<int>(GameListItemType::SdmcDir)) {
  195. installed_games = cache.ListEntriesFilterOrigin(
  196. ContentProviderUnionSlot::SDMC, TitleType::Application, ContentRecordType::Program);
  197. } else if (parent_dir->type() == static_cast<int>(GameListItemType::UserNandDir)) {
  198. installed_games = cache.ListEntriesFilterOrigin(
  199. ContentProviderUnionSlot::UserNAND, TitleType::Application, ContentRecordType::Program);
  200. } else if (parent_dir->type() == static_cast<int>(GameListItemType::SysNandDir)) {
  201. installed_games = cache.ListEntriesFilterOrigin(
  202. ContentProviderUnionSlot::SysNAND, TitleType::Application, ContentRecordType::Program);
  203. }
  204. for (const auto& [slot, game] : installed_games) {
  205. if (slot == ContentProviderUnionSlot::FrontendManual)
  206. continue;
  207. const auto file = cache.GetEntryUnparsed(game.title_id, game.type);
  208. std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(file);
  209. if (!loader)
  210. continue;
  211. std::vector<u8> icon;
  212. std::string name;
  213. u64 program_id = 0;
  214. loader->ReadProgramId(program_id);
  215. const PatchManager patch{program_id};
  216. const auto control = cache.GetEntry(game.title_id, ContentRecordType::Control);
  217. if (control != nullptr)
  218. GetMetadataFromControlNCA(patch, *control, icon, name);
  219. emit EntryReady(MakeGameListEntry(file->GetFullPath(), name, icon, *loader, program_id,
  220. compatibility_list, patch),
  221. parent_dir);
  222. }
  223. }
  224. void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path,
  225. unsigned int recursion, GameListDir* parent_dir) {
  226. const auto callback = [this, target, recursion,
  227. parent_dir](u64* num_entries_out, const std::string& directory,
  228. const std::string& virtual_name) -> bool {
  229. if (stop_processing) {
  230. // Breaks the callback loop.
  231. return false;
  232. }
  233. const std::string physical_name = directory + DIR_SEP + virtual_name;
  234. const bool is_dir = FileUtil::IsDirectory(physical_name);
  235. if (!is_dir &&
  236. (HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name))) {
  237. const auto file = vfs->OpenFile(physical_name, FileSys::Mode::Read);
  238. auto loader = Loader::GetLoader(file);
  239. if (!loader) {
  240. return true;
  241. }
  242. const auto file_type = loader->GetFileType();
  243. if ((file_type == Loader::FileType::Unknown || file_type == Loader::FileType::Error) &&
  244. !UISettings::values.show_unknown) {
  245. return true;
  246. }
  247. u64 program_id = 0;
  248. const auto res2 = loader->ReadProgramId(program_id);
  249. if (target == ScanTarget::FillManualContentProvider) {
  250. if (res2 == Loader::ResultStatus::Success && file_type == Loader::FileType::NCA) {
  251. provider->AddEntry(FileSys::TitleType::Application,
  252. FileSys::GetCRTypeFromNCAType(FileSys::NCA{file}.GetType()),
  253. program_id, file);
  254. } else if (res2 == Loader::ResultStatus::Success &&
  255. (file_type == Loader::FileType::XCI ||
  256. file_type == Loader::FileType::NSP)) {
  257. const auto nsp = file_type == Loader::FileType::NSP
  258. ? std::make_shared<FileSys::NSP>(file)
  259. : FileSys::XCI{file}.GetSecurePartitionNSP();
  260. for (const auto& title : nsp->GetNCAs()) {
  261. for (const auto& entry : title.second) {
  262. provider->AddEntry(entry.first.first, entry.first.second, title.first,
  263. entry.second->GetBaseFile());
  264. }
  265. }
  266. }
  267. } else {
  268. std::vector<u8> icon;
  269. [[maybe_unused]] const auto res1 = loader->ReadIcon(icon);
  270. std::string name = " ";
  271. [[maybe_unused]] const auto res3 = loader->ReadTitle(name);
  272. const FileSys::PatchManager patch{program_id};
  273. emit EntryReady(MakeGameListEntry(physical_name, name, icon, *loader, program_id,
  274. compatibility_list, patch),
  275. parent_dir);
  276. }
  277. } else if (is_dir && recursion > 0) {
  278. watch_list.append(QString::fromStdString(physical_name));
  279. ScanFileSystem(target, physical_name, recursion - 1, parent_dir);
  280. }
  281. return true;
  282. };
  283. FileUtil::ForeachDirectoryEntry(nullptr, dir_path, callback);
  284. }
  285. void GameListWorker::run() {
  286. stop_processing = false;
  287. for (UISettings::GameDir& game_dir : game_dirs) {
  288. if (game_dir.path == QStringLiteral("SDMC")) {
  289. auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SdmcDir);
  290. emit DirEntryReady(game_list_dir);
  291. AddTitlesToGameList(game_list_dir);
  292. } else if (game_dir.path == QStringLiteral("UserNAND")) {
  293. auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::UserNandDir);
  294. emit DirEntryReady(game_list_dir);
  295. AddTitlesToGameList(game_list_dir);
  296. } else if (game_dir.path == QStringLiteral("SysNAND")) {
  297. auto* const game_list_dir = new GameListDir(game_dir, GameListItemType::SysNandDir);
  298. emit DirEntryReady(game_list_dir);
  299. AddTitlesToGameList(game_list_dir);
  300. } else {
  301. watch_list.append(game_dir.path);
  302. auto* const game_list_dir = new GameListDir(game_dir);
  303. emit DirEntryReady(game_list_dir);
  304. provider->ClearAllEntries();
  305. ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path.toStdString(), 2,
  306. game_list_dir);
  307. ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path.toStdString(),
  308. game_dir.deep_scan ? 256 : 0, game_list_dir);
  309. }
  310. };
  311. emit Finished(watch_list);
  312. }
  313. void GameListWorker::Cancel() {
  314. this->disconnect();
  315. stop_processing = true;
  316. }