diff --git a/.gitmodules b/.gitmodules index 3a49c4874..f3051cca0 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index bfa104034..9b3b0d6d5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index e6fa11a03..d797d9fc9 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -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 "") diff --git a/externals/libzip b/externals/libzip new file mode 160000 index 000000000..bd7a8103e --- /dev/null +++ b/externals/libzip @@ -0,0 +1 @@ +Subproject commit bd7a8103e96bc6d50164447f6b7b57bb786d8e2a diff --git a/externals/zlib b/externals/zlib new file mode 160000 index 000000000..094ed57db --- /dev/null +++ b/externals/zlib @@ -0,0 +1 @@ +Subproject commit 094ed57db392170130bc710293568de7b576306d diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index a6b56c9c6..3416854db 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -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) diff --git a/src/core/core.cpp b/src/core/core.cpp index 92ba42fb9..75a7ffb97 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -339,6 +339,7 @@ struct System::Impl { std::unique_ptr cheat_engine; std::unique_ptr memory_freezer; + std::array 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 id) { + impl->build_id = id; +} + +const std::array& System::GetCurrentProcessBuildID() const { + return impl->build_id; +} + System::ResultStatus System::Init(Frontend::EmuWindow& emu_window) { return impl->Init(*this, emu_window); } diff --git a/src/core/core.h b/src/core/core.h index ff10ebe12..f49b7fbf9 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -330,6 +330,10 @@ public: bool GetExitLock() const; + void SetCurrentProcessBuildID(std::array id); + + const std::array& GetCurrentProcessBuildID() const; + private: System(); diff --git a/src/core/file_sys/bis_factory.cpp b/src/core/file_sys/bis_factory.cpp index 8f758d6d9..0af44f340 100644 --- a/src/core/file_sys/bis_factory.cpp +++ b/src/core/file_sys/bis_factory.cpp @@ -136,4 +136,9 @@ u64 BISFactory::GetFullNANDTotalSpace() const { return static_cast(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 diff --git a/src/core/file_sys/bis_factory.h b/src/core/file_sys/bis_factory.h index bdfe728c9..8f0451c98 100644 --- a/src/core/file_sys/bis_factory.h +++ b/src/core/file_sys/bis_factory.h @@ -61,6 +61,8 @@ public: u64 GetUserNANDTotalSpace() const; u64 GetFullNANDTotalSpace() const; + VirtualDir GetBCATDirectory(u64 title_id) const; + private: VirtualDir nand_root; VirtualDir load_root; diff --git a/src/core/file_sys/vfs_libzip.cpp b/src/core/file_sys/vfs_libzip.cpp new file mode 100644 index 000000000..8bdaa7e4a --- /dev/null +++ b/src/core/file_sys/vfs_libzip.cpp @@ -0,0 +1,79 @@ +// Copyright 2019 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#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 src{ + zip_source_buffer_create(data.data(), data.size(), 0, &error), zip_source_close}; + if (src == nullptr) + return nullptr; + + std::unique_ptr zip{zip_open_from_source(src.get(), 0, &error), + zip_close}; + if (zip == nullptr) + return nullptr; + + std::shared_ptr out = std::make_shared(); + + 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 file{ + zip_fopen_index(zip.get(), i, 0), zip_fclose}; + + std::vector 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(buf, parts.back()); + + std::shared_ptr 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( + std::vector{}, std::vector{}, parts[j]); + dtrv->AddDirectory(temp); + dtrv = temp; + } else { + dtrv = std::dynamic_pointer_cast(subdir); + } + } + + if (dtrv == nullptr) + return nullptr; + dtrv->AddFile(new_file); + } + } + + return out; +} + +} // namespace FileSys diff --git a/src/core/file_sys/vfs_libzip.h b/src/core/file_sys/vfs_libzip.h new file mode 100644 index 000000000..f68af576a --- /dev/null +++ b/src/core/file_sys/vfs_libzip.h @@ -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 diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp index 797c9a06f..34409e0c3 100644 --- a/src/core/hle/service/am/am.cpp +++ b/src/core/hle/service/am/am.cpp @@ -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(); - LaunchParameters params{}; + LOG_DEBUG(Service_AM, "called, kind={:08X}", static_cast(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(*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 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(buffer); + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + + rb.Push(RESULT_SUCCESS); + + std::vector buffer(sizeof(LaunchParameterAccountPreselectedUser)); + std::memcpy(buffer.data(), ¶ms, buffer.size()); + + rb.PushIpcInterface(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( diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h index a3baeb673..9169eb2bd 100644 --- a/src/core/hle/service/am/am.h +++ b/src/core/hle/service/am/am.h @@ -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; }; diff --git a/src/core/hle/service/am/applets/applets.cpp b/src/core/hle/service/am/applets/applets.cpp index d2e35362f..720fe766f 100644 --- a/src/core/hle/service/am/applets/applets.cpp +++ b/src/core/hle/service/am/applets/applets.cpp @@ -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); diff --git a/src/core/hle/service/am/applets/applets.h b/src/core/hle/service/am/applets/applets.h index 764c3418c..226be88b1 100644 --- a/src/core/hle/service/am/applets/applets.h +++ b/src/core/hle/service/am/applets/applets.h @@ -190,6 +190,8 @@ public: explicit AppletManager(Core::System& system_); ~AppletManager(); + const AppletFrontendSet& GetAppletFrontendSet() const; + void SetAppletFrontendSet(AppletFrontendSet set); void SetDefaultAppletFrontendSet(); void SetDefaultAppletsIfMissing(); diff --git a/src/core/hle/service/bcat/backend/backend.cpp b/src/core/hle/service/bcat/backend/backend.cpp new file mode 100644 index 000000000..9b677debe --- /dev/null +++ b/src/core/hle/service/bcat/backend/backend.cpp @@ -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 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(dir_name.size(), 0x31ull)); + std::memcpy(impl.current_file.data(), file_name.data(), + std::min(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(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 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> 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 diff --git a/src/core/hle/service/bcat/backend/backend.h b/src/core/hle/service/bcat/backend/backend.h new file mode 100644 index 000000000..3f5d8b5dd --- /dev/null +++ b/src/core/hle/service/bcat/backend/backend.h @@ -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 +#include +#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; +using Passphrase = std::array; + +struct TitleIDVersion { + u64 title_id; + u64 build_id; +}; + +using DirectoryName = std::array; +using FileName = std::array; + +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 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> 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> GetLaunchParameter(TitleIDVersion title) override; +}; + +std::unique_ptr CreateBackendFromSettings(DirectoryGetter getter); + +} // namespace Service::BCAT diff --git a/src/core/hle/service/bcat/backend/boxcat.cpp b/src/core/hle/service/bcat/backend/boxcat.cpp new file mode 100644 index 000000000..e6ee0810b --- /dev/null +++ b/src/core/hle/service/bcat/backend/boxcat.cpp @@ -0,0 +1,503 @@ +// Copyright 2019 yuzu emulator team +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#include +#include +#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 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(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(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 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(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 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(ResponseStatus::NoUpdate)) + return DownloadResult::Success; + if (response->status == static_cast(ResponseStatus::BadClientVersion)) + return DownloadResult::BadClientVersion; + if (response->status == static_cast(ResponseStatus::NoMatchTitleId)) + return DownloadResult::NoMatchTitleId; + if (response->status == static_cast(ResponseStatus::NoMatchBuildId)) + return DownloadResult::NoMatchBuildId; + if (response->status != static_cast(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; + static Digest DigestFile(std::vector bytes) { + Digest out{}; + mbedtls_sha256(bytes.data(), bytes.size(), out.data(), 0); + return out; + } + + std::unique_ptr 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 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 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(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 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 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> 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 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& global, + std::map& games) { + httplib::SSLClient client{BOXCAT_HOSTNAME, static_cast(PORT), + static_cast(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(ResponseStatus::BadClientVersion)) + return StatusResult::BadClientVersion; + + try { + nlohmann::json json = nlohmann::json::parse(response->body); + + if (!json["online"].get()) + return StatusResult::Offline; + + if (json["global"].is_null()) + global = std::nullopt; + else + global = json["global"].get(); + + 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(); + } else { + detail.header = std::nullopt; + } + + if (object["footer"].is_string()) { + detail.footer = object["footer"].get(); + } 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()); + } + } + + 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 diff --git a/src/core/hle/service/bcat/backend/boxcat.h b/src/core/hle/service/bcat/backend/boxcat.h new file mode 100644 index 000000000..601151189 --- /dev/null +++ b/src/core/hle/service/bcat/backend/boxcat.h @@ -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 +#include +#include +#include "core/hle/service/bcat/backend/backend.h" + +namespace Service::BCAT { + +struct EventStatus { + std::optional header; + std::optional footer; + std::vector 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 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> GetLaunchParameter(TitleIDVersion title) override; + + enum class StatusResult { + Success, + Offline, + ParseError, + BadClientVersion, + }; + + static StatusResult GetStatus(std::optional& global, + std::map& games); + +private: + std::atomic_bool is_syncing{false}; + + class Client; + std::unique_ptr client; +}; + +} // namespace Service::BCAT diff --git a/src/core/hle/service/bcat/bcat.cpp b/src/core/hle/service/bcat/bcat.cpp index 179aa4949..c2f946424 100644 --- a/src/core/hle/service/bcat/bcat.cpp +++ b/src/core/hle/service/bcat/bcat.cpp @@ -6,11 +6,15 @@ namespace Service::BCAT { -BCAT::BCAT(std::shared_ptr module, const char* name) - : Module::Interface(std::move(module), name) { +BCAT::BCAT(std::shared_ptr 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); } diff --git a/src/core/hle/service/bcat/bcat.h b/src/core/hle/service/bcat/bcat.h index 802bd689a..813073658 100644 --- a/src/core/hle/service/bcat/bcat.h +++ b/src/core/hle/service/bcat/bcat.h @@ -10,7 +10,8 @@ namespace Service::BCAT { class BCAT final : public Module::Interface { public: - explicit BCAT(std::shared_ptr module, const char* name); + explicit BCAT(std::shared_ptr module, FileSystem::FileSystemController& fsc, + const char* name); ~BCAT() override; }; diff --git a/src/core/hle/service/bcat/module.cpp b/src/core/hle/service/bcat/module.cpp index b7bd738fc..b3fed56c7 100644 --- a/src/core/hle/service/bcat/module.cpp +++ b/src/core/hle/service/bcat/module.cpp @@ -2,34 +2,254 @@ // Licensed under GPLv2 or any later version // Refer to the license.txt file included. +#include +#include +#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; + +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 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(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 { +public: + IDeliveryCacheProgressService(Kernel::SharedPtr 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 event; + const DeliveryCacheProgressImpl& impl; +}; + class IBcatService final : public ServiceFramework { 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 CreateProgressService(SyncType type) { + auto& backend{progress.at(static_cast(type))}; + return std::make_shared(backend.GetEvent(), + backend.GetImpl()); + } + + void RequestSyncDeliveryCache(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_BCAT, "called"); + + backend.Synchronize({Core::CurrentProcess()->GetTitleID(), GetCurrentBuildID()}, + progress.at(static_cast(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(); + 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(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(); + + 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(); + + 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(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(); + rb.PushIpcInterface(*backend); } -Module::Interface::Interface(std::shared_ptr module, const char* name) - : ServiceFramework(name), module(std::move(module)) {} +class IDeliveryCacheFileService final : public ServiceFramework { +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(); + const auto file_name_raw = rp.PopRaw(); + + 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()}; + + 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(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(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(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 { +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(); + 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(write_size, files.size()); + std::vector 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(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(files.size()); + } + + FileSys::VirtualDir root; + FileSys::VirtualDir current_dir; +}; + +class IDeliveryCacheStorageService final : public ServiceFramework { +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(root); + } + + void CreateDirectoryService(Kernel::HLERequestContext& ctx) { + LOG_DEBUG(Service_BCAT, "called"); + + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(RESULT_SUCCESS); + rb.PushIpcInterface(root); + } + + void EnumerateDeliveryCacheDirectory(Kernel::HLERequestContext& ctx) { + auto size = ctx.GetWriteBufferSize() / sizeof(DirectoryName); + + LOG_DEBUG(Service_BCAT, "called, size={:016X}", size); + + size = std::min(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(size); + } + + FileSys::VirtualDir root; + std::vector 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( + fsc.GetBCATDirectory(Core::CurrentProcess()->GetTitleID())); +} + +void Module::Interface::CreateDeliveryCacheStorageServiceWithApplicationId( + Kernel::HLERequestContext& ctx) { + IPC::RequestParser rp{ctx}; + const auto title_id = rp.PopRaw(); + + LOG_DEBUG(Service_BCAT, "called, title_id={:016X}", title_id); + + IPC::ResponseBuilder rb{ctx, 2, 0, 1}; + rb.Push(RESULT_SUCCESS); + rb.PushIpcInterface(fsc.GetBCATDirectory(title_id)); +} + +std::unique_ptr CreateBackendFromSettings(DirectoryGetter getter) { + const auto backend = Settings::values.bcat_backend; + +#ifdef YUZU_ENABLE_BOXCAT + if (backend == "boxcat") + return std::make_unique(std::move(getter)); +#endif + + return std::make_unique(std::move(getter)); +} + +Module::Interface::Interface(std::shared_ptr 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(); - std::make_shared(module, "bcat:a")->InstallAsService(service_manager); - std::make_shared(module, "bcat:m")->InstallAsService(service_manager); - std::make_shared(module, "bcat:u")->InstallAsService(service_manager); - std::make_shared(module, "bcat:s")->InstallAsService(service_manager); + std::make_shared(module, system.GetFileSystemController(), "bcat:a") + ->InstallAsService(system.ServiceManager()); + std::make_shared(module, system.GetFileSystemController(), "bcat:m") + ->InstallAsService(system.ServiceManager()); + std::make_shared(module, system.GetFileSystemController(), "bcat:u") + ->InstallAsService(system.ServiceManager()); + std::make_shared(module, system.GetFileSystemController(), "bcat:s") + ->InstallAsService(system.ServiceManager()); } } // namespace Service::BCAT diff --git a/src/core/hle/service/bcat/module.h b/src/core/hle/service/bcat/module.h index f0d63cab0..27469926a 100644 --- a/src/core/hle/service/bcat/module.h +++ b/src/core/hle/service/bcat/module.h @@ -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 { public: - explicit Interface(std::shared_ptr module, const char* name); + explicit Interface(std::shared_ptr 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; + std::unique_ptr 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 diff --git a/src/core/hle/service/filesystem/filesystem.cpp b/src/core/hle/service/filesystem/filesystem.cpp index 14cd0e322..7fa4e820b 100644 --- a/src/core/hle/service/filesystem/filesystem.cpp +++ b/src/core/hle/service/filesystem/filesystem.cpp @@ -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; diff --git a/src/core/hle/service/filesystem/filesystem.h b/src/core/hle/service/filesystem/filesystem.h index 3e0c03ec0..e6b49d8a2 100644 --- a/src/core/hle/service/filesystem/filesystem.h +++ b/src/core/hle/service/filesystem/filesystem.h @@ -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); diff --git a/src/core/hle/service/nifm/nifm.cpp b/src/core/hle/service/nifm/nifm.cpp index 24d1813a7..756a2af57 100644 --- a/src/core/hle/service/nifm/nifm.cpp +++ b/src/core/hle/service/nifm/nifm.cpp @@ -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 { public: explicit IScanRequest() : ServiceFramework("IScanRequest") { @@ -81,7 +88,7 @@ private: IPC::ResponseBuilder rb{ctx, 3}; rb.Push(RESULT_SUCCESS); - rb.Push(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(0); + rb.Push(1); } void IsAnyInternetRequestAccepted(Kernel::HLERequestContext& ctx) { LOG_WARNING(Service_NIFM, "(STUBBED) called"); IPC::ResponseBuilder rb{ctx, 3}; rb.Push(RESULT_SUCCESS); - rb.Push(0); + rb.Push(1); } Core::System& system; }; diff --git a/src/core/hle/service/service.cpp b/src/core/hle/service/service.cpp index 831a427de..f2c6fe9dc 100644 --- a/src/core/hle/service/service.cpp +++ b/src/core/hle/service/service.cpp @@ -208,7 +208,7 @@ void Init(std::shared_ptr& 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); diff --git a/src/core/loader/nso.cpp b/src/core/loader/nso.cpp index e75c700ad..f629892ae 100644 --- a/src/core/loader/nso.cpp +++ b/src/core/loader/nso.cpp @@ -150,6 +150,7 @@ std::optional 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); diff --git a/src/core/settings.cpp b/src/core/settings.cpp index 7de3fd1e5..d1fc94060 100644 --- a/src/core/settings.cpp +++ b/src/core/settings.cpp @@ -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 diff --git a/src/core/settings.h b/src/core/settings.h index 47bddfb30..9c98a9287 100644 --- a/src/core/settings.h +++ b/src/core/settings.h @@ -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; diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt index dc6fa07fc..ff1c1d985 100644 --- a/src/yuzu/CMakeLists.txt +++ b/src/yuzu/CMakeLists.txt @@ -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() diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp index 92d9fb161..4cb27ddb2 100644 --- a/src/yuzu/configuration/config.cpp +++ b/src/yuzu/configuration/config.cpp @@ -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")); diff --git a/src/yuzu/configuration/config.h b/src/yuzu/configuration/config.h index 6b523ecdd..ba6888004 100644 --- a/src/yuzu/configuration/config.h +++ b/src/yuzu/configuration/config.h @@ -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(); diff --git a/src/yuzu/configuration/configure.ui b/src/yuzu/configuration/configure.ui index 49fadd0ef..372427ae2 100644 --- a/src/yuzu/configuration/configure.ui +++ b/src/yuzu/configuration/configure.ui @@ -98,6 +98,11 @@ Web + + + Services + + @@ -178,6 +183,12 @@
configuration/configure_hotkeys.h
1 + + ConfigureService + QWidget +
configuration/configure_service.h
+ 1 +
diff --git a/src/yuzu/configuration/configure_dialog.cpp b/src/yuzu/configuration/configure_dialog.cpp index 7c875ae87..25b2e1b05 100644 --- a/src/yuzu/configuration/configure_dialog.cpp +++ b/src/yuzu/configuration/configure_dialog.cpp @@ -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); void ConfigureDialog::PopulateSelectionList() { const std::array>, 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); diff --git a/src/yuzu/configuration/configure_service.cpp b/src/yuzu/configuration/configure_service.cpp new file mode 100644 index 000000000..81c9e933f --- /dev/null +++ b/src/yuzu/configuration/configure_service.cpp @@ -0,0 +1,136 @@ +// Copyright 2019 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include +#include +#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("%1
").arg(QString::fromStdString(*status.header)); + } + + if (status.events.size() == 1) { + out += QStringLiteral("%1
").arg(QString::fromStdString(status.events.front())); + } else { + for (const auto& event : status.events) { + out += QStringLiteral("- %1
").arg(QString::fromStdString(event)); + } + } + + if (status.footer.has_value()) { + out += QStringLiteral("%1
").arg(QString::fromStdString(*status.footer)); + } + + return out; +} +} // Anonymous namespace + +ConfigureService::ConfigureService(QWidget* parent) + : QWidget(parent), ui(std::make_unique()) { + 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::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 ConfigureService::BCATDownloadEvents() { + std::optional global; + std::map 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
").arg(QString::fromStdString(*global)); + } + + for (const auto& [key, value] : map) { + out += QStringLiteral("%1%2
%3") + .arg(out.isEmpty() ? QString{} : QStringLiteral("
")) + .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>::finished, this, + [this] { OnUpdateBCATEmptyLabel(watcher.result()); }); +#endif +} + +void ConfigureService::OnUpdateBCATEmptyLabel(std::pair 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 +} diff --git a/src/yuzu/configuration/configure_service.h b/src/yuzu/configuration/configure_service.h new file mode 100644 index 000000000..f5c1b703a --- /dev/null +++ b/src/yuzu/configuration/configure_service.h @@ -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 +#include +#include + +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 BCATDownloadEvents(); + void OnBCATImplChanged(); + void OnUpdateBCATEmptyLabel(std::pair string); + + std::unique_ptr ui; + QFutureWatcher> watcher{this}; +}; diff --git a/src/yuzu/configuration/configure_service.ui b/src/yuzu/configuration/configure_service.ui new file mode 100644 index 000000000..9668dd557 --- /dev/null +++ b/src/yuzu/configuration/configure_service.ui @@ -0,0 +1,124 @@ + + + ConfigureService + + + + 0 + 0 + 433 + 561 + + + + Form + + + + + + + + BCAT + + + + + + + 260 + 16777215 + + + + BCAT is Nintendo's way of sending data to games to engage its community and unlock additional content. + + + true + + + + + + + + 16777215 + 16777215 + + + + BCAT Backend + + + + + + + true + + + + 260 + 16777215 + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + <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> + + + true + + + + + + + + + + + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + true + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/src/yuzu_cmd/config.cpp b/src/yuzu_cmd/config.cpp index d82438502..1a812cb87 100644 --- a/src/yuzu_cmd/config.cpp +++ b/src/yuzu_cmd/config.cpp @@ -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() { diff --git a/src/yuzu_cmd/default_ini.h b/src/yuzu_cmd/default_ini.h index a6171c3ed..8d18a4a5a 100644 --- a/src/yuzu_cmd/default_ini.h +++ b/src/yuzu_cmd/default_ini.h @@ -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 '|'):