patch_manager.cpp 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696
  1. // SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
  2. // SPDX-License-Identifier: GPL-2.0-or-later
  3. #include <algorithm>
  4. #include <array>
  5. #include <cstddef>
  6. #include <cstring>
  7. #include "common/hex_util.h"
  8. #include "common/logging/log.h"
  9. #include "common/settings.h"
  10. #ifndef _WIN32
  11. #include "common/string_util.h"
  12. #endif
  13. #include "core/core.h"
  14. #include "core/file_sys/common_funcs.h"
  15. #include "core/file_sys/content_archive.h"
  16. #include "core/file_sys/control_metadata.h"
  17. #include "core/file_sys/ips_layer.h"
  18. #include "core/file_sys/patch_manager.h"
  19. #include "core/file_sys/registered_cache.h"
  20. #include "core/file_sys/romfs.h"
  21. #include "core/file_sys/vfs_cached.h"
  22. #include "core/file_sys/vfs_layered.h"
  23. #include "core/file_sys/vfs_vector.h"
  24. #include "core/hle/service/filesystem/filesystem.h"
  25. #include "core/hle/service/ns/language.h"
  26. #include "core/hle/service/set/settings_server.h"
  27. #include "core/loader/loader.h"
  28. #include "core/loader/nso.h"
  29. #include "core/memory/cheat_engine.h"
  30. namespace FileSys {
  31. namespace {
  32. constexpr u32 SINGLE_BYTE_MODULUS = 0x100;
  33. constexpr std::array<const char*, 14> EXEFS_FILE_NAMES{
  34. "main", "main.npdm", "rtld", "sdk", "subsdk0", "subsdk1", "subsdk2",
  35. "subsdk3", "subsdk4", "subsdk5", "subsdk6", "subsdk7", "subsdk8", "subsdk9",
  36. };
  37. enum class TitleVersionFormat : u8 {
  38. ThreeElements, ///< vX.Y.Z
  39. FourElements, ///< vX.Y.Z.W
  40. };
  41. std::string FormatTitleVersion(u32 version,
  42. TitleVersionFormat format = TitleVersionFormat::ThreeElements) {
  43. std::array<u8, sizeof(u32)> bytes{};
  44. bytes[0] = static_cast<u8>(version % SINGLE_BYTE_MODULUS);
  45. for (std::size_t i = 1; i < bytes.size(); ++i) {
  46. version /= SINGLE_BYTE_MODULUS;
  47. bytes[i] = static_cast<u8>(version % SINGLE_BYTE_MODULUS);
  48. }
  49. if (format == TitleVersionFormat::FourElements) {
  50. return fmt::format("v{}.{}.{}.{}", bytes[3], bytes[2], bytes[1], bytes[0]);
  51. }
  52. return fmt::format("v{}.{}.{}", bytes[3], bytes[2], bytes[1]);
  53. }
  54. // Returns a directory with name matching name case-insensitive. Returns nullptr if directory
  55. // doesn't have a directory with name.
  56. VirtualDir FindSubdirectoryCaseless(const VirtualDir dir, std::string_view name) {
  57. #ifdef _WIN32
  58. return dir->GetSubdirectory(name);
  59. #else
  60. const auto subdirs = dir->GetSubdirectories();
  61. for (const auto& subdir : subdirs) {
  62. std::string dir_name = Common::ToLower(subdir->GetName());
  63. if (dir_name == name) {
  64. return subdir;
  65. }
  66. }
  67. return nullptr;
  68. #endif
  69. }
  70. std::optional<std::vector<Core::Memory::CheatEntry>> ReadCheatFileFromFolder(
  71. u64 title_id, const PatchManager::BuildID& build_id_, const VirtualDir& base_path, bool upper) {
  72. const auto build_id_raw = Common::HexToString(build_id_, upper);
  73. const auto build_id = build_id_raw.substr(0, sizeof(u64) * 2);
  74. const auto file = base_path->GetFile(fmt::format("{}.txt", build_id));
  75. if (file == nullptr) {
  76. LOG_INFO(Common_Filesystem, "No cheats file found for title_id={:016X}, build_id={}",
  77. title_id, build_id);
  78. return std::nullopt;
  79. }
  80. std::vector<u8> data(file->GetSize());
  81. if (file->Read(data.data(), data.size()) != data.size()) {
  82. LOG_INFO(Common_Filesystem, "Failed to read cheats file for title_id={:016X}, build_id={}",
  83. title_id, build_id);
  84. return std::nullopt;
  85. }
  86. const Core::Memory::TextCheatParser parser;
  87. return parser.Parse(std::string_view(reinterpret_cast<const char*>(data.data()), data.size()));
  88. }
  89. void AppendCommaIfNotEmpty(std::string& to, std::string_view with) {
  90. if (to.empty()) {
  91. to += with;
  92. } else {
  93. to += ", ";
  94. to += with;
  95. }
  96. }
  97. bool IsDirValidAndNonEmpty(const VirtualDir& dir) {
  98. return dir != nullptr && (!dir->GetFiles().empty() || !dir->GetSubdirectories().empty());
  99. }
  100. } // Anonymous namespace
  101. PatchManager::PatchManager(u64 title_id_,
  102. const Service::FileSystem::FileSystemController& fs_controller_,
  103. const ContentProvider& content_provider_)
  104. : title_id{title_id_}, fs_controller{fs_controller_}, content_provider{content_provider_} {}
  105. PatchManager::~PatchManager() = default;
  106. u64 PatchManager::GetTitleID() const {
  107. return title_id;
  108. }
  109. VirtualDir PatchManager::PatchExeFS(VirtualDir exefs) const {
  110. LOG_INFO(Loader, "Patching ExeFS for title_id={:016X}", title_id);
  111. if (exefs == nullptr)
  112. return exefs;
  113. const auto& disabled = Settings::values.disabled_addons[title_id];
  114. const auto update_disabled =
  115. std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
  116. // Game Updates
  117. const auto update_tid = GetUpdateTitleID(title_id);
  118. const auto update = content_provider.GetEntry(update_tid, ContentRecordType::Program);
  119. if (!update_disabled && update != nullptr && update->GetExeFS() != nullptr) {
  120. LOG_INFO(Loader, " ExeFS: Update ({}) applied successfully",
  121. FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)));
  122. exefs = update->GetExeFS();
  123. }
  124. // LayeredExeFS
  125. const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
  126. const auto sdmc_load_dir = fs_controller.GetSDMCModificationLoadRoot(title_id);
  127. std::vector<VirtualDir> patch_dirs = {sdmc_load_dir};
  128. if (load_dir != nullptr) {
  129. const auto load_patch_dirs = load_dir->GetSubdirectories();
  130. patch_dirs.insert(patch_dirs.end(), load_patch_dirs.begin(), load_patch_dirs.end());
  131. }
  132. std::sort(patch_dirs.begin(), patch_dirs.end(),
  133. [](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
  134. std::vector<VirtualDir> layers;
  135. layers.reserve(patch_dirs.size() + 1);
  136. for (const auto& subdir : patch_dirs) {
  137. if (std::find(disabled.begin(), disabled.end(), subdir->GetName()) != disabled.end())
  138. continue;
  139. auto exefs_dir = FindSubdirectoryCaseless(subdir, "exefs");
  140. if (exefs_dir != nullptr)
  141. layers.push_back(std::move(exefs_dir));
  142. }
  143. layers.push_back(exefs);
  144. auto layered = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers));
  145. if (layered != nullptr) {
  146. LOG_INFO(Loader, " ExeFS: LayeredExeFS patches applied successfully");
  147. exefs = std::move(layered);
  148. }
  149. if (Settings::values.dump_exefs) {
  150. LOG_INFO(Loader, "Dumping ExeFS for title_id={:016X}", title_id);
  151. const auto dump_dir = fs_controller.GetModificationDumpRoot(title_id);
  152. if (dump_dir != nullptr) {
  153. const auto exefs_dir = GetOrCreateDirectoryRelative(dump_dir, "/exefs");
  154. VfsRawCopyD(exefs, exefs_dir);
  155. }
  156. }
  157. return exefs;
  158. }
  159. std::vector<VirtualFile> PatchManager::CollectPatches(const std::vector<VirtualDir>& patch_dirs,
  160. const std::string& build_id) const {
  161. const auto& disabled = Settings::values.disabled_addons[title_id];
  162. const auto nso_build_id = fmt::format("{:0<64}", build_id);
  163. std::vector<VirtualFile> out;
  164. out.reserve(patch_dirs.size());
  165. for (const auto& subdir : patch_dirs) {
  166. if (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) != disabled.cend())
  167. continue;
  168. auto exefs_dir = FindSubdirectoryCaseless(subdir, "exefs");
  169. if (exefs_dir != nullptr) {
  170. for (const auto& file : exefs_dir->GetFiles()) {
  171. if (file->GetExtension() == "ips") {
  172. auto name = file->GetName();
  173. const auto this_build_id =
  174. fmt::format("{:0<64}", name.substr(0, name.find('.')));
  175. if (nso_build_id == this_build_id)
  176. out.push_back(file);
  177. } else if (file->GetExtension() == "pchtxt") {
  178. IPSwitchCompiler compiler{file};
  179. if (!compiler.IsValid())
  180. continue;
  181. const auto this_build_id = Common::HexToString(compiler.GetBuildID());
  182. if (nso_build_id == this_build_id)
  183. out.push_back(file);
  184. }
  185. }
  186. }
  187. }
  188. return out;
  189. }
  190. std::vector<u8> PatchManager::PatchNSO(const std::vector<u8>& nso, const std::string& name) const {
  191. if (nso.size() < sizeof(Loader::NSOHeader)) {
  192. return nso;
  193. }
  194. Loader::NSOHeader header;
  195. std::memcpy(&header, nso.data(), sizeof(header));
  196. if (header.magic != Common::MakeMagic('N', 'S', 'O', '0')) {
  197. return nso;
  198. }
  199. const auto build_id_raw = Common::HexToString(header.build_id);
  200. const auto build_id = build_id_raw.substr(0, build_id_raw.find_last_not_of('0') + 1);
  201. if (Settings::values.dump_nso) {
  202. LOG_INFO(Loader, "Dumping NSO for name={}, build_id={}, title_id={:016X}", name, build_id,
  203. title_id);
  204. const auto dump_dir = fs_controller.GetModificationDumpRoot(title_id);
  205. if (dump_dir != nullptr) {
  206. const auto nso_dir = GetOrCreateDirectoryRelative(dump_dir, "/nso");
  207. const auto file = nso_dir->CreateFile(fmt::format("{}-{}.nso", name, build_id));
  208. file->Resize(nso.size());
  209. file->WriteBytes(nso);
  210. }
  211. }
  212. LOG_INFO(Loader, "Patching NSO for name={}, build_id={}", name, build_id);
  213. const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
  214. if (load_dir == nullptr) {
  215. LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id);
  216. return nso;
  217. }
  218. auto patch_dirs = load_dir->GetSubdirectories();
  219. std::sort(patch_dirs.begin(), patch_dirs.end(),
  220. [](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
  221. const auto patches = CollectPatches(patch_dirs, build_id);
  222. auto out = nso;
  223. for (const auto& patch_file : patches) {
  224. if (patch_file->GetExtension() == "ips") {
  225. LOG_INFO(Loader, " - Applying IPS patch from mod \"{}\"",
  226. patch_file->GetContainingDirectory()->GetParentDirectory()->GetName());
  227. const auto patched = PatchIPS(std::make_shared<VectorVfsFile>(out), patch_file);
  228. if (patched != nullptr)
  229. out = patched->ReadAllBytes();
  230. } else if (patch_file->GetExtension() == "pchtxt") {
  231. LOG_INFO(Loader, " - Applying IPSwitch patch from mod \"{}\"",
  232. patch_file->GetContainingDirectory()->GetParentDirectory()->GetName());
  233. const IPSwitchCompiler compiler{patch_file};
  234. const auto patched = compiler.Apply(std::make_shared<VectorVfsFile>(out));
  235. if (patched != nullptr)
  236. out = patched->ReadAllBytes();
  237. }
  238. }
  239. if (out.size() < sizeof(Loader::NSOHeader)) {
  240. return nso;
  241. }
  242. std::memcpy(out.data(), &header, sizeof(header));
  243. return out;
  244. }
  245. bool PatchManager::HasNSOPatch(const BuildID& build_id_, std::string_view name) const {
  246. const auto build_id_raw = Common::HexToString(build_id_);
  247. const auto build_id = build_id_raw.substr(0, build_id_raw.find_last_not_of('0') + 1);
  248. LOG_INFO(Loader, "Querying NSO patch existence for build_id={}, name={}", build_id, name);
  249. const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
  250. if (load_dir == nullptr) {
  251. LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id);
  252. return false;
  253. }
  254. auto patch_dirs = load_dir->GetSubdirectories();
  255. std::sort(patch_dirs.begin(), patch_dirs.end(),
  256. [](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
  257. return !CollectPatches(patch_dirs, build_id).empty();
  258. }
  259. std::vector<Core::Memory::CheatEntry> PatchManager::CreateCheatList(
  260. const BuildID& build_id_) const {
  261. const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
  262. if (load_dir == nullptr) {
  263. LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id);
  264. return {};
  265. }
  266. const auto& disabled = Settings::values.disabled_addons[title_id];
  267. auto patch_dirs = load_dir->GetSubdirectories();
  268. std::sort(patch_dirs.begin(), patch_dirs.end(),
  269. [](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
  270. std::vector<Core::Memory::CheatEntry> out;
  271. for (const auto& subdir : patch_dirs) {
  272. if (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) != disabled.cend()) {
  273. continue;
  274. }
  275. auto cheats_dir = FindSubdirectoryCaseless(subdir, "cheats");
  276. if (cheats_dir != nullptr) {
  277. if (const auto res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, true)) {
  278. std::copy(res->begin(), res->end(), std::back_inserter(out));
  279. continue;
  280. }
  281. if (const auto res = ReadCheatFileFromFolder(title_id, build_id_, cheats_dir, false)) {
  282. std::copy(res->begin(), res->end(), std::back_inserter(out));
  283. }
  284. }
  285. }
  286. return out;
  287. }
  288. static void ApplyLayeredFS(VirtualFile& romfs, u64 title_id, ContentRecordType type,
  289. const Service::FileSystem::FileSystemController& fs_controller) {
  290. const auto load_dir = fs_controller.GetModificationLoadRoot(title_id);
  291. const auto sdmc_load_dir = fs_controller.GetSDMCModificationLoadRoot(title_id);
  292. if ((type != ContentRecordType::Program && type != ContentRecordType::Data &&
  293. type != ContentRecordType::HtmlDocument) ||
  294. (load_dir == nullptr && sdmc_load_dir == nullptr)) {
  295. return;
  296. }
  297. const auto& disabled = Settings::values.disabled_addons[title_id];
  298. std::vector<VirtualDir> patch_dirs = load_dir->GetSubdirectories();
  299. if (std::find(disabled.cbegin(), disabled.cend(), "SDMC") == disabled.cend()) {
  300. patch_dirs.push_back(sdmc_load_dir);
  301. }
  302. std::sort(patch_dirs.begin(), patch_dirs.end(),
  303. [](const VirtualDir& l, const VirtualDir& r) { return l->GetName() < r->GetName(); });
  304. std::vector<VirtualDir> layers;
  305. std::vector<VirtualDir> layers_ext;
  306. layers.reserve(patch_dirs.size() + 1);
  307. layers_ext.reserve(patch_dirs.size() + 1);
  308. for (const auto& subdir : patch_dirs) {
  309. if (std::find(disabled.cbegin(), disabled.cend(), subdir->GetName()) != disabled.cend()) {
  310. continue;
  311. }
  312. auto romfs_dir = FindSubdirectoryCaseless(subdir, "romfs");
  313. if (romfs_dir != nullptr)
  314. layers.emplace_back(std::make_shared<CachedVfsDirectory>(std::move(romfs_dir)));
  315. auto ext_dir = FindSubdirectoryCaseless(subdir, "romfs_ext");
  316. if (ext_dir != nullptr)
  317. layers_ext.emplace_back(std::make_shared<CachedVfsDirectory>(std::move(ext_dir)));
  318. if (type == ContentRecordType::HtmlDocument) {
  319. auto manual_dir = FindSubdirectoryCaseless(subdir, "manual_html");
  320. if (manual_dir != nullptr)
  321. layers.emplace_back(std::make_shared<CachedVfsDirectory>(std::move(manual_dir)));
  322. }
  323. }
  324. // When there are no layers to apply, return early as there is no need to rebuild the RomFS
  325. if (layers.empty() && layers_ext.empty()) {
  326. return;
  327. }
  328. auto extracted = ExtractRomFS(romfs);
  329. if (extracted == nullptr) {
  330. return;
  331. }
  332. layers.emplace_back(std::move(extracted));
  333. auto layered = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers));
  334. if (layered == nullptr) {
  335. return;
  336. }
  337. auto layered_ext = LayeredVfsDirectory::MakeLayeredDirectory(std::move(layers_ext));
  338. auto packed = CreateRomFS(std::move(layered), std::move(layered_ext));
  339. if (packed == nullptr) {
  340. return;
  341. }
  342. LOG_INFO(Loader, " RomFS: LayeredFS patches applied successfully");
  343. romfs = std::move(packed);
  344. }
  345. VirtualFile PatchManager::PatchRomFS(const NCA* base_nca, VirtualFile base_romfs,
  346. ContentRecordType type, VirtualFile packed_update_raw,
  347. bool apply_layeredfs) const {
  348. const auto log_string = fmt::format("Patching RomFS for title_id={:016X}, type={:02X}",
  349. title_id, static_cast<u8>(type));
  350. if (type == ContentRecordType::Program || type == ContentRecordType::Data) {
  351. LOG_INFO(Loader, "{}", log_string);
  352. } else {
  353. LOG_DEBUG(Loader, "{}", log_string);
  354. }
  355. auto romfs = base_romfs;
  356. // Game Updates
  357. const auto update_tid = GetUpdateTitleID(title_id);
  358. const auto update_raw = content_provider.GetEntryRaw(update_tid, type);
  359. const auto& disabled = Settings::values.disabled_addons[title_id];
  360. const auto update_disabled =
  361. std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
  362. if (!update_disabled && update_raw != nullptr && base_nca != nullptr) {
  363. const auto new_nca = std::make_shared<NCA>(update_raw, base_nca);
  364. if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
  365. new_nca->GetRomFS() != nullptr) {
  366. LOG_INFO(Loader, " RomFS: Update ({}) applied successfully",
  367. FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0)));
  368. romfs = new_nca->GetRomFS();
  369. const auto version =
  370. FormatTitleVersion(content_provider.GetEntryVersion(update_tid).value_or(0));
  371. }
  372. } else if (!update_disabled && packed_update_raw != nullptr && base_nca != nullptr) {
  373. const auto new_nca = std::make_shared<NCA>(packed_update_raw, base_nca);
  374. if (new_nca->GetStatus() == Loader::ResultStatus::Success &&
  375. new_nca->GetRomFS() != nullptr) {
  376. LOG_INFO(Loader, " RomFS: Update (PACKED) applied successfully");
  377. romfs = new_nca->GetRomFS();
  378. }
  379. }
  380. // LayeredFS
  381. if (apply_layeredfs) {
  382. ApplyLayeredFS(romfs, title_id, type, fs_controller);
  383. }
  384. return romfs;
  385. }
  386. std::vector<Patch> PatchManager::GetPatches(VirtualFile update_raw) const {
  387. if (title_id == 0) {
  388. return {};
  389. }
  390. std::vector<Patch> out;
  391. const auto& disabled = Settings::values.disabled_addons[title_id];
  392. // Game Updates
  393. const auto update_tid = GetUpdateTitleID(title_id);
  394. PatchManager update{update_tid, fs_controller, content_provider};
  395. const auto metadata = update.GetControlMetadata();
  396. const auto& nacp = metadata.first;
  397. const auto update_disabled =
  398. std::find(disabled.cbegin(), disabled.cend(), "Update") != disabled.cend();
  399. Patch update_patch = {.enabled = !update_disabled,
  400. .name = "Update",
  401. .version = "",
  402. .type = PatchType::Update,
  403. .program_id = title_id,
  404. .title_id = title_id};
  405. if (nacp != nullptr) {
  406. update_patch.version = nacp->GetVersionString();
  407. out.push_back(update_patch);
  408. } else {
  409. if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
  410. const auto meta_ver = content_provider.GetEntryVersion(update_tid);
  411. if (meta_ver.value_or(0) == 0) {
  412. out.push_back(update_patch);
  413. } else {
  414. update_patch.version = FormatTitleVersion(*meta_ver);
  415. out.push_back(update_patch);
  416. }
  417. } else if (update_raw != nullptr) {
  418. update_patch.version = "PACKED";
  419. out.push_back(update_patch);
  420. }
  421. }
  422. // General Mods (LayeredFS and IPS)
  423. const auto mod_dir = fs_controller.GetModificationLoadRoot(title_id);
  424. if (mod_dir != nullptr) {
  425. for (const auto& mod : mod_dir->GetSubdirectories()) {
  426. std::string types;
  427. const auto exefs_dir = FindSubdirectoryCaseless(mod, "exefs");
  428. if (IsDirValidAndNonEmpty(exefs_dir)) {
  429. bool ips = false;
  430. bool ipswitch = false;
  431. bool layeredfs = false;
  432. for (const auto& file : exefs_dir->GetFiles()) {
  433. if (file->GetExtension() == "ips") {
  434. ips = true;
  435. } else if (file->GetExtension() == "pchtxt") {
  436. ipswitch = true;
  437. } else if (std::find(EXEFS_FILE_NAMES.begin(), EXEFS_FILE_NAMES.end(),
  438. file->GetName()) != EXEFS_FILE_NAMES.end()) {
  439. layeredfs = true;
  440. }
  441. }
  442. if (ips)
  443. AppendCommaIfNotEmpty(types, "IPS");
  444. if (ipswitch)
  445. AppendCommaIfNotEmpty(types, "IPSwitch");
  446. if (layeredfs)
  447. AppendCommaIfNotEmpty(types, "LayeredExeFS");
  448. }
  449. if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "romfs")))
  450. AppendCommaIfNotEmpty(types, "LayeredFS");
  451. if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(mod, "cheats")))
  452. AppendCommaIfNotEmpty(types, "Cheats");
  453. if (types.empty())
  454. continue;
  455. const auto mod_disabled =
  456. std::find(disabled.begin(), disabled.end(), mod->GetName()) != disabled.end();
  457. out.push_back({.enabled = !mod_disabled,
  458. .name = mod->GetName(),
  459. .version = types,
  460. .type = PatchType::Mod,
  461. .program_id = title_id,
  462. .title_id = title_id});
  463. }
  464. }
  465. // SDMC mod directory (RomFS LayeredFS)
  466. const auto sdmc_mod_dir = fs_controller.GetSDMCModificationLoadRoot(title_id);
  467. if (sdmc_mod_dir != nullptr) {
  468. std::string types;
  469. if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "exefs"))) {
  470. AppendCommaIfNotEmpty(types, "LayeredExeFS");
  471. }
  472. if (IsDirValidAndNonEmpty(FindSubdirectoryCaseless(sdmc_mod_dir, "romfs"))) {
  473. AppendCommaIfNotEmpty(types, "LayeredFS");
  474. }
  475. if (!types.empty()) {
  476. const auto mod_disabled =
  477. std::find(disabled.begin(), disabled.end(), "SDMC") != disabled.end();
  478. out.push_back({.enabled = !mod_disabled,
  479. .name = "SDMC",
  480. .version = types,
  481. .type = PatchType::Mod,
  482. .program_id = title_id,
  483. .title_id = title_id});
  484. }
  485. }
  486. // DLC
  487. const auto dlc_entries =
  488. content_provider.ListEntriesFilter(TitleType::AOC, ContentRecordType::Data);
  489. std::vector<ContentProviderEntry> dlc_match;
  490. dlc_match.reserve(dlc_entries.size());
  491. std::copy_if(dlc_entries.begin(), dlc_entries.end(), std::back_inserter(dlc_match),
  492. [this](const ContentProviderEntry& entry) {
  493. return GetBaseTitleID(entry.title_id) == title_id &&
  494. content_provider.GetEntry(entry)->GetStatus() ==
  495. Loader::ResultStatus::Success;
  496. });
  497. if (!dlc_match.empty()) {
  498. // Ensure sorted so DLC IDs show in order.
  499. std::sort(dlc_match.begin(), dlc_match.end());
  500. std::string list;
  501. for (size_t i = 0; i < dlc_match.size() - 1; ++i)
  502. list += fmt::format("{}, ", dlc_match[i].title_id & 0x7FF);
  503. list += fmt::format("{}", dlc_match.back().title_id & 0x7FF);
  504. const auto dlc_disabled =
  505. std::find(disabled.begin(), disabled.end(), "DLC") != disabled.end();
  506. out.push_back({.enabled = !dlc_disabled,
  507. .name = "DLC",
  508. .version = std::move(list),
  509. .type = PatchType::DLC,
  510. .program_id = title_id,
  511. .title_id = dlc_match.back().title_id});
  512. }
  513. return out;
  514. }
  515. std::optional<u32> PatchManager::GetGameVersion() const {
  516. const auto update_tid = GetUpdateTitleID(title_id);
  517. if (content_provider.HasEntry(update_tid, ContentRecordType::Program)) {
  518. return content_provider.GetEntryVersion(update_tid);
  519. }
  520. return content_provider.GetEntryVersion(title_id);
  521. }
  522. PatchManager::Metadata PatchManager::GetControlMetadata() const {
  523. const auto base_control_nca = content_provider.GetEntry(title_id, ContentRecordType::Control);
  524. if (base_control_nca == nullptr) {
  525. return {};
  526. }
  527. return ParseControlNCA(*base_control_nca);
  528. }
  529. PatchManager::Metadata PatchManager::ParseControlNCA(const NCA& nca) const {
  530. const auto base_romfs = nca.GetRomFS();
  531. if (base_romfs == nullptr) {
  532. return {};
  533. }
  534. const auto romfs = PatchRomFS(&nca, base_romfs, ContentRecordType::Control);
  535. if (romfs == nullptr) {
  536. return {};
  537. }
  538. const auto extracted = ExtractRomFS(romfs);
  539. if (extracted == nullptr) {
  540. return {};
  541. }
  542. auto nacp_file = extracted->GetFile("control.nacp");
  543. if (nacp_file == nullptr) {
  544. nacp_file = extracted->GetFile("Control.nacp");
  545. }
  546. auto nacp = nacp_file == nullptr ? nullptr : std::make_unique<NACP>(nacp_file);
  547. // Get language code from settings
  548. const auto language_code = Service::Set::GetLanguageCodeFromIndex(
  549. static_cast<u32>(Settings::values.language_index.GetValue()));
  550. // Convert to application language and get priority list
  551. const auto application_language =
  552. Service::NS::ConvertToApplicationLanguage(language_code)
  553. .value_or(Service::NS::ApplicationLanguage::AmericanEnglish);
  554. const auto language_priority_list =
  555. Service::NS::GetApplicationLanguagePriorityList(application_language);
  556. // Convert to language names
  557. auto priority_language_names = FileSys::LANGUAGE_NAMES; // Copy
  558. if (language_priority_list) {
  559. for (size_t i = 0; i < priority_language_names.size(); ++i) {
  560. // Relies on FileSys::LANGUAGE_NAMES being in the same order as
  561. // Service::NS::ApplicationLanguage
  562. const auto language_index = static_cast<u8>(language_priority_list->at(i));
  563. if (language_index < FileSys::LANGUAGE_NAMES.size()) {
  564. priority_language_names[i] = FileSys::LANGUAGE_NAMES[language_index];
  565. } else {
  566. // Not a catastrophe, unlikely to happen
  567. LOG_WARNING(Loader, "Invalid language index {}", language_index);
  568. }
  569. }
  570. }
  571. // Get first matching icon
  572. VirtualFile icon_file;
  573. for (const auto& language : priority_language_names) {
  574. icon_file = extracted->GetFile(std::string("icon_").append(language).append(".dat"));
  575. if (icon_file != nullptr) {
  576. break;
  577. }
  578. }
  579. return {std::move(nacp), icon_file};
  580. }
  581. } // namespace FileSys