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