diff options
| author | 2020-09-15 01:57:13 +0000 | |
|---|---|---|
| committer | 2020-09-15 01:57:13 +0000 | |
| commit | b5f4221c3dba29dad11897ac8d7860d773f359f1 (patch) | |
| tree | 94cb81437733d78d17dab14902a0e5db7fe62e2c /src | |
| parent | Merge pull request #4652 from lioncash/crypto (diff) | |
| parent | patch_manager: Resolve implicit truncations in FormatTitleVersion() (diff) | |
| download | yuzu-b5f4221c3dba29dad11897ac8d7860d773f359f1.tar.gz yuzu-b5f4221c3dba29dad11897ac8d7860d773f359f1.tar.xz yuzu-b5f4221c3dba29dad11897ac8d7860d773f359f1.zip | |
Merge pull request #4655 from lioncash/internal2
patch_manager: Minor cleanup
Diffstat (limited to 'src')
| -rw-r--r-- | src/core/file_sys/patch_manager.cpp | 134 | ||||
| -rw-r--r-- | src/core/file_sys/patch_manager.h | 48 |
2 files changed, 95 insertions, 87 deletions
diff --git a/src/core/file_sys/patch_manager.cpp b/src/core/file_sys/patch_manager.cpp index c228d253e..87c354a43 100644 --- a/src/core/file_sys/patch_manager.cpp +++ b/src/core/file_sys/patch_manager.cpp | |||
| @@ -27,6 +27,7 @@ | |||
| 27 | #include "core/settings.h" | 27 | #include "core/settings.h" |
| 28 | 28 | ||
| 29 | namespace FileSys { | 29 | namespace FileSys { |
| 30 | namespace { | ||
| 30 | 31 | ||
| 31 | constexpr u64 SINGLE_BYTE_MODULUS = 0x100; | 32 | constexpr u64 SINGLE_BYTE_MODULUS = 0x100; |
| 32 | constexpr u64 DLC_BASE_TITLE_ID_MASK = 0xFFFFFFFFFFFFE000; | 33 | constexpr u64 DLC_BASE_TITLE_ID_MASK = 0xFFFFFFFFFFFFE000; |
| @@ -36,19 +37,28 @@ constexpr std::array<const char*, 14> EXEFS_FILE_NAMES{ | |||
| 36 | "subsdk3", "subsdk4", "subsdk5", "subsdk6", "subsdk7", "subsdk8", "subsdk9", | 37 | "subsdk3", "subsdk4", "subsdk5", "subsdk6", "subsdk7", "subsdk8", "subsdk9", |
| 37 | }; | 38 | }; |
| 38 | 39 | ||
| 39 | std::string FormatTitleVersion(u32 version, TitleVersionFormat format) { | 40 | enum class TitleVersionFormat : u8 { |
| 41 | ThreeElements, ///< vX.Y.Z | ||
| 42 | FourElements, ///< vX.Y.Z.W | ||
| 43 | }; | ||
| 44 | |||
| 45 | std::string FormatTitleVersion(u32 version, | ||
| 46 | TitleVersionFormat format = TitleVersionFormat::ThreeElements) { | ||
| 40 | std::array<u8, sizeof(u32)> bytes{}; | 47 | std::array<u8, sizeof(u32)> bytes{}; |
| 41 | bytes[0] = version % SINGLE_BYTE_MODULUS; | 48 | bytes[0] = static_cast<u8>(version % SINGLE_BYTE_MODULUS); |
| 42 | for (std::size_t i = 1; i < bytes.size(); ++i) { | 49 | for (std::size_t i = 1; i < bytes.size(); ++i) { |
| 43 | version /= SINGLE_BYTE_MODULUS; | 50 | version /= SINGLE_BYTE_MODULUS; |
| 44 | bytes[i] = version % SINGLE_BYTE_MODULUS; | 51 | bytes[i] = static_cast<u8>(version % SINGLE_BYTE_MODULUS); |
| 45 | } | 52 | } |
| 46 | 53 | ||
| 47 | if (format == TitleVersionFormat::FourElements) | 54 | if (format == TitleVersionFormat::FourElements) { |
| 48 | return fmt::format("v{}.{}.{}.{}", bytes[3], bytes[2], bytes[1], bytes[0]); | 55 | return fmt::format("v{}.{}.{}.{}", bytes[3], bytes[2], bytes[1], bytes[0]); |
| 56 | } | ||
| 49 | return fmt::format("v{}.{}.{}", bytes[3], bytes[2], bytes[1]); | 57 | return fmt::format("v{}.{}.{}", bytes[3], bytes[2], bytes[1]); |
| 50 | } | 58 | } |
| 51 | 59 | ||
| 60 | // Returns a directory with name matching name case-insensitive. Returns nullptr if directory | ||
| 61 | // doesn't have a directory with name. | ||
| 52 | VirtualDir FindSubdirectoryCaseless(const VirtualDir dir, std::string_view name) { | 62 | VirtualDir FindSubdirectoryCaseless(const VirtualDir dir, std::string_view name) { |
| 53 | #ifdef _WIN32 | 63 | #ifdef _WIN32 |
| 54 | return dir->GetSubdirectory(name); | 64 | return dir->GetSubdirectory(name); |
| @@ -65,6 +75,45 @@ VirtualDir FindSubdirectoryCaseless(const VirtualDir dir, std::string_view name) | |||
| 65 | #endif | 75 | #endif |
| 66 | } | 76 | } |
| 67 | 77 | ||
| 78 | std::optional<std::vector<Core::Memory::CheatEntry>> ReadCheatFileFromFolder( | ||
| 79 | const Core::System& system, u64 title_id, const PatchManager::BuildID& build_id_, | ||
| 80 | const VirtualDir& base_path, bool upper) { | ||
| 81 | const auto build_id_raw = Common::HexToString(build_id_, upper); | ||
| 82 | const auto build_id = build_id_raw.substr(0, sizeof(u64) * 2); | ||
| 83 | const auto file = base_path->GetFile(fmt::format("{}.txt", build_id)); | ||
| 84 | |||
| 85 | if (file == nullptr) { | ||
| 86 | LOG_INFO(Common_Filesystem, "No cheats file found for title_id={:016X}, build_id={}", | ||
| 87 | title_id, build_id); | ||
| 88 | return std::nullopt; | ||
| 89 | } | ||
| 90 | |||
| 91 | std::vector<u8> data(file->GetSize()); | ||
| 92 | if (file->Read(data.data(), data.size()) != data.size()) { | ||
| 93 | LOG_INFO(Common_Filesystem, "Failed to read cheats file for title_id={:016X}, build_id={}", | ||
| 94 | title_id, build_id); | ||
| 95 | return std::nullopt; | ||
| 96 | } | ||
| 97 | |||
| 98 | Core::Memory::TextCheatParser parser; | ||
| 99 | return parser.Parse(system, | ||
| 100 | std::string_view(reinterpret_cast<const char*>(data.data()), data.size())); | ||
| 101 | } | ||
| 102 | |||
| 103 | void AppendCommaIfNotEmpty(std::string& to, std::string_view with) { | ||
| 104 | if (to.empty()) { | ||
| 105 | to += with; | ||
| 106 | } else { | ||
| 107 | to += ", "; | ||
| 108 | to += with; | ||
| 109 | } | ||
| 110 | } | ||
| 111 | |||
| 112 | bool IsDirValidAndNonEmpty(const VirtualDir& dir) { | ||
| 113 | return dir != nullptr && (!dir->GetFiles().empty() || !dir->GetSubdirectories().empty()); | ||
| 114 | } | ||
| 115 | } // Anonymous namespace | ||
| 116 | |||
| 68 | PatchManager::PatchManager(u64 title_id) : title_id(title_id) {} | 117 | PatchManager::PatchManager(u64 title_id) : title_id(title_id) {} |
| 69 | 118 | ||
| 70 | PatchManager::~PatchManager() = default; | 119 | PatchManager::~PatchManager() = default; |
| @@ -245,7 +294,7 @@ std::vector<u8> PatchManager::PatchNSO(const std::vector<u8>& nso, const std::st | |||
| 245 | return out; | 294 | return out; |
| 246 | } | 295 | } |
| 247 | 296 | ||
| 248 | bool PatchManager::HasNSOPatch(const std::array<u8, 32>& build_id_) const { | 297 | bool PatchManager::HasNSOPatch(const BuildID& build_id_) const { |
| 249 | const auto build_id_raw = Common::HexToString(build_id_); | 298 | const auto build_id_raw = Common::HexToString(build_id_); |
| 250 | const auto build_id = build_id_raw.substr(0, build_id_raw.find_last_not_of('0') + 1); | 299 | const auto build_id = build_id_raw.substr(0, build_id_raw.find_last_not_of('0') + 1); |
| 251 | 300 | ||
| @@ -265,36 +314,8 @@ bool PatchManager::HasNSOPatch(const std::array<u8, 32>& build_id_) const { | |||
| 265 | return !CollectPatches(patch_dirs, build_id).empty(); | 314 | return !CollectPatches(patch_dirs, build_id).empty(); |
| 266 | } | 315 | } |
| 267 | 316 | ||
| 268 | namespace { | ||
| 269 | std::optional<std::vector<Core::Memory::CheatEntry>> ReadCheatFileFromFolder( | ||
| 270 | const Core::System& system, u64 title_id, const std::array<u8, 0x20>& build_id_, | ||
| 271 | const VirtualDir& base_path, bool upper) { | ||
| 272 | const auto build_id_raw = Common::HexToString(build_id_, upper); | ||
| 273 | const auto build_id = build_id_raw.substr(0, sizeof(u64) * 2); | ||
| 274 | const auto file = base_path->GetFile(fmt::format("{}.txt", build_id)); | ||
| 275 | |||
| 276 | if (file == nullptr) { | ||
| 277 | LOG_INFO(Common_Filesystem, "No cheats file found for title_id={:016X}, build_id={}", | ||
| 278 | title_id, build_id); | ||
| 279 | return std::nullopt; | ||
| 280 | } | ||
| 281 | |||
| 282 | std::vector<u8> data(file->GetSize()); | ||
| 283 | if (file->Read(data.data(), data.size()) != data.size()) { | ||
| 284 | LOG_INFO(Common_Filesystem, "Failed to read cheats file for title_id={:016X}, build_id={}", | ||
| 285 | title_id, build_id); | ||
| 286 | return std::nullopt; | ||
| 287 | } | ||
| 288 | |||
| 289 | Core::Memory::TextCheatParser parser; | ||
| 290 | return parser.Parse(system, | ||
| 291 | std::string_view(reinterpret_cast<const char*>(data.data()), data.size())); | ||
| 292 | } | ||
| 293 | |||
| 294 | } // Anonymous namespace | ||
| 295 | |||
| 296 | std::vector<Core::Memory::CheatEntry> PatchManager::CreateCheatList( | 317 | std::vector<Core::Memory::CheatEntry> PatchManager::CreateCheatList( |
| 297 | const Core::System& system, const std::array<u8, 32>& build_id_) const { | 318 | const Core::System& system, const BuildID& build_id_) const { |
| 298 | const auto load_dir = system.GetFileSystemController().GetModificationLoadRoot(title_id); | 319 | const auto load_dir = system.GetFileSystemController().GetModificationLoadRoot(title_id); |
| 299 | if (load_dir == nullptr) { | 320 | if (load_dir == nullptr) { |
| 300 | LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id); | 321 | LOG_ERROR(Loader, "Cannot load mods for invalid title_id={:016X}", title_id); |
| @@ -435,21 +456,11 @@ VirtualFile PatchManager::PatchRomFS(VirtualFile romfs, u64 ivfc_offset, Content | |||
| 435 | return romfs; | 456 | return romfs; |
| 436 | } | 457 | } |
| 437 | 458 | ||
| 438 | static void AppendCommaIfNotEmpty(std::string& to, const std::string& with) { | 459 | PatchManager::PatchVersionNames PatchManager::GetPatchVersionNames(VirtualFile update_raw) const { |
| 439 | if (to.empty()) | 460 | if (title_id == 0) { |
| 440 | to += with; | ||
| 441 | else | ||
| 442 | to += ", " + with; | ||
| 443 | } | ||
| 444 | |||
| 445 | static bool IsDirValidAndNonEmpty(const VirtualDir& dir) { | ||
| 446 | return dir != nullptr && (!dir->GetFiles().empty() || !dir->GetSubdirectories().empty()); | ||
| 447 | } | ||
| 448 | |||
| 449 | std::map<std::string, std::string, std::less<>> PatchManager::GetPatchVersionNames( | ||
| 450 | VirtualFile update_raw) const { | ||
| 451 | if (title_id == 0) | ||
| 452 | return {}; | 461 | return {}; |
| 462 | } | ||
| 463 | |||
| 453 | std::map<std::string, std::string, std::less<>> out; | 464 | std::map<std::string, std::string, std::less<>> out; |
| 454 | const auto& installed = Core::System::GetInstance().GetContentProvider(); | 465 | const auto& installed = Core::System::GetInstance().GetContentProvider(); |
| 455 | const auto& disabled = Settings::values.disabled_addons[title_id]; | 466 | const auto& disabled = Settings::values.disabled_addons[title_id]; |
| @@ -472,8 +483,7 @@ std::map<std::string, std::string, std::less<>> PatchManager::GetPatchVersionNam | |||
| 472 | if (meta_ver.value_or(0) == 0) { | 483 | if (meta_ver.value_or(0) == 0) { |
| 473 | out.insert_or_assign(update_label, ""); | 484 | out.insert_or_assign(update_label, ""); |
| 474 | } else { | 485 | } else { |
| 475 | out.insert_or_assign( | 486 | out.insert_or_assign(update_label, FormatTitleVersion(*meta_ver)); |
| 476 | update_label, FormatTitleVersion(*meta_ver, TitleVersionFormat::ThreeElements)); | ||
| 477 | } | 487 | } |
| 478 | } else if (update_raw != nullptr) { | 488 | } else if (update_raw != nullptr) { |
| 479 | out.insert_or_assign(update_label, "PACKED"); | 489 | out.insert_or_assign(update_label, "PACKED"); |
| @@ -562,40 +572,46 @@ std::optional<u32> PatchManager::GetGameVersion() const { | |||
| 562 | return installed.GetEntryVersion(title_id); | 572 | return installed.GetEntryVersion(title_id); |
| 563 | } | 573 | } |
| 564 | 574 | ||
| 565 | std::pair<std::unique_ptr<NACP>, VirtualFile> PatchManager::GetControlMetadata() const { | 575 | PatchManager::Metadata PatchManager::GetControlMetadata() const { |
| 566 | const auto& installed = Core::System::GetInstance().GetContentProvider(); | 576 | const auto& installed = Core::System::GetInstance().GetContentProvider(); |
| 567 | 577 | ||
| 568 | const auto base_control_nca = installed.GetEntry(title_id, ContentRecordType::Control); | 578 | const auto base_control_nca = installed.GetEntry(title_id, ContentRecordType::Control); |
| 569 | if (base_control_nca == nullptr) | 579 | if (base_control_nca == nullptr) { |
| 570 | return {}; | 580 | return {}; |
| 581 | } | ||
| 571 | 582 | ||
| 572 | return ParseControlNCA(*base_control_nca); | 583 | return ParseControlNCA(*base_control_nca); |
| 573 | } | 584 | } |
| 574 | 585 | ||
| 575 | std::pair<std::unique_ptr<NACP>, VirtualFile> PatchManager::ParseControlNCA(const NCA& nca) const { | 586 | PatchManager::Metadata PatchManager::ParseControlNCA(const NCA& nca) const { |
| 576 | const auto base_romfs = nca.GetRomFS(); | 587 | const auto base_romfs = nca.GetRomFS(); |
| 577 | if (base_romfs == nullptr) | 588 | if (base_romfs == nullptr) { |
| 578 | return {}; | 589 | return {}; |
| 590 | } | ||
| 579 | 591 | ||
| 580 | const auto romfs = PatchRomFS(base_romfs, nca.GetBaseIVFCOffset(), ContentRecordType::Control); | 592 | const auto romfs = PatchRomFS(base_romfs, nca.GetBaseIVFCOffset(), ContentRecordType::Control); |
| 581 | if (romfs == nullptr) | 593 | if (romfs == nullptr) { |
| 582 | return {}; | 594 | return {}; |
| 595 | } | ||
| 583 | 596 | ||
| 584 | const auto extracted = ExtractRomFS(romfs); | 597 | const auto extracted = ExtractRomFS(romfs); |
| 585 | if (extracted == nullptr) | 598 | if (extracted == nullptr) { |
| 586 | return {}; | 599 | return {}; |
| 600 | } | ||
| 587 | 601 | ||
| 588 | auto nacp_file = extracted->GetFile("control.nacp"); | 602 | auto nacp_file = extracted->GetFile("control.nacp"); |
| 589 | if (nacp_file == nullptr) | 603 | if (nacp_file == nullptr) { |
| 590 | nacp_file = extracted->GetFile("Control.nacp"); | 604 | nacp_file = extracted->GetFile("Control.nacp"); |
| 605 | } | ||
| 591 | 606 | ||
| 592 | auto nacp = nacp_file == nullptr ? nullptr : std::make_unique<NACP>(nacp_file); | 607 | auto nacp = nacp_file == nullptr ? nullptr : std::make_unique<NACP>(nacp_file); |
| 593 | 608 | ||
| 594 | VirtualFile icon_file; | 609 | VirtualFile icon_file; |
| 595 | for (const auto& language : FileSys::LANGUAGE_NAMES) { | 610 | for (const auto& language : FileSys::LANGUAGE_NAMES) { |
| 596 | icon_file = extracted->GetFile("icon_" + std::string(language) + ".dat"); | 611 | icon_file = extracted->GetFile(std::string("icon_").append(language).append(".dat")); |
| 597 | if (icon_file != nullptr) | 612 | if (icon_file != nullptr) { |
| 598 | break; | 613 | break; |
| 614 | } | ||
| 599 | } | 615 | } |
| 600 | 616 | ||
| 601 | return {std::move(nacp), icon_file}; | 617 | return {std::move(nacp), icon_file}; |
diff --git a/src/core/file_sys/patch_manager.h b/src/core/file_sys/patch_manager.h index 532f4995f..1f28c6241 100644 --- a/src/core/file_sys/patch_manager.h +++ b/src/core/file_sys/patch_manager.h | |||
| @@ -22,70 +22,62 @@ namespace FileSys { | |||
| 22 | class NCA; | 22 | class NCA; |
| 23 | class NACP; | 23 | class NACP; |
| 24 | 24 | ||
| 25 | enum class TitleVersionFormat : u8 { | ||
| 26 | ThreeElements, ///< vX.Y.Z | ||
| 27 | FourElements, ///< vX.Y.Z.W | ||
| 28 | }; | ||
| 29 | |||
| 30 | std::string FormatTitleVersion(u32 version, | ||
| 31 | TitleVersionFormat format = TitleVersionFormat::ThreeElements); | ||
| 32 | |||
| 33 | // Returns a directory with name matching name case-insensitive. Returns nullptr if directory | ||
| 34 | // doesn't have a directory with name. | ||
| 35 | VirtualDir FindSubdirectoryCaseless(VirtualDir dir, std::string_view name); | ||
| 36 | |||
| 37 | // A centralized class to manage patches to games. | 25 | // A centralized class to manage patches to games. |
| 38 | class PatchManager { | 26 | class PatchManager { |
| 39 | public: | 27 | public: |
| 28 | using BuildID = std::array<u8, 0x20>; | ||
| 29 | using Metadata = std::pair<std::unique_ptr<NACP>, VirtualFile>; | ||
| 30 | using PatchVersionNames = std::map<std::string, std::string, std::less<>>; | ||
| 31 | |||
| 40 | explicit PatchManager(u64 title_id); | 32 | explicit PatchManager(u64 title_id); |
| 41 | ~PatchManager(); | 33 | ~PatchManager(); |
| 42 | 34 | ||
| 43 | u64 GetTitleID() const; | 35 | [[nodiscard]] u64 GetTitleID() const; |
| 44 | 36 | ||
| 45 | // Currently tracked ExeFS patches: | 37 | // Currently tracked ExeFS patches: |
| 46 | // - Game Updates | 38 | // - Game Updates |
| 47 | VirtualDir PatchExeFS(VirtualDir exefs) const; | 39 | [[nodiscard]] VirtualDir PatchExeFS(VirtualDir exefs) const; |
| 48 | 40 | ||
| 49 | // Currently tracked NSO patches: | 41 | // Currently tracked NSO patches: |
| 50 | // - IPS | 42 | // - IPS |
| 51 | // - IPSwitch | 43 | // - IPSwitch |
| 52 | std::vector<u8> PatchNSO(const std::vector<u8>& nso, const std::string& name) const; | 44 | [[nodiscard]] std::vector<u8> PatchNSO(const std::vector<u8>& nso, |
| 45 | const std::string& name) const; | ||
| 53 | 46 | ||
| 54 | // Checks to see if PatchNSO() will have any effect given the NSO's build ID. | 47 | // Checks to see if PatchNSO() will have any effect given the NSO's build ID. |
| 55 | // Used to prevent expensive copies in NSO loader. | 48 | // Used to prevent expensive copies in NSO loader. |
| 56 | bool HasNSOPatch(const std::array<u8, 0x20>& build_id) const; | 49 | [[nodiscard]] bool HasNSOPatch(const BuildID& build_id) const; |
| 57 | 50 | ||
| 58 | // Creates a CheatList object with all | 51 | // Creates a CheatList object with all |
| 59 | std::vector<Core::Memory::CheatEntry> CreateCheatList( | 52 | [[nodiscard]] std::vector<Core::Memory::CheatEntry> CreateCheatList( |
| 60 | const Core::System& system, const std::array<u8, 0x20>& build_id) const; | 53 | const Core::System& system, const BuildID& build_id) const; |
| 61 | 54 | ||
| 62 | // Currently tracked RomFS patches: | 55 | // Currently tracked RomFS patches: |
| 63 | // - Game Updates | 56 | // - Game Updates |
| 64 | // - LayeredFS | 57 | // - LayeredFS |
| 65 | VirtualFile PatchRomFS(VirtualFile base, u64 ivfc_offset, | 58 | [[nodiscard]] VirtualFile PatchRomFS(VirtualFile base, u64 ivfc_offset, |
| 66 | ContentRecordType type = ContentRecordType::Program, | 59 | ContentRecordType type = ContentRecordType::Program, |
| 67 | VirtualFile update_raw = nullptr) const; | 60 | VirtualFile update_raw = nullptr) const; |
| 68 | 61 | ||
| 69 | // Returns a vector of pairs between patch names and patch versions. | 62 | // Returns a vector of pairs between patch names and patch versions. |
| 70 | // i.e. Update 3.2.2 will return {"Update", "3.2.2"} | 63 | // i.e. Update 3.2.2 will return {"Update", "3.2.2"} |
| 71 | std::map<std::string, std::string, std::less<>> GetPatchVersionNames( | 64 | [[nodiscard]] PatchVersionNames GetPatchVersionNames(VirtualFile update_raw = nullptr) const; |
| 72 | VirtualFile update_raw = nullptr) const; | ||
| 73 | 65 | ||
| 74 | // If the game update exists, returns the u32 version field in its Meta-type NCA. If that fails, | 66 | // If the game update exists, returns the u32 version field in its Meta-type NCA. If that fails, |
| 75 | // it will fallback to the Meta-type NCA of the base game. If that fails, the result will be | 67 | // it will fallback to the Meta-type NCA of the base game. If that fails, the result will be |
| 76 | // std::nullopt | 68 | // std::nullopt |
| 77 | std::optional<u32> GetGameVersion() const; | 69 | [[nodiscard]] std::optional<u32> GetGameVersion() const; |
| 78 | 70 | ||
| 79 | // Given title_id of the program, attempts to get the control data of the update and parse | 71 | // Given title_id of the program, attempts to get the control data of the update and parse |
| 80 | // it, falling back to the base control data. | 72 | // it, falling back to the base control data. |
| 81 | std::pair<std::unique_ptr<NACP>, VirtualFile> GetControlMetadata() const; | 73 | [[nodiscard]] Metadata GetControlMetadata() const; |
| 82 | 74 | ||
| 83 | // Version of GetControlMetadata that takes an arbitrary NCA | 75 | // Version of GetControlMetadata that takes an arbitrary NCA |
| 84 | std::pair<std::unique_ptr<NACP>, VirtualFile> ParseControlNCA(const NCA& nca) const; | 76 | [[nodiscard]] Metadata ParseControlNCA(const NCA& nca) const; |
| 85 | 77 | ||
| 86 | private: | 78 | private: |
| 87 | std::vector<VirtualFile> CollectPatches(const std::vector<VirtualDir>& patch_dirs, | 79 | [[nodiscard]] std::vector<VirtualFile> CollectPatches(const std::vector<VirtualDir>& patch_dirs, |
| 88 | const std::string& build_id) const; | 80 | const std::string& build_id) const; |
| 89 | 81 | ||
| 90 | u64 title_id; | 82 | u64 title_id; |
| 91 | }; | 83 | }; |