mirror of
				https://git.suyu.dev/suyu/suyu
				synced 2025-10-30 15:39:02 -05:00 
			
		
		
		
	Merge pull request #2539 from DarkLordZach/bcat
bcat: Implement BCAT service and connect to yuzu Boxcat server
This commit is contained in:
		
							
								
								
									
										6
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							| @@ -46,3 +46,9 @@ | ||||
| [submodule "sirit"] | ||||
|     path = externals/sirit | ||||
|     url = https://github.com/ReinUsesLisp/sirit | ||||
| [submodule "libzip"] | ||||
| 	path = externals/libzip | ||||
| 	url = https://github.com/DarkLordZach/libzip | ||||
| [submodule "zlib"] | ||||
| 	path = externals/zlib | ||||
| 	url = https://github.com/DarkLordZach/zlib | ||||
|   | ||||
| @@ -21,6 +21,8 @@ option(YUZU_USE_BUNDLED_UNICORN "Build/Download bundled Unicorn" ON) | ||||
|  | ||||
| option(YUZU_USE_QT_WEB_ENGINE "Use QtWebEngine for web applet implementation" OFF) | ||||
|  | ||||
| option(YUZU_ENABLE_BOXCAT "Enable the Boxcat service, a yuzu high-level implementation of BCAT" ON) | ||||
|  | ||||
| option(ENABLE_CUBEB "Enables the cubeb audio backend" ON) | ||||
|  | ||||
| option(ENABLE_VULKAN "Enables Vulkan backend" ON) | ||||
|   | ||||
							
								
								
									
										6
									
								
								externals/CMakeLists.txt
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								externals/CMakeLists.txt
									
									
									
									
										vendored
									
									
								
							| @@ -77,6 +77,12 @@ if (ENABLE_VULKAN) | ||||
|     add_subdirectory(sirit) | ||||
| endif() | ||||
|  | ||||
| # libzip | ||||
| add_subdirectory(libzip) | ||||
|  | ||||
| # zlib | ||||
| add_subdirectory(zlib) | ||||
|  | ||||
| if (ENABLE_WEB_SERVICE) | ||||
|     # LibreSSL | ||||
|     set(LIBRESSL_SKIP_INSTALL ON CACHE BOOL "") | ||||
|   | ||||
							
								
								
									
										1
									
								
								externals/libzip
									
									
									
									
										vendored
									
									
										Submodule
									
								
							
							
								
								
								
								
								
							
						
						
									
										1
									
								
								externals/libzip
									
									
									
									
										vendored
									
									
										Submodule
									
								
							 Submodule externals/libzip added at bd7a8103e9
									
								
							
							
								
								
									
										1
									
								
								externals/zlib
									
									
									
									
										vendored
									
									
										Submodule
									
								
							
							
								
								
								
								
								
							
						
						
									
										1
									
								
								externals/zlib
									
									
									
									
										vendored
									
									
										Submodule
									
								
							 Submodule externals/zlib added at 094ed57db3
									
								
							| @@ -1,3 +1,9 @@ | ||||
| if (YUZU_ENABLE_BOXCAT) | ||||
|     set(BCAT_BOXCAT_ADDITIONAL_SOURCES hle/service/bcat/backend/boxcat.cpp hle/service/bcat/backend/boxcat.h) | ||||
| else() | ||||
|     set(BCAT_BOXCAT_ADDITIONAL_SOURCES) | ||||
| endif() | ||||
|  | ||||
| add_library(core STATIC | ||||
|     arm/arm_interface.h | ||||
|     arm/arm_interface.cpp | ||||
| @@ -82,6 +88,8 @@ add_library(core STATIC | ||||
|     file_sys/vfs_concat.h | ||||
|     file_sys/vfs_layered.cpp | ||||
|     file_sys/vfs_layered.h | ||||
|     file_sys/vfs_libzip.cpp | ||||
|     file_sys/vfs_libzip.h | ||||
|     file_sys/vfs_offset.cpp | ||||
|     file_sys/vfs_offset.h | ||||
|     file_sys/vfs_real.cpp | ||||
| @@ -241,6 +249,9 @@ add_library(core STATIC | ||||
|     hle/service/audio/errors.h | ||||
|     hle/service/audio/hwopus.cpp | ||||
|     hle/service/audio/hwopus.h | ||||
|     hle/service/bcat/backend/backend.cpp | ||||
|     hle/service/bcat/backend/backend.h | ||||
|     ${BCAT_BOXCAT_ADDITIONAL_SOURCES} | ||||
|     hle/service/bcat/bcat.cpp | ||||
|     hle/service/bcat/bcat.h | ||||
|     hle/service/bcat/module.cpp | ||||
| @@ -499,6 +510,15 @@ create_target_directory_groups(core) | ||||
|  | ||||
| target_link_libraries(core PUBLIC common PRIVATE audio_core video_core) | ||||
| target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt json-headers mbedtls opus unicorn open_source_archives) | ||||
|  | ||||
| if (YUZU_ENABLE_BOXCAT) | ||||
|     get_directory_property(OPENSSL_LIBS | ||||
|         DIRECTORY ${PROJECT_SOURCE_DIR}/externals/libressl | ||||
|         DEFINITION OPENSSL_LIBS) | ||||
|     target_compile_definitions(core PRIVATE -DCPPHTTPLIB_OPENSSL_SUPPORT -DYUZU_ENABLE_BOXCAT) | ||||
|     target_link_libraries(core PRIVATE httplib json-headers ${OPENSSL_LIBS} zip) | ||||
| endif() | ||||
|  | ||||
| if (ENABLE_WEB_SERVICE) | ||||
|     target_compile_definitions(core PRIVATE -DENABLE_WEB_SERVICE) | ||||
|     target_link_libraries(core PRIVATE web_service) | ||||
|   | ||||
| @@ -339,6 +339,7 @@ struct System::Impl { | ||||
|  | ||||
|     std::unique_ptr<Memory::CheatEngine> cheat_engine; | ||||
|     std::unique_ptr<Tools::Freezer> memory_freezer; | ||||
|     std::array<u8, 0x20> build_id{}; | ||||
|  | ||||
|     /// Frontend applets | ||||
|     Service::AM::Applets::AppletManager applet_manager; | ||||
| @@ -640,6 +641,14 @@ bool System::GetExitLock() const { | ||||
|     return impl->exit_lock; | ||||
| } | ||||
|  | ||||
| void System::SetCurrentProcessBuildID(std::array<u8, 32> id) { | ||||
|     impl->build_id = id; | ||||
| } | ||||
|  | ||||
| const std::array<u8, 32>& System::GetCurrentProcessBuildID() const { | ||||
|     return impl->build_id; | ||||
| } | ||||
|  | ||||
| System::ResultStatus System::Init(Frontend::EmuWindow& emu_window) { | ||||
|     return impl->Init(*this, emu_window); | ||||
| } | ||||
|   | ||||
| @@ -330,6 +330,10 @@ public: | ||||
|  | ||||
|     bool GetExitLock() const; | ||||
|  | ||||
|     void SetCurrentProcessBuildID(std::array<u8, 0x20> id); | ||||
|  | ||||
|     const std::array<u8, 0x20>& GetCurrentProcessBuildID() const; | ||||
|  | ||||
| private: | ||||
|     System(); | ||||
|  | ||||
|   | ||||
| @@ -136,4 +136,9 @@ u64 BISFactory::GetFullNANDTotalSpace() const { | ||||
|     return static_cast<u64>(Settings::values.nand_total_size); | ||||
| } | ||||
|  | ||||
| VirtualDir BISFactory::GetBCATDirectory(u64 title_id) const { | ||||
|     return GetOrCreateDirectoryRelative(nand_root, | ||||
|                                         fmt::format("/system/save/bcat/{:016X}", title_id)); | ||||
| } | ||||
|  | ||||
| } // namespace FileSys | ||||
|   | ||||
| @@ -61,6 +61,8 @@ public: | ||||
|     u64 GetUserNANDTotalSpace() const; | ||||
|     u64 GetFullNANDTotalSpace() const; | ||||
|  | ||||
|     VirtualDir GetBCATDirectory(u64 title_id) const; | ||||
|  | ||||
| private: | ||||
|     VirtualDir nand_root; | ||||
|     VirtualDir load_root; | ||||
|   | ||||
							
								
								
									
										79
									
								
								src/core/file_sys/vfs_libzip.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								src/core/file_sys/vfs_libzip.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,79 @@ | ||||
| // Copyright 2019 yuzu emulator team | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #include <string> | ||||
| #include <zip.h> | ||||
| #include "common/logging/backend.h" | ||||
| #include "core/file_sys/vfs.h" | ||||
| #include "core/file_sys/vfs_libzip.h" | ||||
| #include "core/file_sys/vfs_vector.h" | ||||
|  | ||||
| namespace FileSys { | ||||
|  | ||||
| VirtualDir ExtractZIP(VirtualFile file) { | ||||
|     zip_error_t error{}; | ||||
|  | ||||
|     const auto data = file->ReadAllBytes(); | ||||
|     std::unique_ptr<zip_source_t, decltype(&zip_source_close)> src{ | ||||
|         zip_source_buffer_create(data.data(), data.size(), 0, &error), zip_source_close}; | ||||
|     if (src == nullptr) | ||||
|         return nullptr; | ||||
|  | ||||
|     std::unique_ptr<zip_t, decltype(&zip_close)> zip{zip_open_from_source(src.get(), 0, &error), | ||||
|                                                      zip_close}; | ||||
|     if (zip == nullptr) | ||||
|         return nullptr; | ||||
|  | ||||
|     std::shared_ptr<VectorVfsDirectory> out = std::make_shared<VectorVfsDirectory>(); | ||||
|  | ||||
|     const auto num_entries = zip_get_num_entries(zip.get(), 0); | ||||
|  | ||||
|     zip_stat_t stat{}; | ||||
|     zip_stat_init(&stat); | ||||
|  | ||||
|     for (std::size_t i = 0; i < num_entries; ++i) { | ||||
|         const auto stat_res = zip_stat_index(zip.get(), i, 0, &stat); | ||||
|         if (stat_res == -1) | ||||
|             return nullptr; | ||||
|  | ||||
|         const std::string name(stat.name); | ||||
|         if (name.empty()) | ||||
|             continue; | ||||
|  | ||||
|         if (name.back() != '/') { | ||||
|             std::unique_ptr<zip_file_t, decltype(&zip_fclose)> file{ | ||||
|                 zip_fopen_index(zip.get(), i, 0), zip_fclose}; | ||||
|  | ||||
|             std::vector<u8> buf(stat.size); | ||||
|             if (zip_fread(file.get(), buf.data(), buf.size()) != buf.size()) | ||||
|                 return nullptr; | ||||
|  | ||||
|             const auto parts = FileUtil::SplitPathComponents(stat.name); | ||||
|             const auto new_file = std::make_shared<VectorVfsFile>(buf, parts.back()); | ||||
|  | ||||
|             std::shared_ptr<VectorVfsDirectory> dtrv = out; | ||||
|             for (std::size_t j = 0; j < parts.size() - 1; ++j) { | ||||
|                 if (dtrv == nullptr) | ||||
|                     return nullptr; | ||||
|                 const auto subdir = dtrv->GetSubdirectory(parts[j]); | ||||
|                 if (subdir == nullptr) { | ||||
|                     const auto temp = std::make_shared<VectorVfsDirectory>( | ||||
|                         std::vector<VirtualFile>{}, std::vector<VirtualDir>{}, parts[j]); | ||||
|                     dtrv->AddDirectory(temp); | ||||
|                     dtrv = temp; | ||||
|                 } else { | ||||
|                     dtrv = std::dynamic_pointer_cast<VectorVfsDirectory>(subdir); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             if (dtrv == nullptr) | ||||
|                 return nullptr; | ||||
|             dtrv->AddFile(new_file); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return out; | ||||
| } | ||||
|  | ||||
| } // namespace FileSys | ||||
							
								
								
									
										13
									
								
								src/core/file_sys/vfs_libzip.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/core/file_sys/vfs_libzip.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| // Copyright 2019 yuzu emulator team | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include "core/file_sys/vfs_types.h" | ||||
|  | ||||
| namespace FileSys { | ||||
|  | ||||
| VirtualDir ExtractZIP(VirtualFile zip); | ||||
|  | ||||
| } // namespace FileSys | ||||
| @@ -31,6 +31,7 @@ | ||||
| #include "core/hle/service/am/tcap.h" | ||||
| #include "core/hle/service/apm/controller.h" | ||||
| #include "core/hle/service/apm/interface.h" | ||||
| #include "core/hle/service/bcat/backend/backend.h" | ||||
| #include "core/hle/service/filesystem/filesystem.h" | ||||
| #include "core/hle/service/ns/ns.h" | ||||
| #include "core/hle/service/nvflinger/nvflinger.h" | ||||
| @@ -46,15 +47,20 @@ constexpr ResultCode ERR_NO_DATA_IN_CHANNEL{ErrorModule::AM, 0x2}; | ||||
| constexpr ResultCode ERR_NO_MESSAGES{ErrorModule::AM, 0x3}; | ||||
| constexpr ResultCode ERR_SIZE_OUT_OF_BOUNDS{ErrorModule::AM, 0x1F7}; | ||||
|  | ||||
| constexpr u32 POP_LAUNCH_PARAMETER_MAGIC = 0xC79497CA; | ||||
| enum class LaunchParameterKind : u32 { | ||||
|     ApplicationSpecific = 1, | ||||
|     AccountPreselectedUser = 2, | ||||
| }; | ||||
|  | ||||
| struct LaunchParameters { | ||||
| constexpr u32 LAUNCH_PARAMETER_ACCOUNT_PRESELECTED_USER_MAGIC = 0xC79497CA; | ||||
|  | ||||
| struct LaunchParameterAccountPreselectedUser { | ||||
|     u32_le magic; | ||||
|     u32_le is_account_selected; | ||||
|     u128 current_user; | ||||
|     INSERT_PADDING_BYTES(0x70); | ||||
| }; | ||||
| static_assert(sizeof(LaunchParameters) == 0x88); | ||||
| static_assert(sizeof(LaunchParameterAccountPreselectedUser) == 0x88); | ||||
|  | ||||
| IWindowController::IWindowController(Core::System& system_) | ||||
|     : ServiceFramework("IWindowController"), system{system_} { | ||||
| @@ -1128,26 +1134,55 @@ void IApplicationFunctions::EndBlockingHomeButton(Kernel::HLERequestContext& ctx | ||||
| } | ||||
|  | ||||
| void IApplicationFunctions::PopLaunchParameter(Kernel::HLERequestContext& ctx) { | ||||
|     LOG_DEBUG(Service_AM, "called"); | ||||
|     IPC::RequestParser rp{ctx}; | ||||
|     const auto kind = rp.PopEnum<LaunchParameterKind>(); | ||||
|  | ||||
|     LaunchParameters params{}; | ||||
|     LOG_DEBUG(Service_AM, "called, kind={:08X}", static_cast<u8>(kind)); | ||||
|  | ||||
|     params.magic = POP_LAUNCH_PARAMETER_MAGIC; | ||||
|     params.is_account_selected = 1; | ||||
|     if (kind == LaunchParameterKind::ApplicationSpecific && !launch_popped_application_specific) { | ||||
|         const auto backend = BCAT::CreateBackendFromSettings( | ||||
|             [this](u64 tid) { return system.GetFileSystemController().GetBCATDirectory(tid); }); | ||||
|         const auto build_id_full = Core::System::GetInstance().GetCurrentProcessBuildID(); | ||||
|         u64 build_id{}; | ||||
|         std::memcpy(&build_id, build_id_full.data(), sizeof(u64)); | ||||
|  | ||||
|     Account::ProfileManager profile_manager{}; | ||||
|     const auto uuid = profile_manager.GetUser(Settings::values.current_user); | ||||
|     ASSERT(uuid); | ||||
|     params.current_user = uuid->uuid; | ||||
|         const auto data = | ||||
|             backend->GetLaunchParameter({Core::CurrentProcess()->GetTitleID(), build_id}); | ||||
|  | ||||
|     IPC::ResponseBuilder rb{ctx, 2, 0, 1}; | ||||
|         if (data.has_value()) { | ||||
|             IPC::ResponseBuilder rb{ctx, 2, 0, 1}; | ||||
|             rb.Push(RESULT_SUCCESS); | ||||
|             rb.PushIpcInterface<AM::IStorage>(*data); | ||||
|             launch_popped_application_specific = true; | ||||
|             return; | ||||
|         } | ||||
|     } else if (kind == LaunchParameterKind::AccountPreselectedUser && | ||||
|                !launch_popped_account_preselect) { | ||||
|         LaunchParameterAccountPreselectedUser params{}; | ||||
|  | ||||
|     rb.Push(RESULT_SUCCESS); | ||||
|         params.magic = LAUNCH_PARAMETER_ACCOUNT_PRESELECTED_USER_MAGIC; | ||||
|         params.is_account_selected = 1; | ||||
|  | ||||
|     std::vector<u8> buffer(sizeof(LaunchParameters)); | ||||
|     std::memcpy(buffer.data(), ¶ms, buffer.size()); | ||||
|         Account::ProfileManager profile_manager{}; | ||||
|         const auto uuid = profile_manager.GetUser(Settings::values.current_user); | ||||
|         ASSERT(uuid); | ||||
|         params.current_user = uuid->uuid; | ||||
|  | ||||
|     rb.PushIpcInterface<AM::IStorage>(buffer); | ||||
|         IPC::ResponseBuilder rb{ctx, 2, 0, 1}; | ||||
|  | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|  | ||||
|         std::vector<u8> buffer(sizeof(LaunchParameterAccountPreselectedUser)); | ||||
|         std::memcpy(buffer.data(), ¶ms, buffer.size()); | ||||
|  | ||||
|         rb.PushIpcInterface<AM::IStorage>(buffer); | ||||
|         launch_popped_account_preselect = true; | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     LOG_ERROR(Service_AM, "Attempted to load launch parameter but none was found!"); | ||||
|     IPC::ResponseBuilder rb{ctx, 2}; | ||||
|     rb.Push(ERR_NO_DATA_IN_CHANNEL); | ||||
| } | ||||
|  | ||||
| void IApplicationFunctions::CreateApplicationAndRequestToStartForQuest( | ||||
|   | ||||
| @@ -255,6 +255,8 @@ private: | ||||
|     void EnableApplicationCrashReport(Kernel::HLERequestContext& ctx); | ||||
|     void GetGpuErrorDetectedSystemEvent(Kernel::HLERequestContext& ctx); | ||||
|  | ||||
|     bool launch_popped_application_specific = false; | ||||
|     bool launch_popped_account_preselect = false; | ||||
|     Kernel::EventPair gpu_error_detected_event; | ||||
|     Core::System& system; | ||||
| }; | ||||
|   | ||||
| @@ -157,6 +157,10 @@ AppletManager::AppletManager(Core::System& system_) : system{system_} {} | ||||
|  | ||||
| AppletManager::~AppletManager() = default; | ||||
|  | ||||
| const AppletFrontendSet& AppletManager::GetAppletFrontendSet() const { | ||||
|     return frontend; | ||||
| } | ||||
|  | ||||
| void AppletManager::SetAppletFrontendSet(AppletFrontendSet set) { | ||||
|     if (set.parental_controls != nullptr) | ||||
|         frontend.parental_controls = std::move(set.parental_controls); | ||||
|   | ||||
| @@ -190,6 +190,8 @@ public: | ||||
|     explicit AppletManager(Core::System& system_); | ||||
|     ~AppletManager(); | ||||
|  | ||||
|     const AppletFrontendSet& GetAppletFrontendSet() const; | ||||
|  | ||||
|     void SetAppletFrontendSet(AppletFrontendSet set); | ||||
|     void SetDefaultAppletFrontendSet(); | ||||
|     void SetDefaultAppletsIfMissing(); | ||||
|   | ||||
							
								
								
									
										136
									
								
								src/core/hle/service/bcat/backend/backend.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/core/hle/service/bcat/backend/backend.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | ||||
| // Copyright 2019 yuzu emulator team | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #include "common/hex_util.h" | ||||
| #include "common/logging/log.h" | ||||
| #include "core/core.h" | ||||
| #include "core/hle/lock.h" | ||||
| #include "core/hle/service/bcat/backend/backend.h" | ||||
|  | ||||
| namespace Service::BCAT { | ||||
|  | ||||
| ProgressServiceBackend::ProgressServiceBackend(std::string event_name) : impl{} { | ||||
|     auto& kernel{Core::System::GetInstance().Kernel()}; | ||||
|     event = Kernel::WritableEvent::CreateEventPair( | ||||
|         kernel, Kernel::ResetType::Automatic, "ProgressServiceBackend:UpdateEvent:" + event_name); | ||||
| } | ||||
|  | ||||
| Kernel::SharedPtr<Kernel::ReadableEvent> ProgressServiceBackend::GetEvent() { | ||||
|     return event.readable; | ||||
| } | ||||
|  | ||||
| DeliveryCacheProgressImpl& ProgressServiceBackend::GetImpl() { | ||||
|     return impl; | ||||
| } | ||||
|  | ||||
| void ProgressServiceBackend::SetNeedHLELock(bool need) { | ||||
|     need_hle_lock = need; | ||||
| } | ||||
|  | ||||
| void ProgressServiceBackend::SetTotalSize(u64 size) { | ||||
|     impl.total_bytes = size; | ||||
|     SignalUpdate(); | ||||
| } | ||||
|  | ||||
| void ProgressServiceBackend::StartConnecting() { | ||||
|     impl.status = DeliveryCacheProgressImpl::Status::Connecting; | ||||
|     SignalUpdate(); | ||||
| } | ||||
|  | ||||
| void ProgressServiceBackend::StartProcessingDataList() { | ||||
|     impl.status = DeliveryCacheProgressImpl::Status::ProcessingDataList; | ||||
|     SignalUpdate(); | ||||
| } | ||||
|  | ||||
| void ProgressServiceBackend::StartDownloadingFile(std::string_view dir_name, | ||||
|                                                   std::string_view file_name, u64 file_size) { | ||||
|     impl.status = DeliveryCacheProgressImpl::Status::Downloading; | ||||
|     impl.current_downloaded_bytes = 0; | ||||
|     impl.current_total_bytes = file_size; | ||||
|     std::memcpy(impl.current_directory.data(), dir_name.data(), | ||||
|                 std::min<u64>(dir_name.size(), 0x31ull)); | ||||
|     std::memcpy(impl.current_file.data(), file_name.data(), | ||||
|                 std::min<u64>(file_name.size(), 0x31ull)); | ||||
|     SignalUpdate(); | ||||
| } | ||||
|  | ||||
| void ProgressServiceBackend::UpdateFileProgress(u64 downloaded) { | ||||
|     impl.current_downloaded_bytes = downloaded; | ||||
|     SignalUpdate(); | ||||
| } | ||||
|  | ||||
| void ProgressServiceBackend::FinishDownloadingFile() { | ||||
|     impl.total_downloaded_bytes += impl.current_total_bytes; | ||||
|     SignalUpdate(); | ||||
| } | ||||
|  | ||||
| void ProgressServiceBackend::CommitDirectory(std::string_view dir_name) { | ||||
|     impl.status = DeliveryCacheProgressImpl::Status::Committing; | ||||
|     impl.current_file.fill(0); | ||||
|     impl.current_downloaded_bytes = 0; | ||||
|     impl.current_total_bytes = 0; | ||||
|     std::memcpy(impl.current_directory.data(), dir_name.data(), | ||||
|                 std::min<u64>(dir_name.size(), 0x31ull)); | ||||
|     SignalUpdate(); | ||||
| } | ||||
|  | ||||
| void ProgressServiceBackend::FinishDownload(ResultCode result) { | ||||
|     impl.total_downloaded_bytes = impl.total_bytes; | ||||
|     impl.status = DeliveryCacheProgressImpl::Status::Done; | ||||
|     impl.result = result; | ||||
|     SignalUpdate(); | ||||
| } | ||||
|  | ||||
| void ProgressServiceBackend::SignalUpdate() const { | ||||
|     if (need_hle_lock) { | ||||
|         std::lock_guard<std::recursive_mutex> lock(HLE::g_hle_lock); | ||||
|         event.writable->Signal(); | ||||
|     } else { | ||||
|         event.writable->Signal(); | ||||
|     } | ||||
| } | ||||
|  | ||||
| Backend::Backend(DirectoryGetter getter) : dir_getter(std::move(getter)) {} | ||||
|  | ||||
| Backend::~Backend() = default; | ||||
|  | ||||
| NullBackend::NullBackend(const DirectoryGetter& getter) : Backend(std::move(getter)) {} | ||||
|  | ||||
| NullBackend::~NullBackend() = default; | ||||
|  | ||||
| bool NullBackend::Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) { | ||||
|     LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, build_id={:016X}", title.title_id, | ||||
|               title.build_id); | ||||
|  | ||||
|     progress.FinishDownload(RESULT_SUCCESS); | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| bool NullBackend::SynchronizeDirectory(TitleIDVersion title, std::string name, | ||||
|                                        ProgressServiceBackend& progress) { | ||||
|     LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, build_id={:016X}, name={}", title.title_id, | ||||
|               title.build_id, name); | ||||
|  | ||||
|     progress.FinishDownload(RESULT_SUCCESS); | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| bool NullBackend::Clear(u64 title_id) { | ||||
|     LOG_DEBUG(Service_BCAT, "called, title_id={:016X}"); | ||||
|  | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| void NullBackend::SetPassphrase(u64 title_id, const Passphrase& passphrase) { | ||||
|     LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, passphrase = {}", title_id, | ||||
|               Common::HexToString(passphrase)); | ||||
| } | ||||
|  | ||||
| std::optional<std::vector<u8>> NullBackend::GetLaunchParameter(TitleIDVersion title) { | ||||
|     LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, build_id={:016X}", title.title_id, | ||||
|               title.build_id); | ||||
|     return std::nullopt; | ||||
| } | ||||
|  | ||||
| } // namespace Service::BCAT | ||||
							
								
								
									
										147
									
								
								src/core/hle/service/bcat/backend/backend.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								src/core/hle/service/bcat/backend/backend.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| // Copyright 2019 yuzu emulator team | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <functional> | ||||
| #include <optional> | ||||
| #include "common/common_types.h" | ||||
| #include "core/file_sys/vfs_types.h" | ||||
| #include "core/hle/kernel/readable_event.h" | ||||
| #include "core/hle/kernel/writable_event.h" | ||||
| #include "core/hle/result.h" | ||||
|  | ||||
| namespace Service::BCAT { | ||||
|  | ||||
| struct DeliveryCacheProgressImpl; | ||||
|  | ||||
| using DirectoryGetter = std::function<FileSys::VirtualDir(u64)>; | ||||
| using Passphrase = std::array<u8, 0x20>; | ||||
|  | ||||
| struct TitleIDVersion { | ||||
|     u64 title_id; | ||||
|     u64 build_id; | ||||
| }; | ||||
|  | ||||
| using DirectoryName = std::array<char, 0x20>; | ||||
| using FileName = std::array<char, 0x20>; | ||||
|  | ||||
| struct DeliveryCacheProgressImpl { | ||||
|     enum class Status : s32 { | ||||
|         None = 0x0, | ||||
|         Queued = 0x1, | ||||
|         Connecting = 0x2, | ||||
|         ProcessingDataList = 0x3, | ||||
|         Downloading = 0x4, | ||||
|         Committing = 0x5, | ||||
|         Done = 0x9, | ||||
|     }; | ||||
|  | ||||
|     Status status; | ||||
|     ResultCode result = RESULT_SUCCESS; | ||||
|     DirectoryName current_directory; | ||||
|     FileName current_file; | ||||
|     s64 current_downloaded_bytes; ///< Bytes downloaded on current file. | ||||
|     s64 current_total_bytes;      ///< Bytes total on current file. | ||||
|     s64 total_downloaded_bytes;   ///< Bytes downloaded on overall download. | ||||
|     s64 total_bytes;              ///< Bytes total on overall download. | ||||
|     INSERT_PADDING_BYTES( | ||||
|         0x198); ///< Appears to be unused in official code, possibly reserved for future use. | ||||
| }; | ||||
| static_assert(sizeof(DeliveryCacheProgressImpl) == 0x200, | ||||
|               "DeliveryCacheProgressImpl has incorrect size."); | ||||
|  | ||||
| // A class to manage the signalling to the game about BCAT download progress. | ||||
| // Some of this class is implemented in module.cpp to avoid exposing the implementation structure. | ||||
| class ProgressServiceBackend { | ||||
|     friend class IBcatService; | ||||
|  | ||||
| public: | ||||
|     // Clients should call this with true if any of the functions are going to be called from a | ||||
|     // non-HLE thread and this class need to lock the hle mutex. (default is false) | ||||
|     void SetNeedHLELock(bool need); | ||||
|  | ||||
|     // Sets the number of bytes total in the entire download. | ||||
|     void SetTotalSize(u64 size); | ||||
|  | ||||
|     // Notifies the application that the backend has started connecting to the server. | ||||
|     void StartConnecting(); | ||||
|     // Notifies the application that the backend has begun accumulating and processing metadata. | ||||
|     void StartProcessingDataList(); | ||||
|  | ||||
|     // Notifies the application that a file is starting to be downloaded. | ||||
|     void StartDownloadingFile(std::string_view dir_name, std::string_view file_name, u64 file_size); | ||||
|     // Updates the progress of the current file to the size passed. | ||||
|     void UpdateFileProgress(u64 downloaded); | ||||
|     // Notifies the application that the current file has completed download. | ||||
|     void FinishDownloadingFile(); | ||||
|  | ||||
|     // Notifies the application that all files in this directory have completed and are being | ||||
|     // finalized. | ||||
|     void CommitDirectory(std::string_view dir_name); | ||||
|  | ||||
|     // Notifies the application that the operation completed with result code result. | ||||
|     void FinishDownload(ResultCode result); | ||||
|  | ||||
| private: | ||||
|     explicit ProgressServiceBackend(std::string event_name); | ||||
|  | ||||
|     Kernel::SharedPtr<Kernel::ReadableEvent> GetEvent(); | ||||
|     DeliveryCacheProgressImpl& GetImpl(); | ||||
|  | ||||
|     void SignalUpdate() const; | ||||
|  | ||||
|     DeliveryCacheProgressImpl impl; | ||||
|     Kernel::EventPair event; | ||||
|     bool need_hle_lock = false; | ||||
| }; | ||||
|  | ||||
| // A class representing an abstract backend for BCAT functionality. | ||||
| class Backend { | ||||
| public: | ||||
|     explicit Backend(DirectoryGetter getter); | ||||
|     virtual ~Backend(); | ||||
|  | ||||
|     // Called when the backend is needed to synchronize the data for the game with title ID and | ||||
|     // version in title. A ProgressServiceBackend object is provided to alert the application of | ||||
|     // status. | ||||
|     virtual bool Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) = 0; | ||||
|     // Very similar to Synchronize, but only for the directory provided. Backends should not alter | ||||
|     // the data for any other directories. | ||||
|     virtual bool SynchronizeDirectory(TitleIDVersion title, std::string name, | ||||
|                                       ProgressServiceBackend& progress) = 0; | ||||
|  | ||||
|     // Removes all cached data associated with title id provided. | ||||
|     virtual bool Clear(u64 title_id) = 0; | ||||
|  | ||||
|     // Sets the BCAT Passphrase to be used with the associated title ID. | ||||
|     virtual void SetPassphrase(u64 title_id, const Passphrase& passphrase) = 0; | ||||
|  | ||||
|     // Gets the launch parameter used by AM associated with the title ID and version provided. | ||||
|     virtual std::optional<std::vector<u8>> GetLaunchParameter(TitleIDVersion title) = 0; | ||||
|  | ||||
| protected: | ||||
|     DirectoryGetter dir_getter; | ||||
| }; | ||||
|  | ||||
| // A backend of BCAT that provides no operation. | ||||
| class NullBackend : public Backend { | ||||
| public: | ||||
|     explicit NullBackend(const DirectoryGetter& getter); | ||||
|     ~NullBackend() override; | ||||
|  | ||||
|     bool Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) override; | ||||
|     bool SynchronizeDirectory(TitleIDVersion title, std::string name, | ||||
|                               ProgressServiceBackend& progress) override; | ||||
|  | ||||
|     bool Clear(u64 title_id) override; | ||||
|  | ||||
|     void SetPassphrase(u64 title_id, const Passphrase& passphrase) override; | ||||
|  | ||||
|     std::optional<std::vector<u8>> GetLaunchParameter(TitleIDVersion title) override; | ||||
| }; | ||||
|  | ||||
| std::unique_ptr<Backend> CreateBackendFromSettings(DirectoryGetter getter); | ||||
|  | ||||
| } // namespace Service::BCAT | ||||
							
								
								
									
										503
									
								
								src/core/hle/service/bcat/backend/boxcat.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										503
									
								
								src/core/hle/service/bcat/backend/boxcat.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,503 @@ | ||||
| // Copyright 2019 yuzu emulator team | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #include <fmt/ostream.h> | ||||
| #include <httplib.h> | ||||
| #include <json.hpp> | ||||
| #include <mbedtls/sha256.h> | ||||
| #include "common/hex_util.h" | ||||
| #include "common/logging/backend.h" | ||||
| #include "common/logging/log.h" | ||||
| #include "core/core.h" | ||||
| #include "core/file_sys/vfs.h" | ||||
| #include "core/file_sys/vfs_libzip.h" | ||||
| #include "core/file_sys/vfs_vector.h" | ||||
| #include "core/frontend/applets/error.h" | ||||
| #include "core/hle/service/am/applets/applets.h" | ||||
| #include "core/hle/service/bcat/backend/boxcat.h" | ||||
| #include "core/settings.h" | ||||
|  | ||||
| namespace { | ||||
|  | ||||
| // Prevents conflicts with windows macro called CreateFile | ||||
| FileSys::VirtualFile VfsCreateFileWrap(FileSys::VirtualDir dir, std::string_view name) { | ||||
|     return dir->CreateFile(name); | ||||
| } | ||||
|  | ||||
| // Prevents conflicts with windows macro called DeleteFile | ||||
| bool VfsDeleteFileWrap(FileSys::VirtualDir dir, std::string_view name) { | ||||
|     return dir->DeleteFile(name); | ||||
| } | ||||
|  | ||||
| } // Anonymous namespace | ||||
|  | ||||
| namespace Service::BCAT { | ||||
|  | ||||
| constexpr ResultCode ERROR_GENERAL_BCAT_FAILURE{ErrorModule::BCAT, 1}; | ||||
|  | ||||
| constexpr char BOXCAT_HOSTNAME[] = "api.yuzu-emu.org"; | ||||
|  | ||||
| // Formatted using fmt with arg[0] = hex title id | ||||
| constexpr char BOXCAT_PATHNAME_DATA[] = "/game-assets/{:016X}/boxcat"; | ||||
| constexpr char BOXCAT_PATHNAME_LAUNCHPARAM[] = "/game-assets/{:016X}/launchparam"; | ||||
|  | ||||
| constexpr char BOXCAT_PATHNAME_EVENTS[] = "/game-assets/boxcat/events"; | ||||
|  | ||||
| constexpr char BOXCAT_API_VERSION[] = "1"; | ||||
| constexpr char BOXCAT_CLIENT_TYPE[] = "yuzu"; | ||||
|  | ||||
| // HTTP status codes for Boxcat | ||||
| enum class ResponseStatus { | ||||
|     Ok = 200,               ///< Operation completed successfully. | ||||
|     BadClientVersion = 301, ///< The Boxcat-Client-Version doesn't match the server. | ||||
|     NoUpdate = 304,         ///< The digest provided would match the new data, no need to update. | ||||
|     NoMatchTitleId = 404,   ///< The title ID provided doesn't have a boxcat implementation. | ||||
|     NoMatchBuildId = 406,   ///< The build ID provided is blacklisted (potentially because of format | ||||
|                             ///< issues or whatnot) and has no data. | ||||
| }; | ||||
|  | ||||
| enum class DownloadResult { | ||||
|     Success = 0, | ||||
|     NoResponse, | ||||
|     GeneralWebError, | ||||
|     NoMatchTitleId, | ||||
|     NoMatchBuildId, | ||||
|     InvalidContentType, | ||||
|     GeneralFSError, | ||||
|     BadClientVersion, | ||||
| }; | ||||
|  | ||||
| constexpr std::array<const char*, 8> DOWNLOAD_RESULT_LOG_MESSAGES{ | ||||
|     "Success", | ||||
|     "There was no response from the server.", | ||||
|     "There was a general web error code returned from the server.", | ||||
|     "The title ID of the current game doesn't have a boxcat implementation. If you believe an " | ||||
|     "implementation should be added, contact yuzu support.", | ||||
|     "The build ID of the current version of the game is marked as incompatible with the current " | ||||
|     "BCAT distribution. Try upgrading or downgrading your game version or contacting yuzu support.", | ||||
|     "The content type of the web response was invalid.", | ||||
|     "There was a general filesystem error while saving the zip file.", | ||||
|     "The server is either too new or too old to serve the request. Try using the latest version of " | ||||
|     "an official release of yuzu.", | ||||
| }; | ||||
|  | ||||
| std::ostream& operator<<(std::ostream& os, DownloadResult result) { | ||||
|     return os << DOWNLOAD_RESULT_LOG_MESSAGES.at(static_cast<std::size_t>(result)); | ||||
| } | ||||
|  | ||||
| constexpr u32 PORT = 443; | ||||
| constexpr u32 TIMEOUT_SECONDS = 30; | ||||
| constexpr u64 VFS_COPY_BLOCK_SIZE = 1ull << 24; // 4MB | ||||
|  | ||||
| namespace { | ||||
|  | ||||
| std::string GetBINFilePath(u64 title_id) { | ||||
|     return fmt::format("{}bcat/{:016X}/launchparam.bin", | ||||
|                        FileUtil::GetUserPath(FileUtil::UserPath::CacheDir), title_id); | ||||
| } | ||||
|  | ||||
| std::string GetZIPFilePath(u64 title_id) { | ||||
|     return fmt::format("{}bcat/{:016X}/data.zip", | ||||
|                        FileUtil::GetUserPath(FileUtil::UserPath::CacheDir), title_id); | ||||
| } | ||||
|  | ||||
| // If the error is something the user should know about (build ID mismatch, bad client version), | ||||
| // display an error. | ||||
| void HandleDownloadDisplayResult(DownloadResult res) { | ||||
|     if (res == DownloadResult::Success || res == DownloadResult::NoResponse || | ||||
|         res == DownloadResult::GeneralWebError || res == DownloadResult::GeneralFSError || | ||||
|         res == DownloadResult::NoMatchTitleId || res == DownloadResult::InvalidContentType) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const auto& frontend{Core::System::GetInstance().GetAppletManager().GetAppletFrontendSet()}; | ||||
|     frontend.error->ShowCustomErrorText( | ||||
|         ResultCode(-1), "There was an error while attempting to use Boxcat.", | ||||
|         DOWNLOAD_RESULT_LOG_MESSAGES[static_cast<std::size_t>(res)], [] {}); | ||||
| } | ||||
|  | ||||
| bool VfsRawCopyProgress(FileSys::VirtualFile src, FileSys::VirtualFile dest, | ||||
|                         std::string_view dir_name, ProgressServiceBackend& progress, | ||||
|                         std::size_t block_size = 0x1000) { | ||||
|     if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable()) | ||||
|         return false; | ||||
|     if (!dest->Resize(src->GetSize())) | ||||
|         return false; | ||||
|  | ||||
|     progress.StartDownloadingFile(dir_name, src->GetName(), src->GetSize()); | ||||
|  | ||||
|     std::vector<u8> temp(std::min(block_size, src->GetSize())); | ||||
|     for (std::size_t i = 0; i < src->GetSize(); i += block_size) { | ||||
|         const auto read = std::min(block_size, src->GetSize() - i); | ||||
|  | ||||
|         if (src->Read(temp.data(), read, i) != read) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         if (dest->Write(temp.data(), read, i) != read) { | ||||
|             return false; | ||||
|         } | ||||
|  | ||||
|         progress.UpdateFileProgress(i); | ||||
|     } | ||||
|  | ||||
|     progress.FinishDownloadingFile(); | ||||
|  | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| bool VfsRawCopyDProgressSingle(FileSys::VirtualDir src, FileSys::VirtualDir dest, | ||||
|                                ProgressServiceBackend& progress, std::size_t block_size = 0x1000) { | ||||
|     if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable()) | ||||
|         return false; | ||||
|  | ||||
|     for (const auto& file : src->GetFiles()) { | ||||
|         const auto out_file = VfsCreateFileWrap(dest, file->GetName()); | ||||
|         if (!VfsRawCopyProgress(file, out_file, src->GetName(), progress, block_size)) { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|     progress.CommitDirectory(src->GetName()); | ||||
|  | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| bool VfsRawCopyDProgress(FileSys::VirtualDir src, FileSys::VirtualDir dest, | ||||
|                          ProgressServiceBackend& progress, std::size_t block_size = 0x1000) { | ||||
|     if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable()) | ||||
|         return false; | ||||
|  | ||||
|     for (const auto& dir : src->GetSubdirectories()) { | ||||
|         const auto out = dest->CreateSubdirectory(dir->GetName()); | ||||
|         if (!VfsRawCopyDProgressSingle(dir, out, progress, block_size)) { | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| } // Anonymous namespace | ||||
|  | ||||
| class Boxcat::Client { | ||||
| public: | ||||
|     Client(std::string path, u64 title_id, u64 build_id) | ||||
|         : path(std::move(path)), title_id(title_id), build_id(build_id) {} | ||||
|  | ||||
|     DownloadResult DownloadDataZip() { | ||||
|         return DownloadInternal(fmt::format(BOXCAT_PATHNAME_DATA, title_id), TIMEOUT_SECONDS, | ||||
|                                 "application/zip"); | ||||
|     } | ||||
|  | ||||
|     DownloadResult DownloadLaunchParam() { | ||||
|         return DownloadInternal(fmt::format(BOXCAT_PATHNAME_LAUNCHPARAM, title_id), | ||||
|                                 TIMEOUT_SECONDS / 3, "application/octet-stream"); | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     DownloadResult DownloadInternal(const std::string& resolved_path, u32 timeout_seconds, | ||||
|                                     const std::string& content_type_name) { | ||||
|         if (client == nullptr) { | ||||
|             client = std::make_unique<httplib::SSLClient>(BOXCAT_HOSTNAME, PORT, timeout_seconds); | ||||
|         } | ||||
|  | ||||
|         httplib::Headers headers{ | ||||
|             {std::string("Game-Assets-API-Version"), std::string(BOXCAT_API_VERSION)}, | ||||
|             {std::string("Boxcat-Client-Type"), std::string(BOXCAT_CLIENT_TYPE)}, | ||||
|             {std::string("Game-Build-Id"), fmt::format("{:016X}", build_id)}, | ||||
|         }; | ||||
|  | ||||
|         if (FileUtil::Exists(path)) { | ||||
|             FileUtil::IOFile file{path, "rb"}; | ||||
|             if (file.IsOpen()) { | ||||
|                 std::vector<u8> bytes(file.GetSize()); | ||||
|                 file.ReadBytes(bytes.data(), bytes.size()); | ||||
|                 const auto digest = DigestFile(bytes); | ||||
|                 headers.insert({std::string("If-None-Match"), Common::HexToString(digest, false)}); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         const auto response = client->Get(resolved_path.c_str(), headers); | ||||
|         if (response == nullptr) | ||||
|             return DownloadResult::NoResponse; | ||||
|  | ||||
|         if (response->status == static_cast<int>(ResponseStatus::NoUpdate)) | ||||
|             return DownloadResult::Success; | ||||
|         if (response->status == static_cast<int>(ResponseStatus::BadClientVersion)) | ||||
|             return DownloadResult::BadClientVersion; | ||||
|         if (response->status == static_cast<int>(ResponseStatus::NoMatchTitleId)) | ||||
|             return DownloadResult::NoMatchTitleId; | ||||
|         if (response->status == static_cast<int>(ResponseStatus::NoMatchBuildId)) | ||||
|             return DownloadResult::NoMatchBuildId; | ||||
|         if (response->status != static_cast<int>(ResponseStatus::Ok)) | ||||
|             return DownloadResult::GeneralWebError; | ||||
|  | ||||
|         const auto content_type = response->headers.find("content-type"); | ||||
|         if (content_type == response->headers.end() || | ||||
|             content_type->second.find(content_type_name) == std::string::npos) { | ||||
|             return DownloadResult::InvalidContentType; | ||||
|         } | ||||
|  | ||||
|         FileUtil::CreateFullPath(path); | ||||
|         FileUtil::IOFile file{path, "wb"}; | ||||
|         if (!file.IsOpen()) | ||||
|             return DownloadResult::GeneralFSError; | ||||
|         if (!file.Resize(response->body.size())) | ||||
|             return DownloadResult::GeneralFSError; | ||||
|         if (file.WriteBytes(response->body.data(), response->body.size()) != response->body.size()) | ||||
|             return DownloadResult::GeneralFSError; | ||||
|  | ||||
|         return DownloadResult::Success; | ||||
|     } | ||||
|  | ||||
|     using Digest = std::array<u8, 0x20>; | ||||
|     static Digest DigestFile(std::vector<u8> bytes) { | ||||
|         Digest out{}; | ||||
|         mbedtls_sha256(bytes.data(), bytes.size(), out.data(), 0); | ||||
|         return out; | ||||
|     } | ||||
|  | ||||
|     std::unique_ptr<httplib::Client> client; | ||||
|     std::string path; | ||||
|     u64 title_id; | ||||
|     u64 build_id; | ||||
| }; | ||||
|  | ||||
| Boxcat::Boxcat(DirectoryGetter getter) : Backend(std::move(getter)) {} | ||||
|  | ||||
| Boxcat::~Boxcat() = default; | ||||
|  | ||||
| void SynchronizeInternal(DirectoryGetter dir_getter, TitleIDVersion title, | ||||
|                          ProgressServiceBackend& progress, | ||||
|                          std::optional<std::string> dir_name = {}) { | ||||
|     progress.SetNeedHLELock(true); | ||||
|  | ||||
|     if (Settings::values.bcat_boxcat_local) { | ||||
|         LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping download."); | ||||
|         const auto dir = dir_getter(title.title_id); | ||||
|         if (dir) | ||||
|             progress.SetTotalSize(dir->GetSize()); | ||||
|         progress.FinishDownload(RESULT_SUCCESS); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const auto zip_path{GetZIPFilePath(title.title_id)}; | ||||
|     Boxcat::Client client{zip_path, title.title_id, title.build_id}; | ||||
|  | ||||
|     progress.StartConnecting(); | ||||
|  | ||||
|     const auto res = client.DownloadDataZip(); | ||||
|     if (res != DownloadResult::Success) { | ||||
|         LOG_ERROR(Service_BCAT, "Boxcat synchronization failed with error '{}'!", res); | ||||
|  | ||||
|         if (res == DownloadResult::NoMatchBuildId || res == DownloadResult::NoMatchTitleId) { | ||||
|             FileUtil::Delete(zip_path); | ||||
|         } | ||||
|  | ||||
|         HandleDownloadDisplayResult(res); | ||||
|         progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     progress.StartProcessingDataList(); | ||||
|  | ||||
|     FileUtil::IOFile zip{zip_path, "rb"}; | ||||
|     const auto size = zip.GetSize(); | ||||
|     std::vector<u8> bytes(size); | ||||
|     if (!zip.IsOpen() || size == 0 || zip.ReadBytes(bytes.data(), bytes.size()) != bytes.size()) { | ||||
|         LOG_ERROR(Service_BCAT, "Boxcat failed to read ZIP file at path '{}'!", zip_path); | ||||
|         progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const auto extracted = FileSys::ExtractZIP(std::make_shared<FileSys::VectorVfsFile>(bytes)); | ||||
|     if (extracted == nullptr) { | ||||
|         LOG_ERROR(Service_BCAT, "Boxcat failed to extract ZIP file!"); | ||||
|         progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     if (dir_name == std::nullopt) { | ||||
|         progress.SetTotalSize(extracted->GetSize()); | ||||
|  | ||||
|         const auto target_dir = dir_getter(title.title_id); | ||||
|         if (target_dir == nullptr || !VfsRawCopyDProgress(extracted, target_dir, progress)) { | ||||
|             LOG_ERROR(Service_BCAT, "Boxcat failed to copy extracted ZIP to target directory!"); | ||||
|             progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE); | ||||
|             return; | ||||
|         } | ||||
|     } else { | ||||
|         const auto target_dir = dir_getter(title.title_id); | ||||
|         if (target_dir == nullptr) { | ||||
|             LOG_ERROR(Service_BCAT, "Boxcat failed to get directory for title ID!"); | ||||
|             progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const auto target_sub = target_dir->GetSubdirectory(*dir_name); | ||||
|         const auto source_sub = extracted->GetSubdirectory(*dir_name); | ||||
|  | ||||
|         progress.SetTotalSize(source_sub->GetSize()); | ||||
|  | ||||
|         std::vector<std::string> filenames; | ||||
|         { | ||||
|             const auto files = target_sub->GetFiles(); | ||||
|             std::transform(files.begin(), files.end(), std::back_inserter(filenames), | ||||
|                            [](const auto& vfile) { return vfile->GetName(); }); | ||||
|         } | ||||
|  | ||||
|         for (const auto& filename : filenames) { | ||||
|             VfsDeleteFileWrap(target_sub, filename); | ||||
|         } | ||||
|  | ||||
|         if (target_sub == nullptr || source_sub == nullptr || | ||||
|             !VfsRawCopyDProgressSingle(source_sub, target_sub, progress)) { | ||||
|             LOG_ERROR(Service_BCAT, "Boxcat failed to copy extracted ZIP to target directory!"); | ||||
|             progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE); | ||||
|             return; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     progress.FinishDownload(RESULT_SUCCESS); | ||||
| } | ||||
|  | ||||
| bool Boxcat::Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) { | ||||
|     is_syncing.exchange(true); | ||||
|     std::thread([this, title, &progress] { SynchronizeInternal(dir_getter, title, progress); }) | ||||
|         .detach(); | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| bool Boxcat::SynchronizeDirectory(TitleIDVersion title, std::string name, | ||||
|                                   ProgressServiceBackend& progress) { | ||||
|     is_syncing.exchange(true); | ||||
|     std::thread( | ||||
|         [this, title, name, &progress] { SynchronizeInternal(dir_getter, title, progress, name); }) | ||||
|         .detach(); | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| bool Boxcat::Clear(u64 title_id) { | ||||
|     if (Settings::values.bcat_boxcat_local) { | ||||
|         LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping clear."); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     const auto dir = dir_getter(title_id); | ||||
|  | ||||
|     std::vector<std::string> dirnames; | ||||
|  | ||||
|     for (const auto& subdir : dir->GetSubdirectories()) | ||||
|         dirnames.push_back(subdir->GetName()); | ||||
|  | ||||
|     for (const auto& subdir : dirnames) { | ||||
|         if (!dir->DeleteSubdirectoryRecursive(subdir)) | ||||
|             return false; | ||||
|     } | ||||
|  | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| void Boxcat::SetPassphrase(u64 title_id, const Passphrase& passphrase) { | ||||
|     LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, passphrase={}", title_id, | ||||
|               Common::HexToString(passphrase)); | ||||
| } | ||||
|  | ||||
| std::optional<std::vector<u8>> Boxcat::GetLaunchParameter(TitleIDVersion title) { | ||||
|     const auto path{GetBINFilePath(title.title_id)}; | ||||
|  | ||||
|     if (Settings::values.bcat_boxcat_local) { | ||||
|         LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping download."); | ||||
|     } else { | ||||
|         Boxcat::Client client{path, title.title_id, title.build_id}; | ||||
|  | ||||
|         const auto res = client.DownloadLaunchParam(); | ||||
|         if (res != DownloadResult::Success) { | ||||
|             LOG_ERROR(Service_BCAT, "Boxcat synchronization failed with error '{}'!", res); | ||||
|  | ||||
|             if (res == DownloadResult::NoMatchBuildId || res == DownloadResult::NoMatchTitleId) { | ||||
|                 FileUtil::Delete(path); | ||||
|             } | ||||
|  | ||||
|             HandleDownloadDisplayResult(res); | ||||
|             return std::nullopt; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     FileUtil::IOFile bin{path, "rb"}; | ||||
|     const auto size = bin.GetSize(); | ||||
|     std::vector<u8> bytes(size); | ||||
|     if (!bin.IsOpen() || size == 0 || bin.ReadBytes(bytes.data(), bytes.size()) != bytes.size()) { | ||||
|         LOG_ERROR(Service_BCAT, "Boxcat failed to read launch parameter binary at path '{}'!", | ||||
|                   path); | ||||
|         return std::nullopt; | ||||
|     } | ||||
|  | ||||
|     return bytes; | ||||
| } | ||||
|  | ||||
| Boxcat::StatusResult Boxcat::GetStatus(std::optional<std::string>& global, | ||||
|                                        std::map<std::string, EventStatus>& games) { | ||||
|     httplib::SSLClient client{BOXCAT_HOSTNAME, static_cast<int>(PORT), | ||||
|                               static_cast<int>(TIMEOUT_SECONDS)}; | ||||
|  | ||||
|     httplib::Headers headers{ | ||||
|         {std::string("Game-Assets-API-Version"), std::string(BOXCAT_API_VERSION)}, | ||||
|         {std::string("Boxcat-Client-Type"), std::string(BOXCAT_CLIENT_TYPE)}, | ||||
|     }; | ||||
|  | ||||
|     const auto response = client.Get(BOXCAT_PATHNAME_EVENTS, headers); | ||||
|     if (response == nullptr) | ||||
|         return StatusResult::Offline; | ||||
|  | ||||
|     if (response->status == static_cast<int>(ResponseStatus::BadClientVersion)) | ||||
|         return StatusResult::BadClientVersion; | ||||
|  | ||||
|     try { | ||||
|         nlohmann::json json = nlohmann::json::parse(response->body); | ||||
|  | ||||
|         if (!json["online"].get<bool>()) | ||||
|             return StatusResult::Offline; | ||||
|  | ||||
|         if (json["global"].is_null()) | ||||
|             global = std::nullopt; | ||||
|         else | ||||
|             global = json["global"].get<std::string>(); | ||||
|  | ||||
|         if (json["games"].is_array()) { | ||||
|             for (const auto object : json["games"]) { | ||||
|                 if (object.is_object() && object.find("name") != object.end()) { | ||||
|                     EventStatus detail{}; | ||||
|                     if (object["header"].is_string()) { | ||||
|                         detail.header = object["header"].get<std::string>(); | ||||
|                     } else { | ||||
|                         detail.header = std::nullopt; | ||||
|                     } | ||||
|  | ||||
|                     if (object["footer"].is_string()) { | ||||
|                         detail.footer = object["footer"].get<std::string>(); | ||||
|                     } else { | ||||
|                         detail.footer = std::nullopt; | ||||
|                     } | ||||
|  | ||||
|                     if (object["events"].is_array()) { | ||||
|                         for (const auto& event : object["events"]) { | ||||
|                             if (!event.is_string()) | ||||
|                                 continue; | ||||
|                             detail.events.push_back(event.get<std::string>()); | ||||
|                         } | ||||
|                     } | ||||
|  | ||||
|                     games.insert_or_assign(object["name"], std::move(detail)); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         return StatusResult::Success; | ||||
|     } catch (const nlohmann::json::parse_error& e) { | ||||
|         return StatusResult::ParseError; | ||||
|     } | ||||
| } | ||||
|  | ||||
| } // namespace Service::BCAT | ||||
							
								
								
									
										58
									
								
								src/core/hle/service/bcat/backend/boxcat.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								src/core/hle/service/bcat/backend/boxcat.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| // Copyright 2019 yuzu emulator team | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <atomic> | ||||
| #include <map> | ||||
| #include <optional> | ||||
| #include "core/hle/service/bcat/backend/backend.h" | ||||
|  | ||||
| namespace Service::BCAT { | ||||
|  | ||||
| struct EventStatus { | ||||
|     std::optional<std::string> header; | ||||
|     std::optional<std::string> footer; | ||||
|     std::vector<std::string> events; | ||||
| }; | ||||
|  | ||||
| /// Boxcat is yuzu's custom backend implementation of Nintendo's BCAT service. It is free to use and | ||||
| /// doesn't require a switch or nintendo account. The content is controlled by the yuzu team. | ||||
| class Boxcat final : public Backend { | ||||
|     friend void SynchronizeInternal(DirectoryGetter dir_getter, TitleIDVersion title, | ||||
|                                     ProgressServiceBackend& progress, | ||||
|                                     std::optional<std::string> dir_name); | ||||
|  | ||||
| public: | ||||
|     explicit Boxcat(DirectoryGetter getter); | ||||
|     ~Boxcat() override; | ||||
|  | ||||
|     bool Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) override; | ||||
|     bool SynchronizeDirectory(TitleIDVersion title, std::string name, | ||||
|                               ProgressServiceBackend& progress) override; | ||||
|  | ||||
|     bool Clear(u64 title_id) override; | ||||
|  | ||||
|     void SetPassphrase(u64 title_id, const Passphrase& passphrase) override; | ||||
|  | ||||
|     std::optional<std::vector<u8>> GetLaunchParameter(TitleIDVersion title) override; | ||||
|  | ||||
|     enum class StatusResult { | ||||
|         Success, | ||||
|         Offline, | ||||
|         ParseError, | ||||
|         BadClientVersion, | ||||
|     }; | ||||
|  | ||||
|     static StatusResult GetStatus(std::optional<std::string>& global, | ||||
|                                   std::map<std::string, EventStatus>& games); | ||||
|  | ||||
| private: | ||||
|     std::atomic_bool is_syncing{false}; | ||||
|  | ||||
|     class Client; | ||||
|     std::unique_ptr<Client> client; | ||||
| }; | ||||
|  | ||||
| } // namespace Service::BCAT | ||||
| @@ -6,11 +6,15 @@ | ||||
|  | ||||
| namespace Service::BCAT { | ||||
|  | ||||
| BCAT::BCAT(std::shared_ptr<Module> module, const char* name) | ||||
|     : Module::Interface(std::move(module), name) { | ||||
| BCAT::BCAT(std::shared_ptr<Module> module, FileSystem::FileSystemController& fsc, const char* name) | ||||
|     : Module::Interface(std::move(module), fsc, name) { | ||||
|     // clang-format off | ||||
|     static const FunctionInfo functions[] = { | ||||
|         {0, &BCAT::CreateBcatService, "CreateBcatService"}, | ||||
|         {1, &BCAT::CreateDeliveryCacheStorageService, "CreateDeliveryCacheStorageService"}, | ||||
|         {2, &BCAT::CreateDeliveryCacheStorageServiceWithApplicationId, "CreateDeliveryCacheStorageServiceWithApplicationId"}, | ||||
|     }; | ||||
|     // clang-format on | ||||
|     RegisterHandlers(functions); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -10,7 +10,8 @@ namespace Service::BCAT { | ||||
|  | ||||
| class BCAT final : public Module::Interface { | ||||
| public: | ||||
|     explicit BCAT(std::shared_ptr<Module> module, const char* name); | ||||
|     explicit BCAT(std::shared_ptr<Module> module, FileSystem::FileSystemController& fsc, | ||||
|                   const char* name); | ||||
|     ~BCAT() override; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -2,34 +2,254 @@ | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #include <cctype> | ||||
| #include <mbedtls/md5.h> | ||||
| #include "backend/boxcat.h" | ||||
| #include "common/hex_util.h" | ||||
| #include "common/logging/log.h" | ||||
| #include "common/string_util.h" | ||||
| #include "core/file_sys/vfs.h" | ||||
| #include "core/hle/ipc_helpers.h" | ||||
| #include "core/hle/kernel/process.h" | ||||
| #include "core/hle/kernel/readable_event.h" | ||||
| #include "core/hle/kernel/writable_event.h" | ||||
| #include "core/hle/service/bcat/backend/backend.h" | ||||
| #include "core/hle/service/bcat/bcat.h" | ||||
| #include "core/hle/service/bcat/module.h" | ||||
| #include "core/hle/service/filesystem/filesystem.h" | ||||
| #include "core/settings.h" | ||||
|  | ||||
| namespace Service::BCAT { | ||||
|  | ||||
| constexpr ResultCode ERROR_INVALID_ARGUMENT{ErrorModule::BCAT, 1}; | ||||
| constexpr ResultCode ERROR_FAILED_OPEN_ENTITY{ErrorModule::BCAT, 2}; | ||||
| constexpr ResultCode ERROR_ENTITY_ALREADY_OPEN{ErrorModule::BCAT, 6}; | ||||
| constexpr ResultCode ERROR_NO_OPEN_ENTITY{ErrorModule::BCAT, 7}; | ||||
|  | ||||
| // The command to clear the delivery cache just calls fs IFileSystem DeleteFile on all of the files | ||||
| // and if any of them have a non-zero result it just forwards that result. This is the FS error code | ||||
| // for permission denied, which is the closest approximation of this scenario. | ||||
| constexpr ResultCode ERROR_FAILED_CLEAR_CACHE{ErrorModule::FS, 6400}; | ||||
|  | ||||
| using BCATDigest = std::array<u8, 0x10>; | ||||
|  | ||||
| namespace { | ||||
|  | ||||
| u64 GetCurrentBuildID() { | ||||
|     const auto& id = Core::System::GetInstance().GetCurrentProcessBuildID(); | ||||
|     u64 out{}; | ||||
|     std::memcpy(&out, id.data(), sizeof(u64)); | ||||
|     return out; | ||||
| } | ||||
|  | ||||
| // The digest is only used to determine if a file is unique compared to others of the same name. | ||||
| // Since the algorithm isn't ever checked in game, MD5 is safe. | ||||
| BCATDigest DigestFile(const FileSys::VirtualFile& file) { | ||||
|     BCATDigest out{}; | ||||
|     const auto bytes = file->ReadAllBytes(); | ||||
|     mbedtls_md5(bytes.data(), bytes.size(), out.data()); | ||||
|     return out; | ||||
| } | ||||
|  | ||||
| // For a name to be valid it must be non-empty, must have a null terminating character as the final | ||||
| // char, can only contain numbers, letters, underscores and a hyphen if directory and a period if | ||||
| // file. | ||||
| bool VerifyNameValidInternal(Kernel::HLERequestContext& ctx, std::array<char, 0x20> name, | ||||
|                              char match_char) { | ||||
|     const auto null_chars = std::count(name.begin(), name.end(), 0); | ||||
|     const auto bad_chars = std::count_if(name.begin(), name.end(), [match_char](char c) { | ||||
|         return !std::isalnum(static_cast<u8>(c)) && c != '_' && c != match_char && c != '\0'; | ||||
|     }); | ||||
|     if (null_chars == 0x20 || null_chars == 0 || bad_chars != 0 || name[0x1F] != '\0') { | ||||
|         LOG_ERROR(Service_BCAT, "Name passed was invalid!"); | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ERROR_INVALID_ARGUMENT); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| bool VerifyNameValidDir(Kernel::HLERequestContext& ctx, DirectoryName name) { | ||||
|     return VerifyNameValidInternal(ctx, name, '-'); | ||||
| } | ||||
|  | ||||
| bool VerifyNameValidFile(Kernel::HLERequestContext& ctx, FileName name) { | ||||
|     return VerifyNameValidInternal(ctx, name, '.'); | ||||
| } | ||||
|  | ||||
| } // Anonymous namespace | ||||
|  | ||||
| struct DeliveryCacheDirectoryEntry { | ||||
|     FileName name; | ||||
|     u64 size; | ||||
|     BCATDigest digest; | ||||
| }; | ||||
|  | ||||
| class IDeliveryCacheProgressService final : public ServiceFramework<IDeliveryCacheProgressService> { | ||||
| public: | ||||
|     IDeliveryCacheProgressService(Kernel::SharedPtr<Kernel::ReadableEvent> event, | ||||
|                                   const DeliveryCacheProgressImpl& impl) | ||||
|         : ServiceFramework{"IDeliveryCacheProgressService"}, event(std::move(event)), impl(impl) { | ||||
|         // clang-format off | ||||
|         static const FunctionInfo functions[] = { | ||||
|             {0, &IDeliveryCacheProgressService::GetEvent, "GetEvent"}, | ||||
|             {1, &IDeliveryCacheProgressService::GetImpl, "GetImpl"}, | ||||
|         }; | ||||
|         // clang-format on | ||||
|  | ||||
|         RegisterHandlers(functions); | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     void GetEvent(Kernel::HLERequestContext& ctx) { | ||||
|         LOG_DEBUG(Service_BCAT, "called"); | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 2, 1}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|         rb.PushCopyObjects(event); | ||||
|     } | ||||
|  | ||||
|     void GetImpl(Kernel::HLERequestContext& ctx) { | ||||
|         LOG_DEBUG(Service_BCAT, "called"); | ||||
|  | ||||
|         ctx.WriteBuffer(&impl, sizeof(DeliveryCacheProgressImpl)); | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|     } | ||||
|  | ||||
|     Kernel::SharedPtr<Kernel::ReadableEvent> event; | ||||
|     const DeliveryCacheProgressImpl& impl; | ||||
| }; | ||||
|  | ||||
| class IBcatService final : public ServiceFramework<IBcatService> { | ||||
| public: | ||||
|     IBcatService() : ServiceFramework("IBcatService") { | ||||
|     IBcatService(Backend& backend) : ServiceFramework("IBcatService"), backend(backend) { | ||||
|         // clang-format off | ||||
|         static const FunctionInfo functions[] = { | ||||
|             {10100, nullptr, "RequestSyncDeliveryCache"}, | ||||
|             {10101, nullptr, "RequestSyncDeliveryCacheWithDirectoryName"}, | ||||
|             {10100, &IBcatService::RequestSyncDeliveryCache, "RequestSyncDeliveryCache"}, | ||||
|             {10101, &IBcatService::RequestSyncDeliveryCacheWithDirectoryName, "RequestSyncDeliveryCacheWithDirectoryName"}, | ||||
|             {10200, nullptr, "CancelSyncDeliveryCacheRequest"}, | ||||
|             {20100, nullptr, "RequestSyncDeliveryCacheWithApplicationId"}, | ||||
|             {20101, nullptr, "RequestSyncDeliveryCacheWithApplicationIdAndDirectoryName"}, | ||||
|             {30100, nullptr, "SetPassphrase"}, | ||||
|             {30100, &IBcatService::SetPassphrase, "SetPassphrase"}, | ||||
|             {30200, nullptr, "RegisterBackgroundDeliveryTask"}, | ||||
|             {30201, nullptr, "UnregisterBackgroundDeliveryTask"}, | ||||
|             {30202, nullptr, "BlockDeliveryTask"}, | ||||
|             {30203, nullptr, "UnblockDeliveryTask"}, | ||||
|             {90100, nullptr, "EnumerateBackgroundDeliveryTask"}, | ||||
|             {90200, nullptr, "GetDeliveryList"}, | ||||
|             {90201, nullptr, "ClearDeliveryCacheStorage"}, | ||||
|             {90201, &IBcatService::ClearDeliveryCacheStorage, "ClearDeliveryCacheStorage"}, | ||||
|             {90300, nullptr, "GetPushNotificationLog"}, | ||||
|         }; | ||||
|         // clang-format on | ||||
|         RegisterHandlers(functions); | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     enum class SyncType { | ||||
|         Normal, | ||||
|         Directory, | ||||
|         Count, | ||||
|     }; | ||||
|  | ||||
|     std::shared_ptr<IDeliveryCacheProgressService> CreateProgressService(SyncType type) { | ||||
|         auto& backend{progress.at(static_cast<std::size_t>(type))}; | ||||
|         return std::make_shared<IDeliveryCacheProgressService>(backend.GetEvent(), | ||||
|                                                                backend.GetImpl()); | ||||
|     } | ||||
|  | ||||
|     void RequestSyncDeliveryCache(Kernel::HLERequestContext& ctx) { | ||||
|         LOG_DEBUG(Service_BCAT, "called"); | ||||
|  | ||||
|         backend.Synchronize({Core::CurrentProcess()->GetTitleID(), GetCurrentBuildID()}, | ||||
|                             progress.at(static_cast<std::size_t>(SyncType::Normal))); | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 2, 0, 1}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|         rb.PushIpcInterface(CreateProgressService(SyncType::Normal)); | ||||
|     } | ||||
|  | ||||
|     void RequestSyncDeliveryCacheWithDirectoryName(Kernel::HLERequestContext& ctx) { | ||||
|         IPC::RequestParser rp{ctx}; | ||||
|         const auto name_raw = rp.PopRaw<DirectoryName>(); | ||||
|         const auto name = | ||||
|             Common::StringFromFixedZeroTerminatedBuffer(name_raw.data(), name_raw.size()); | ||||
|  | ||||
|         LOG_DEBUG(Service_BCAT, "called, name={}", name); | ||||
|  | ||||
|         backend.SynchronizeDirectory({Core::CurrentProcess()->GetTitleID(), GetCurrentBuildID()}, | ||||
|                                      name, | ||||
|                                      progress.at(static_cast<std::size_t>(SyncType::Directory))); | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 2, 0, 1}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|         rb.PushIpcInterface(CreateProgressService(SyncType::Directory)); | ||||
|     } | ||||
|  | ||||
|     void SetPassphrase(Kernel::HLERequestContext& ctx) { | ||||
|         IPC::RequestParser rp{ctx}; | ||||
|         const auto title_id = rp.PopRaw<u64>(); | ||||
|  | ||||
|         const auto passphrase_raw = ctx.ReadBuffer(); | ||||
|  | ||||
|         LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, passphrase={}", title_id, | ||||
|                   Common::HexToString(passphrase_raw)); | ||||
|  | ||||
|         if (title_id == 0) { | ||||
|             LOG_ERROR(Service_BCAT, "Invalid title ID!"); | ||||
|             IPC::ResponseBuilder rb{ctx, 2}; | ||||
|             rb.Push(ERROR_INVALID_ARGUMENT); | ||||
|         } | ||||
|  | ||||
|         if (passphrase_raw.size() > 0x40) { | ||||
|             LOG_ERROR(Service_BCAT, "Passphrase too large!"); | ||||
|             IPC::ResponseBuilder rb{ctx, 2}; | ||||
|             rb.Push(ERROR_INVALID_ARGUMENT); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         Passphrase passphrase{}; | ||||
|         std::memcpy(passphrase.data(), passphrase_raw.data(), | ||||
|                     std::min(passphrase.size(), passphrase_raw.size())); | ||||
|  | ||||
|         backend.SetPassphrase(title_id, passphrase); | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|     } | ||||
|  | ||||
|     void ClearDeliveryCacheStorage(Kernel::HLERequestContext& ctx) { | ||||
|         IPC::RequestParser rp{ctx}; | ||||
|         const auto title_id = rp.PopRaw<u64>(); | ||||
|  | ||||
|         LOG_DEBUG(Service_BCAT, "called, title_id={:016X}", title_id); | ||||
|  | ||||
|         if (title_id == 0) { | ||||
|             LOG_ERROR(Service_BCAT, "Invalid title ID!"); | ||||
|             IPC::ResponseBuilder rb{ctx, 2}; | ||||
|             rb.Push(ERROR_INVALID_ARGUMENT); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         if (!backend.Clear(title_id)) { | ||||
|             LOG_ERROR(Service_BCAT, "Could not clear the directory successfully!"); | ||||
|             IPC::ResponseBuilder rb{ctx, 2}; | ||||
|             rb.Push(ERROR_FAILED_CLEAR_CACHE); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|     } | ||||
|  | ||||
|     Backend& backend; | ||||
|  | ||||
|     std::array<ProgressServiceBackend, static_cast<std::size_t>(SyncType::Count)> progress{ | ||||
|         ProgressServiceBackend{"Normal"}, | ||||
|         ProgressServiceBackend{"Directory"}, | ||||
|     }; | ||||
| }; | ||||
|  | ||||
| void Module::Interface::CreateBcatService(Kernel::HLERequestContext& ctx) { | ||||
| @@ -37,20 +257,331 @@ void Module::Interface::CreateBcatService(Kernel::HLERequestContext& ctx) { | ||||
|  | ||||
|     IPC::ResponseBuilder rb{ctx, 2, 0, 1}; | ||||
|     rb.Push(RESULT_SUCCESS); | ||||
|     rb.PushIpcInterface<IBcatService>(); | ||||
|     rb.PushIpcInterface<IBcatService>(*backend); | ||||
| } | ||||
|  | ||||
| Module::Interface::Interface(std::shared_ptr<Module> module, const char* name) | ||||
|     : ServiceFramework(name), module(std::move(module)) {} | ||||
| class IDeliveryCacheFileService final : public ServiceFramework<IDeliveryCacheFileService> { | ||||
| public: | ||||
|     IDeliveryCacheFileService(FileSys::VirtualDir root_) | ||||
|         : ServiceFramework{"IDeliveryCacheFileService"}, root(std::move(root_)) { | ||||
|         // clang-format off | ||||
|         static const FunctionInfo functions[] = { | ||||
|             {0, &IDeliveryCacheFileService::Open, "Open"}, | ||||
|             {1, &IDeliveryCacheFileService::Read, "Read"}, | ||||
|             {2, &IDeliveryCacheFileService::GetSize, "GetSize"}, | ||||
|             {3, &IDeliveryCacheFileService::GetDigest, "GetDigest"}, | ||||
|         }; | ||||
|         // clang-format on | ||||
|  | ||||
|         RegisterHandlers(functions); | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     void Open(Kernel::HLERequestContext& ctx) { | ||||
|         IPC::RequestParser rp{ctx}; | ||||
|         const auto dir_name_raw = rp.PopRaw<DirectoryName>(); | ||||
|         const auto file_name_raw = rp.PopRaw<FileName>(); | ||||
|  | ||||
|         const auto dir_name = | ||||
|             Common::StringFromFixedZeroTerminatedBuffer(dir_name_raw.data(), dir_name_raw.size()); | ||||
|         const auto file_name = | ||||
|             Common::StringFromFixedZeroTerminatedBuffer(file_name_raw.data(), file_name_raw.size()); | ||||
|  | ||||
|         LOG_DEBUG(Service_BCAT, "called, dir_name={}, file_name={}", dir_name, file_name); | ||||
|  | ||||
|         if (!VerifyNameValidDir(ctx, dir_name_raw) || !VerifyNameValidFile(ctx, file_name_raw)) | ||||
|             return; | ||||
|  | ||||
|         if (current_file != nullptr) { | ||||
|             LOG_ERROR(Service_BCAT, "A file has already been opened on this interface!"); | ||||
|             IPC::ResponseBuilder rb{ctx, 2}; | ||||
|             rb.Push(ERROR_ENTITY_ALREADY_OPEN); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const auto dir = root->GetSubdirectory(dir_name); | ||||
|  | ||||
|         if (dir == nullptr) { | ||||
|             LOG_ERROR(Service_BCAT, "The directory of name={} couldn't be opened!", dir_name); | ||||
|             IPC::ResponseBuilder rb{ctx, 2}; | ||||
|             rb.Push(ERROR_FAILED_OPEN_ENTITY); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         current_file = dir->GetFile(file_name); | ||||
|  | ||||
|         if (current_file == nullptr) { | ||||
|             LOG_ERROR(Service_BCAT, "The file of name={} couldn't be opened!", file_name); | ||||
|             IPC::ResponseBuilder rb{ctx, 2}; | ||||
|             rb.Push(ERROR_FAILED_OPEN_ENTITY); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|     } | ||||
|  | ||||
|     void Read(Kernel::HLERequestContext& ctx) { | ||||
|         IPC::RequestParser rp{ctx}; | ||||
|         const auto offset{rp.PopRaw<u64>()}; | ||||
|  | ||||
|         auto size = ctx.GetWriteBufferSize(); | ||||
|  | ||||
|         LOG_DEBUG(Service_BCAT, "called, offset={:016X}, size={:016X}", offset, size); | ||||
|  | ||||
|         if (current_file == nullptr) { | ||||
|             LOG_ERROR(Service_BCAT, "There is no file currently open!"); | ||||
|             IPC::ResponseBuilder rb{ctx, 2}; | ||||
|             rb.Push(ERROR_NO_OPEN_ENTITY); | ||||
|         } | ||||
|  | ||||
|         size = std::min<u64>(current_file->GetSize() - offset, size); | ||||
|         const auto buffer = current_file->ReadBytes(size, offset); | ||||
|         ctx.WriteBuffer(buffer); | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 4}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|         rb.Push<u64>(buffer.size()); | ||||
|     } | ||||
|  | ||||
|     void GetSize(Kernel::HLERequestContext& ctx) { | ||||
|         LOG_DEBUG(Service_BCAT, "called"); | ||||
|  | ||||
|         if (current_file == nullptr) { | ||||
|             LOG_ERROR(Service_BCAT, "There is no file currently open!"); | ||||
|             IPC::ResponseBuilder rb{ctx, 2}; | ||||
|             rb.Push(ERROR_NO_OPEN_ENTITY); | ||||
|         } | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 4}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|         rb.Push<u64>(current_file->GetSize()); | ||||
|     } | ||||
|  | ||||
|     void GetDigest(Kernel::HLERequestContext& ctx) { | ||||
|         LOG_DEBUG(Service_BCAT, "called"); | ||||
|  | ||||
|         if (current_file == nullptr) { | ||||
|             LOG_ERROR(Service_BCAT, "There is no file currently open!"); | ||||
|             IPC::ResponseBuilder rb{ctx, 2}; | ||||
|             rb.Push(ERROR_NO_OPEN_ENTITY); | ||||
|         } | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 6}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|         rb.PushRaw(DigestFile(current_file)); | ||||
|     } | ||||
|  | ||||
|     FileSys::VirtualDir root; | ||||
|     FileSys::VirtualFile current_file; | ||||
| }; | ||||
|  | ||||
| class IDeliveryCacheDirectoryService final | ||||
|     : public ServiceFramework<IDeliveryCacheDirectoryService> { | ||||
| public: | ||||
|     IDeliveryCacheDirectoryService(FileSys::VirtualDir root_) | ||||
|         : ServiceFramework{"IDeliveryCacheDirectoryService"}, root(std::move(root_)) { | ||||
|         // clang-format off | ||||
|         static const FunctionInfo functions[] = { | ||||
|             {0, &IDeliveryCacheDirectoryService::Open, "Open"}, | ||||
|             {1, &IDeliveryCacheDirectoryService::Read, "Read"}, | ||||
|             {2, &IDeliveryCacheDirectoryService::GetCount, "GetCount"}, | ||||
|         }; | ||||
|         // clang-format on | ||||
|  | ||||
|         RegisterHandlers(functions); | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     void Open(Kernel::HLERequestContext& ctx) { | ||||
|         IPC::RequestParser rp{ctx}; | ||||
|         const auto name_raw = rp.PopRaw<DirectoryName>(); | ||||
|         const auto name = | ||||
|             Common::StringFromFixedZeroTerminatedBuffer(name_raw.data(), name_raw.size()); | ||||
|  | ||||
|         LOG_DEBUG(Service_BCAT, "called, name={}", name); | ||||
|  | ||||
|         if (!VerifyNameValidDir(ctx, name_raw)) | ||||
|             return; | ||||
|  | ||||
|         if (current_dir != nullptr) { | ||||
|             LOG_ERROR(Service_BCAT, "A file has already been opened on this interface!"); | ||||
|             IPC::ResponseBuilder rb{ctx, 2}; | ||||
|             rb.Push(ERROR_ENTITY_ALREADY_OPEN); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         current_dir = root->GetSubdirectory(name); | ||||
|  | ||||
|         if (current_dir == nullptr) { | ||||
|             LOG_ERROR(Service_BCAT, "Failed to open the directory name={}!", name); | ||||
|             IPC::ResponseBuilder rb{ctx, 2}; | ||||
|             rb.Push(ERROR_FAILED_OPEN_ENTITY); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|     } | ||||
|  | ||||
|     void Read(Kernel::HLERequestContext& ctx) { | ||||
|         auto write_size = ctx.GetWriteBufferSize() / sizeof(DeliveryCacheDirectoryEntry); | ||||
|  | ||||
|         LOG_DEBUG(Service_BCAT, "called, write_size={:016X}", write_size); | ||||
|  | ||||
|         if (current_dir == nullptr) { | ||||
|             LOG_ERROR(Service_BCAT, "There is no open directory!"); | ||||
|             IPC::ResponseBuilder rb{ctx, 2}; | ||||
|             rb.Push(ERROR_NO_OPEN_ENTITY); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const auto files = current_dir->GetFiles(); | ||||
|         write_size = std::min<u64>(write_size, files.size()); | ||||
|         std::vector<DeliveryCacheDirectoryEntry> entries(write_size); | ||||
|         std::transform( | ||||
|             files.begin(), files.begin() + write_size, entries.begin(), [](const auto& file) { | ||||
|                 FileName name{}; | ||||
|                 std::memcpy(name.data(), file->GetName().data(), | ||||
|                             std::min(file->GetName().size(), name.size())); | ||||
|                 return DeliveryCacheDirectoryEntry{name, file->GetSize(), DigestFile(file)}; | ||||
|             }); | ||||
|  | ||||
|         ctx.WriteBuffer(entries); | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 3}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|         rb.Push<u32>(write_size * sizeof(DeliveryCacheDirectoryEntry)); | ||||
|     } | ||||
|  | ||||
|     void GetCount(Kernel::HLERequestContext& ctx) { | ||||
|         LOG_DEBUG(Service_BCAT, "called"); | ||||
|  | ||||
|         if (current_dir == nullptr) { | ||||
|             LOG_ERROR(Service_BCAT, "There is no open directory!"); | ||||
|             IPC::ResponseBuilder rb{ctx, 2}; | ||||
|             rb.Push(ERROR_NO_OPEN_ENTITY); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         const auto files = current_dir->GetFiles(); | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 3}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|         rb.Push<u32>(files.size()); | ||||
|     } | ||||
|  | ||||
|     FileSys::VirtualDir root; | ||||
|     FileSys::VirtualDir current_dir; | ||||
| }; | ||||
|  | ||||
| class IDeliveryCacheStorageService final : public ServiceFramework<IDeliveryCacheStorageService> { | ||||
| public: | ||||
|     IDeliveryCacheStorageService(FileSys::VirtualDir root_) | ||||
|         : ServiceFramework{"IDeliveryCacheStorageService"}, root(std::move(root_)) { | ||||
|         // clang-format off | ||||
|         static const FunctionInfo functions[] = { | ||||
|             {0, &IDeliveryCacheStorageService::CreateFileService, "CreateFileService"}, | ||||
|             {1, &IDeliveryCacheStorageService::CreateDirectoryService, "CreateDirectoryService"}, | ||||
|             {10, &IDeliveryCacheStorageService::EnumerateDeliveryCacheDirectory, "EnumerateDeliveryCacheDirectory"}, | ||||
|         }; | ||||
|         // clang-format on | ||||
|  | ||||
|         RegisterHandlers(functions); | ||||
|  | ||||
|         for (const auto& subdir : root->GetSubdirectories()) { | ||||
|             DirectoryName name{}; | ||||
|             std::memcpy(name.data(), subdir->GetName().data(), | ||||
|                         std::min(sizeof(DirectoryName) - 1, subdir->GetName().size())); | ||||
|             entries.push_back(name); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| private: | ||||
|     void CreateFileService(Kernel::HLERequestContext& ctx) { | ||||
|         LOG_DEBUG(Service_BCAT, "called"); | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 2, 0, 1}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|         rb.PushIpcInterface<IDeliveryCacheFileService>(root); | ||||
|     } | ||||
|  | ||||
|     void CreateDirectoryService(Kernel::HLERequestContext& ctx) { | ||||
|         LOG_DEBUG(Service_BCAT, "called"); | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 2, 0, 1}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|         rb.PushIpcInterface<IDeliveryCacheDirectoryService>(root); | ||||
|     } | ||||
|  | ||||
|     void EnumerateDeliveryCacheDirectory(Kernel::HLERequestContext& ctx) { | ||||
|         auto size = ctx.GetWriteBufferSize() / sizeof(DirectoryName); | ||||
|  | ||||
|         LOG_DEBUG(Service_BCAT, "called, size={:016X}", size); | ||||
|  | ||||
|         size = std::min<u64>(size, entries.size() - next_read_index); | ||||
|         ctx.WriteBuffer(entries.data() + next_read_index, size * sizeof(DirectoryName)); | ||||
|         next_read_index += size; | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 3}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|         rb.Push<u32>(size); | ||||
|     } | ||||
|  | ||||
|     FileSys::VirtualDir root; | ||||
|     std::vector<DirectoryName> entries; | ||||
|     u64 next_read_index = 0; | ||||
| }; | ||||
|  | ||||
| void Module::Interface::CreateDeliveryCacheStorageService(Kernel::HLERequestContext& ctx) { | ||||
|     LOG_DEBUG(Service_BCAT, "called"); | ||||
|  | ||||
|     IPC::ResponseBuilder rb{ctx, 2, 0, 1}; | ||||
|     rb.Push(RESULT_SUCCESS); | ||||
|     rb.PushIpcInterface<IDeliveryCacheStorageService>( | ||||
|         fsc.GetBCATDirectory(Core::CurrentProcess()->GetTitleID())); | ||||
| } | ||||
|  | ||||
| void Module::Interface::CreateDeliveryCacheStorageServiceWithApplicationId( | ||||
|     Kernel::HLERequestContext& ctx) { | ||||
|     IPC::RequestParser rp{ctx}; | ||||
|     const auto title_id = rp.PopRaw<u64>(); | ||||
|  | ||||
|     LOG_DEBUG(Service_BCAT, "called, title_id={:016X}", title_id); | ||||
|  | ||||
|     IPC::ResponseBuilder rb{ctx, 2, 0, 1}; | ||||
|     rb.Push(RESULT_SUCCESS); | ||||
|     rb.PushIpcInterface<IDeliveryCacheStorageService>(fsc.GetBCATDirectory(title_id)); | ||||
| } | ||||
|  | ||||
| std::unique_ptr<Backend> CreateBackendFromSettings(DirectoryGetter getter) { | ||||
|     const auto backend = Settings::values.bcat_backend; | ||||
|  | ||||
| #ifdef YUZU_ENABLE_BOXCAT | ||||
|     if (backend == "boxcat") | ||||
|         return std::make_unique<Boxcat>(std::move(getter)); | ||||
| #endif | ||||
|  | ||||
|     return std::make_unique<NullBackend>(std::move(getter)); | ||||
| } | ||||
|  | ||||
| Module::Interface::Interface(std::shared_ptr<Module> module, FileSystem::FileSystemController& fsc, | ||||
|                              const char* name) | ||||
|     : ServiceFramework(name), module(std::move(module)), fsc(fsc), | ||||
|       backend(CreateBackendFromSettings([&fsc](u64 tid) { return fsc.GetBCATDirectory(tid); })) {} | ||||
|  | ||||
| Module::Interface::~Interface() = default; | ||||
|  | ||||
| void InstallInterfaces(SM::ServiceManager& service_manager) { | ||||
| void InstallInterfaces(Core::System& system) { | ||||
|     auto module = std::make_shared<Module>(); | ||||
|     std::make_shared<BCAT>(module, "bcat:a")->InstallAsService(service_manager); | ||||
|     std::make_shared<BCAT>(module, "bcat:m")->InstallAsService(service_manager); | ||||
|     std::make_shared<BCAT>(module, "bcat:u")->InstallAsService(service_manager); | ||||
|     std::make_shared<BCAT>(module, "bcat:s")->InstallAsService(service_manager); | ||||
|     std::make_shared<BCAT>(module, system.GetFileSystemController(), "bcat:a") | ||||
|         ->InstallAsService(system.ServiceManager()); | ||||
|     std::make_shared<BCAT>(module, system.GetFileSystemController(), "bcat:m") | ||||
|         ->InstallAsService(system.ServiceManager()); | ||||
|     std::make_shared<BCAT>(module, system.GetFileSystemController(), "bcat:u") | ||||
|         ->InstallAsService(system.ServiceManager()); | ||||
|     std::make_shared<BCAT>(module, system.GetFileSystemController(), "bcat:s") | ||||
|         ->InstallAsService(system.ServiceManager()); | ||||
| } | ||||
|  | ||||
| } // namespace Service::BCAT | ||||
|   | ||||
| @@ -6,23 +6,39 @@ | ||||
|  | ||||
| #include "core/hle/service/service.h" | ||||
|  | ||||
| namespace Service::BCAT { | ||||
| namespace Service { | ||||
|  | ||||
| namespace FileSystem { | ||||
| class FileSystemController; | ||||
| } // namespace FileSystem | ||||
|  | ||||
| namespace BCAT { | ||||
|  | ||||
| class Backend; | ||||
|  | ||||
| class Module final { | ||||
| public: | ||||
|     class Interface : public ServiceFramework<Interface> { | ||||
|     public: | ||||
|         explicit Interface(std::shared_ptr<Module> module, const char* name); | ||||
|         explicit Interface(std::shared_ptr<Module> module, FileSystem::FileSystemController& fsc, | ||||
|                            const char* name); | ||||
|         ~Interface() override; | ||||
|  | ||||
|         void CreateBcatService(Kernel::HLERequestContext& ctx); | ||||
|         void CreateDeliveryCacheStorageService(Kernel::HLERequestContext& ctx); | ||||
|         void CreateDeliveryCacheStorageServiceWithApplicationId(Kernel::HLERequestContext& ctx); | ||||
|  | ||||
|     protected: | ||||
|         FileSystem::FileSystemController& fsc; | ||||
|  | ||||
|         std::shared_ptr<Module> module; | ||||
|         std::unique_ptr<Backend> backend; | ||||
|     }; | ||||
| }; | ||||
|  | ||||
| /// Registers all BCAT services with the specified service manager. | ||||
| void InstallInterfaces(SM::ServiceManager& service_manager); | ||||
| void InstallInterfaces(Core::System& system); | ||||
|  | ||||
| } // namespace Service::BCAT | ||||
| } // namespace BCAT | ||||
|  | ||||
| } // namespace Service | ||||
|   | ||||
| @@ -674,6 +674,15 @@ FileSys::VirtualDir FileSystemController::GetModificationDumpRoot(u64 title_id) | ||||
|     return bis_factory->GetModificationDumpRoot(title_id); | ||||
| } | ||||
|  | ||||
| FileSys::VirtualDir FileSystemController::GetBCATDirectory(u64 title_id) const { | ||||
|     LOG_TRACE(Service_FS, "Opening BCAT root for tid={:016X}", title_id); | ||||
|  | ||||
|     if (bis_factory == nullptr) | ||||
|         return nullptr; | ||||
|  | ||||
|     return bis_factory->GetBCATDirectory(title_id); | ||||
| } | ||||
|  | ||||
| void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite) { | ||||
|     if (overwrite) { | ||||
|         bis_factory = nullptr; | ||||
|   | ||||
| @@ -110,6 +110,8 @@ public: | ||||
|     FileSys::VirtualDir GetModificationLoadRoot(u64 title_id) const; | ||||
|     FileSys::VirtualDir GetModificationDumpRoot(u64 title_id) const; | ||||
|  | ||||
|     FileSys::VirtualDir GetBCATDirectory(u64 title_id) const; | ||||
|  | ||||
|     // Creates the SaveData, SDMC, and BIS Factories. Should be called once and before any function | ||||
|     // above is called. | ||||
|     void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite = true); | ||||
|   | ||||
| @@ -12,6 +12,13 @@ | ||||
|  | ||||
| namespace Service::NIFM { | ||||
|  | ||||
| enum class RequestState : u32 { | ||||
|     NotSubmitted = 1, | ||||
|     Error = 1, ///< The duplicate 1 is intentional; it means both not submitted and error on HW. | ||||
|     Pending = 2, | ||||
|     Connected = 3, | ||||
| }; | ||||
|  | ||||
| class IScanRequest final : public ServiceFramework<IScanRequest> { | ||||
| public: | ||||
|     explicit IScanRequest() : ServiceFramework("IScanRequest") { | ||||
| @@ -81,7 +88,7 @@ private: | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 3}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|         rb.Push<u32>(0); | ||||
|         rb.PushEnum(RequestState::Connected); | ||||
|     } | ||||
|  | ||||
|     void GetResult(Kernel::HLERequestContext& ctx) { | ||||
| @@ -189,14 +196,14 @@ private: | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 3}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|         rb.Push<u8>(0); | ||||
|         rb.Push<u8>(1); | ||||
|     } | ||||
|     void IsAnyInternetRequestAccepted(Kernel::HLERequestContext& ctx) { | ||||
|         LOG_WARNING(Service_NIFM, "(STUBBED) called"); | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 3}; | ||||
|         rb.Push(RESULT_SUCCESS); | ||||
|         rb.Push<u8>(0); | ||||
|         rb.Push<u8>(1); | ||||
|     } | ||||
|     Core::System& system; | ||||
| }; | ||||
|   | ||||
| @@ -208,7 +208,7 @@ void Init(std::shared_ptr<SM::ServiceManager>& sm, Core::System& system) { | ||||
|     AOC::InstallInterfaces(*sm, system); | ||||
|     APM::InstallInterfaces(system); | ||||
|     Audio::InstallInterfaces(*sm, system); | ||||
|     BCAT::InstallInterfaces(*sm); | ||||
|     BCAT::InstallInterfaces(system); | ||||
|     BPC::InstallInterfaces(*sm); | ||||
|     BtDrv::InstallInterfaces(*sm, system); | ||||
|     BTM::InstallInterfaces(*sm, system); | ||||
|   | ||||
| @@ -150,6 +150,7 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::Process& process, | ||||
|     // Apply cheats if they exist and the program has a valid title ID | ||||
|     if (pm) { | ||||
|         auto& system = Core::System::GetInstance(); | ||||
|         system.SetCurrentProcessBuildID(nso_header.build_id); | ||||
|         const auto cheats = pm->CreateCheatList(system, nso_header.build_id); | ||||
|         if (!cheats.empty()) { | ||||
|             system.RegisterCheatList(cheats, nso_header.build_id, load_base, image_size); | ||||
|   | ||||
| @@ -103,6 +103,8 @@ void LogSettings() { | ||||
|     LogSetting("Debugging_UseGdbstub", Settings::values.use_gdbstub); | ||||
|     LogSetting("Debugging_GdbstubPort", Settings::values.gdbstub_port); | ||||
|     LogSetting("Debugging_ProgramArgs", Settings::values.program_args); | ||||
|     LogSetting("Services_BCATBackend", Settings::values.bcat_backend); | ||||
|     LogSetting("Services_BCATBoxcatLocal", Settings::values.bcat_boxcat_local); | ||||
| } | ||||
|  | ||||
| } // namespace Settings | ||||
|   | ||||
| @@ -448,6 +448,10 @@ struct Values { | ||||
|     bool reporting_services; | ||||
|     bool quest_flag; | ||||
|  | ||||
|     // BCAT | ||||
|     std::string bcat_backend; | ||||
|     bool bcat_boxcat_local; | ||||
|  | ||||
|     // WebService | ||||
|     bool enable_telemetry; | ||||
|     std::string web_api_url; | ||||
|   | ||||
| @@ -66,6 +66,9 @@ add_executable(yuzu | ||||
|     configuration/configure_profile_manager.cpp | ||||
|     configuration/configure_profile_manager.h | ||||
|     configuration/configure_profile_manager.ui | ||||
|     configuration/configure_service.cpp | ||||
|     configuration/configure_service.h | ||||
|     configuration/configure_service.ui | ||||
|     configuration/configure_system.cpp | ||||
|     configuration/configure_system.h | ||||
|     configuration/configure_system.ui | ||||
| @@ -186,6 +189,10 @@ if (YUZU_USE_QT_WEB_ENGINE) | ||||
|     target_compile_definitions(yuzu PRIVATE -DYUZU_USE_QT_WEB_ENGINE) | ||||
| endif () | ||||
|  | ||||
| if (YUZU_ENABLE_BOXCAT) | ||||
|     target_compile_definitions(yuzu PRIVATE -DYUZU_ENABLE_BOXCAT) | ||||
| endif () | ||||
|  | ||||
| if(UNIX AND NOT APPLE) | ||||
|     install(TARGETS yuzu RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") | ||||
| endif() | ||||
|   | ||||
| @@ -525,6 +525,17 @@ void Config::ReadDebuggingValues() { | ||||
|     qt_config->endGroup(); | ||||
| } | ||||
|  | ||||
| void Config::ReadServiceValues() { | ||||
|     qt_config->beginGroup(QStringLiteral("Services")); | ||||
|     Settings::values.bcat_backend = | ||||
|         ReadSetting(QStringLiteral("bcat_backend"), QStringLiteral("boxcat")) | ||||
|             .toString() | ||||
|             .toStdString(); | ||||
|     Settings::values.bcat_boxcat_local = | ||||
|         ReadSetting(QStringLiteral("bcat_boxcat_local"), false).toBool(); | ||||
|     qt_config->endGroup(); | ||||
| } | ||||
|  | ||||
| void Config::ReadDisabledAddOnValues() { | ||||
|     const auto size = qt_config->beginReadArray(QStringLiteral("DisabledAddOns")); | ||||
|  | ||||
| @@ -769,6 +780,7 @@ void Config::ReadValues() { | ||||
|     ReadMiscellaneousValues(); | ||||
|     ReadDebuggingValues(); | ||||
|     ReadWebServiceValues(); | ||||
|     ReadServiceValues(); | ||||
|     ReadDisabledAddOnValues(); | ||||
|     ReadUIValues(); | ||||
| } | ||||
| @@ -866,6 +878,7 @@ void Config::SaveValues() { | ||||
|     SaveMiscellaneousValues(); | ||||
|     SaveDebuggingValues(); | ||||
|     SaveWebServiceValues(); | ||||
|     SaveServiceValues(); | ||||
|     SaveDisabledAddOnValues(); | ||||
|     SaveUIValues(); | ||||
| } | ||||
| @@ -963,6 +976,14 @@ void Config::SaveDebuggingValues() { | ||||
|     qt_config->endGroup(); | ||||
| } | ||||
|  | ||||
| void Config::SaveServiceValues() { | ||||
|     qt_config->beginGroup(QStringLiteral("Services")); | ||||
|     WriteSetting(QStringLiteral("bcat_backend"), | ||||
|                  QString::fromStdString(Settings::values.bcat_backend), QStringLiteral("null")); | ||||
|     WriteSetting(QStringLiteral("bcat_boxcat_local"), Settings::values.bcat_boxcat_local, false); | ||||
|     qt_config->endGroup(); | ||||
| } | ||||
|  | ||||
| void Config::SaveDisabledAddOnValues() { | ||||
|     qt_config->beginWriteArray(QStringLiteral("DisabledAddOns")); | ||||
|  | ||||
|   | ||||
| @@ -42,6 +42,7 @@ private: | ||||
|     void ReadCoreValues(); | ||||
|     void ReadDataStorageValues(); | ||||
|     void ReadDebuggingValues(); | ||||
|     void ReadServiceValues(); | ||||
|     void ReadDisabledAddOnValues(); | ||||
|     void ReadMiscellaneousValues(); | ||||
|     void ReadPathValues(); | ||||
| @@ -65,6 +66,7 @@ private: | ||||
|     void SaveCoreValues(); | ||||
|     void SaveDataStorageValues(); | ||||
|     void SaveDebuggingValues(); | ||||
|     void SaveServiceValues(); | ||||
|     void SaveDisabledAddOnValues(); | ||||
|     void SaveMiscellaneousValues(); | ||||
|     void SavePathValues(); | ||||
|   | ||||
| @@ -98,6 +98,11 @@ | ||||
|          <string>Web</string> | ||||
|         </attribute> | ||||
|        </widget> | ||||
|        <widget class="ConfigureService" name="serviceTab"> | ||||
|         <attribute name="title"> | ||||
|          <string>Services</string> | ||||
|         </attribute> | ||||
|        </widget> | ||||
|       </widget> | ||||
|      </item> | ||||
|     </layout> | ||||
| @@ -178,6 +183,12 @@ | ||||
|    <header>configuration/configure_hotkeys.h</header> | ||||
|    <container>1</container> | ||||
|   </customwidget> | ||||
|   <customwidget> | ||||
|    <class>ConfigureService</class> | ||||
|    <extends>QWidget</extends> | ||||
|    <header>configuration/configure_service.h</header> | ||||
|    <container>1</container> | ||||
|   </customwidget> | ||||
|  </customwidgets> | ||||
|  <resources/> | ||||
|  <connections> | ||||
|   | ||||
| @@ -44,6 +44,7 @@ void ConfigureDialog::ApplyConfiguration() { | ||||
|     ui->audioTab->ApplyConfiguration(); | ||||
|     ui->debugTab->ApplyConfiguration(); | ||||
|     ui->webTab->ApplyConfiguration(); | ||||
|     ui->serviceTab->ApplyConfiguration(); | ||||
|     Settings::Apply(); | ||||
|     Settings::LogSettings(); | ||||
| } | ||||
| @@ -74,7 +75,8 @@ Q_DECLARE_METATYPE(QList<QWidget*>); | ||||
| void ConfigureDialog::PopulateSelectionList() { | ||||
|     const std::array<std::pair<QString, QList<QWidget*>>, 4> items{ | ||||
|         {{tr("General"), {ui->generalTab, ui->webTab, ui->debugTab, ui->gameListTab}}, | ||||
|          {tr("System"), {ui->systemTab, ui->profileManagerTab, ui->filesystemTab, ui->audioTab}}, | ||||
|          {tr("System"), | ||||
|           {ui->systemTab, ui->profileManagerTab, ui->serviceTab, ui->filesystemTab, ui->audioTab}}, | ||||
|          {tr("Graphics"), {ui->graphicsTab}}, | ||||
|          {tr("Controls"), {ui->inputTab, ui->hotkeysTab}}}, | ||||
|     }; | ||||
| @@ -108,6 +110,7 @@ void ConfigureDialog::UpdateVisibleTabs() { | ||||
|         {ui->webTab, tr("Web")}, | ||||
|         {ui->gameListTab, tr("Game List")}, | ||||
|         {ui->filesystemTab, tr("Filesystem")}, | ||||
|         {ui->serviceTab, tr("Services")}, | ||||
|     }; | ||||
|  | ||||
|     [[maybe_unused]] const QSignalBlocker blocker(ui->tabWidget); | ||||
|   | ||||
							
								
								
									
										136
									
								
								src/yuzu/configuration/configure_service.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/yuzu/configuration/configure_service.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | ||||
| // Copyright 2019 yuzu Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #include <QGraphicsItem> | ||||
| #include <QtConcurrent/QtConcurrent> | ||||
| #include "core/hle/service/bcat/backend/boxcat.h" | ||||
| #include "core/settings.h" | ||||
| #include "ui_configure_service.h" | ||||
| #include "yuzu/configuration/configure_service.h" | ||||
|  | ||||
| namespace { | ||||
| QString FormatEventStatusString(const Service::BCAT::EventStatus& status) { | ||||
|     QString out; | ||||
|  | ||||
|     if (status.header.has_value()) { | ||||
|         out += QStringLiteral("<i>%1</i><br>").arg(QString::fromStdString(*status.header)); | ||||
|     } | ||||
|  | ||||
|     if (status.events.size() == 1) { | ||||
|         out += QStringLiteral("%1<br>").arg(QString::fromStdString(status.events.front())); | ||||
|     } else { | ||||
|         for (const auto& event : status.events) { | ||||
|             out += QStringLiteral("- %1<br>").arg(QString::fromStdString(event)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if (status.footer.has_value()) { | ||||
|         out += QStringLiteral("<i>%1</i><br>").arg(QString::fromStdString(*status.footer)); | ||||
|     } | ||||
|  | ||||
|     return out; | ||||
| } | ||||
| } // Anonymous namespace | ||||
|  | ||||
| ConfigureService::ConfigureService(QWidget* parent) | ||||
|     : QWidget(parent), ui(std::make_unique<Ui::ConfigureService>()) { | ||||
|     ui->setupUi(this); | ||||
|  | ||||
|     ui->bcat_source->addItem(QStringLiteral("None")); | ||||
|     ui->bcat_empty_label->setHidden(true); | ||||
|     ui->bcat_empty_header->setHidden(true); | ||||
|  | ||||
| #ifdef YUZU_ENABLE_BOXCAT | ||||
|     ui->bcat_source->addItem(QStringLiteral("Boxcat"), QStringLiteral("boxcat")); | ||||
| #endif | ||||
|  | ||||
|     connect(ui->bcat_source, QOverload<int>::of(&QComboBox::currentIndexChanged), this, | ||||
|             &ConfigureService::OnBCATImplChanged); | ||||
|  | ||||
|     this->SetConfiguration(); | ||||
| } | ||||
|  | ||||
| ConfigureService::~ConfigureService() = default; | ||||
|  | ||||
| void ConfigureService::ApplyConfiguration() { | ||||
|     Settings::values.bcat_backend = ui->bcat_source->currentText().toLower().toStdString(); | ||||
| } | ||||
|  | ||||
| void ConfigureService::RetranslateUi() { | ||||
|     ui->retranslateUi(this); | ||||
| } | ||||
|  | ||||
| void ConfigureService::SetConfiguration() { | ||||
|     const int index = | ||||
|         ui->bcat_source->findData(QString::fromStdString(Settings::values.bcat_backend)); | ||||
|     ui->bcat_source->setCurrentIndex(index == -1 ? 0 : index); | ||||
| } | ||||
|  | ||||
| std::pair<QString, QString> ConfigureService::BCATDownloadEvents() { | ||||
|     std::optional<std::string> global; | ||||
|     std::map<std::string, Service::BCAT::EventStatus> map; | ||||
|     const auto res = Service::BCAT::Boxcat::GetStatus(global, map); | ||||
|  | ||||
|     switch (res) { | ||||
|     case Service::BCAT::Boxcat::StatusResult::Offline: | ||||
|         return {QString{}, | ||||
|                 tr("The boxcat service is offline or you are not connected to the internet.")}; | ||||
|     case Service::BCAT::Boxcat::StatusResult::ParseError: | ||||
|         return {QString{}, | ||||
|                 tr("There was an error while processing the boxcat event data. Contact the yuzu " | ||||
|                    "developers.")}; | ||||
|     case Service::BCAT::Boxcat::StatusResult::BadClientVersion: | ||||
|         return {QString{}, | ||||
|                 tr("The version of yuzu you are using is either too new or too old for the server. " | ||||
|                    "Try updating to the latest official release of yuzu.")}; | ||||
|     } | ||||
|  | ||||
|     if (map.empty()) { | ||||
|         return {QStringLiteral("Current Boxcat Events"), | ||||
|                 tr("There are currently no events on boxcat.")}; | ||||
|     } | ||||
|  | ||||
|     QString out; | ||||
|  | ||||
|     if (global.has_value()) { | ||||
|         out += QStringLiteral("%1<br>").arg(QString::fromStdString(*global)); | ||||
|     } | ||||
|  | ||||
|     for (const auto& [key, value] : map) { | ||||
|         out += QStringLiteral("%1<b>%2</b><br>%3") | ||||
|                    .arg(out.isEmpty() ? QString{} : QStringLiteral("<br>")) | ||||
|                    .arg(QString::fromStdString(key)) | ||||
|                    .arg(FormatEventStatusString(value)); | ||||
|     } | ||||
|     return {QStringLiteral("Current Boxcat Events"), std::move(out)}; | ||||
| } | ||||
|  | ||||
| void ConfigureService::OnBCATImplChanged() { | ||||
| #ifdef YUZU_ENABLE_BOXCAT | ||||
|     const auto boxcat = ui->bcat_source->currentText() == QStringLiteral("Boxcat"); | ||||
|     ui->bcat_empty_header->setHidden(!boxcat); | ||||
|     ui->bcat_empty_label->setHidden(!boxcat); | ||||
|     ui->bcat_empty_header->setText(QString{}); | ||||
|     ui->bcat_empty_label->setText(tr("Yuzu is retrieving the latest boxcat status...")); | ||||
|  | ||||
|     if (!boxcat) | ||||
|         return; | ||||
|  | ||||
|     const auto future = QtConcurrent::run([this] { return BCATDownloadEvents(); }); | ||||
|  | ||||
|     watcher.setFuture(future); | ||||
|     connect(&watcher, &QFutureWatcher<std::pair<QString, QString>>::finished, this, | ||||
|             [this] { OnUpdateBCATEmptyLabel(watcher.result()); }); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void ConfigureService::OnUpdateBCATEmptyLabel(std::pair<QString, QString> string) { | ||||
| #ifdef YUZU_ENABLE_BOXCAT | ||||
|     const auto boxcat = ui->bcat_source->currentText() == QStringLiteral("Boxcat"); | ||||
|     if (boxcat) { | ||||
|         ui->bcat_empty_header->setText(string.first); | ||||
|         ui->bcat_empty_label->setText(string.second); | ||||
|     } | ||||
| #endif | ||||
| } | ||||
							
								
								
									
										34
									
								
								src/yuzu/configuration/configure_service.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/yuzu/configuration/configure_service.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| // Copyright 2019 yuzu Emulator Project | ||||
| // Licensed under GPLv2 or any later version | ||||
| // Refer to the license.txt file included. | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <memory> | ||||
| #include <QFutureWatcher> | ||||
| #include <QWidget> | ||||
|  | ||||
| namespace Ui { | ||||
| class ConfigureService; | ||||
| } | ||||
|  | ||||
| class ConfigureService : public QWidget { | ||||
|     Q_OBJECT | ||||
|  | ||||
| public: | ||||
|     explicit ConfigureService(QWidget* parent = nullptr); | ||||
|     ~ConfigureService() override; | ||||
|  | ||||
|     void ApplyConfiguration(); | ||||
|     void RetranslateUi(); | ||||
|  | ||||
| private: | ||||
|     void SetConfiguration(); | ||||
|  | ||||
|     std::pair<QString, QString> BCATDownloadEvents(); | ||||
|     void OnBCATImplChanged(); | ||||
|     void OnUpdateBCATEmptyLabel(std::pair<QString, QString> string); | ||||
|  | ||||
|     std::unique_ptr<Ui::ConfigureService> ui; | ||||
|     QFutureWatcher<std::pair<QString, QString>> watcher{this}; | ||||
| }; | ||||
							
								
								
									
										124
									
								
								src/yuzu/configuration/configure_service.ui
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/yuzu/configuration/configure_service.ui
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,124 @@ | ||||
| <?xml version="1.0" encoding="UTF-8"?> | ||||
| <ui version="4.0"> | ||||
|  <class>ConfigureService</class> | ||||
|  <widget class="QWidget" name="ConfigureService"> | ||||
|   <property name="geometry"> | ||||
|    <rect> | ||||
|     <x>0</x> | ||||
|     <y>0</y> | ||||
|     <width>433</width> | ||||
|     <height>561</height> | ||||
|    </rect> | ||||
|   </property> | ||||
|   <property name="windowTitle"> | ||||
|    <string>Form</string> | ||||
|   </property> | ||||
|   <layout class="QVBoxLayout" name="verticalLayout"> | ||||
|    <item> | ||||
|     <layout class="QVBoxLayout" name="verticalLayout_3"> | ||||
|      <item> | ||||
|       <widget class="QGroupBox" name="groupBox"> | ||||
|        <property name="title"> | ||||
|         <string>BCAT</string> | ||||
|        </property> | ||||
|        <layout class="QGridLayout" name="gridLayout"> | ||||
|         <item row="1" column="1" colspan="2"> | ||||
|          <widget class="QLabel" name="label_2"> | ||||
|           <property name="maximumSize"> | ||||
|            <size> | ||||
|             <width>260</width> | ||||
|             <height>16777215</height> | ||||
|            </size> | ||||
|           </property> | ||||
|           <property name="text"> | ||||
|            <string>BCAT is Nintendo's way of sending data to games to engage its community and unlock additional content.</string> | ||||
|           </property> | ||||
|           <property name="wordWrap"> | ||||
|            <bool>true</bool> | ||||
|           </property> | ||||
|          </widget> | ||||
|         </item> | ||||
|         <item row="0" column="0"> | ||||
|          <widget class="QLabel" name="label"> | ||||
|           <property name="maximumSize"> | ||||
|            <size> | ||||
|             <width>16777215</width> | ||||
|             <height>16777215</height> | ||||
|            </size> | ||||
|           </property> | ||||
|           <property name="text"> | ||||
|            <string>BCAT Backend</string> | ||||
|           </property> | ||||
|          </widget> | ||||
|         </item> | ||||
|         <item row="3" column="1" colspan="2"> | ||||
|          <widget class="QLabel" name="bcat_empty_label"> | ||||
|           <property name="enabled"> | ||||
|            <bool>true</bool> | ||||
|           </property> | ||||
|           <property name="maximumSize"> | ||||
|            <size> | ||||
|             <width>260</width> | ||||
|             <height>16777215</height> | ||||
|            </size> | ||||
|           </property> | ||||
|           <property name="text"> | ||||
|            <string/> | ||||
|           </property> | ||||
|           <property name="alignment"> | ||||
|            <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> | ||||
|           </property> | ||||
|           <property name="wordWrap"> | ||||
|            <bool>true</bool> | ||||
|           </property> | ||||
|          </widget> | ||||
|         </item> | ||||
|         <item row="2" column="1" colspan="2"> | ||||
|          <widget class="QLabel" name="label_3"> | ||||
|           <property name="text"> | ||||
|            <string><html><head/><body><p><a href="https://yuzu-emu.org/help/feature/boxcat"><span style=" text-decoration: underline; color:#0000ff;">Learn more about BCAT, Boxcat, and Current Events</span></a></p></body></html></string> | ||||
|           </property> | ||||
|           <property name="openExternalLinks"> | ||||
|            <bool>true</bool> | ||||
|           </property> | ||||
|          </widget> | ||||
|         </item> | ||||
|         <item row="0" column="1" colspan="2"> | ||||
|          <widget class="QComboBox" name="bcat_source"/> | ||||
|         </item> | ||||
|         <item row="3" column="0"> | ||||
|          <widget class="QLabel" name="bcat_empty_header"> | ||||
|           <property name="text"> | ||||
|            <string/> | ||||
|           </property> | ||||
|           <property name="alignment"> | ||||
|            <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> | ||||
|           </property> | ||||
|           <property name="wordWrap"> | ||||
|            <bool>true</bool> | ||||
|           </property> | ||||
|          </widget> | ||||
|         </item> | ||||
|        </layout> | ||||
|       </widget> | ||||
|      </item> | ||||
|     </layout> | ||||
|    </item> | ||||
|    <item> | ||||
|     <spacer name="verticalSpacer"> | ||||
|      <property name="orientation"> | ||||
|       <enum>Qt::Vertical</enum> | ||||
|      </property> | ||||
|      <property name="sizeHint" stdset="0"> | ||||
|       <size> | ||||
|        <width>20</width> | ||||
|        <height>40</height> | ||||
|       </size> | ||||
|      </property> | ||||
|     </spacer> | ||||
|    </item> | ||||
|   </layout> | ||||
|  </widget> | ||||
|  <resources/> | ||||
|  <connections/> | ||||
| </ui> | ||||
| @@ -433,6 +433,11 @@ void Config::ReadValues() { | ||||
|         sdl2_config->Get("WebService", "web_api_url", "https://api.yuzu-emu.org"); | ||||
|     Settings::values.yuzu_username = sdl2_config->Get("WebService", "yuzu_username", ""); | ||||
|     Settings::values.yuzu_token = sdl2_config->Get("WebService", "yuzu_token", ""); | ||||
|  | ||||
|     // Services | ||||
|     Settings::values.bcat_backend = sdl2_config->Get("Services", "bcat_backend", "boxcat"); | ||||
|     Settings::values.bcat_boxcat_local = | ||||
|         sdl2_config->GetBoolean("Services", "bcat_boxcat_local", false); | ||||
| } | ||||
|  | ||||
| void Config::Reload() { | ||||
|   | ||||
| @@ -251,6 +251,11 @@ web_api_url = https://api.yuzu-emu.org | ||||
| yuzu_username = | ||||
| yuzu_token = | ||||
|  | ||||
| [Services] | ||||
| # The name of the backend to use for BCAT | ||||
| # If this is set to 'boxcat' boxcat will be used, otherwise a null implementation will be used | ||||
| bcat_backend = | ||||
|  | ||||
| [AddOns] | ||||
| # Used to disable add-ons | ||||
| # List of title IDs of games that will have add-ons disabled (separated by '|'): | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 David
					David