mirror of https://git.suyu.dev/suyu/suyu
Merge pull request #2539 from DarkLordZach/bcat
bcat: Implement BCAT service and connect to yuzu Boxcat server
This commit is contained in:
commit
9aac7fbc22
|
@ -46,3 +46,9 @@
|
||||||
[submodule "sirit"]
|
[submodule "sirit"]
|
||||||
path = externals/sirit
|
path = externals/sirit
|
||||||
url = https://github.com/ReinUsesLisp/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_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_CUBEB "Enables the cubeb audio backend" ON)
|
||||||
|
|
||||||
option(ENABLE_VULKAN "Enables Vulkan backend" ON)
|
option(ENABLE_VULKAN "Enables Vulkan backend" ON)
|
||||||
|
|
|
@ -77,6 +77,12 @@ if (ENABLE_VULKAN)
|
||||||
add_subdirectory(sirit)
|
add_subdirectory(sirit)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# libzip
|
||||||
|
add_subdirectory(libzip)
|
||||||
|
|
||||||
|
# zlib
|
||||||
|
add_subdirectory(zlib)
|
||||||
|
|
||||||
if (ENABLE_WEB_SERVICE)
|
if (ENABLE_WEB_SERVICE)
|
||||||
# LibreSSL
|
# LibreSSL
|
||||||
set(LIBRESSL_SKIP_INSTALL ON CACHE BOOL "")
|
set(LIBRESSL_SKIP_INSTALL ON CACHE BOOL "")
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit bd7a8103e96bc6d50164447f6b7b57bb786d8e2a
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 094ed57db392170130bc710293568de7b576306d
|
|
@ -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
|
add_library(core STATIC
|
||||||
arm/arm_interface.h
|
arm/arm_interface.h
|
||||||
arm/arm_interface.cpp
|
arm/arm_interface.cpp
|
||||||
|
@ -82,6 +88,8 @@ add_library(core STATIC
|
||||||
file_sys/vfs_concat.h
|
file_sys/vfs_concat.h
|
||||||
file_sys/vfs_layered.cpp
|
file_sys/vfs_layered.cpp
|
||||||
file_sys/vfs_layered.h
|
file_sys/vfs_layered.h
|
||||||
|
file_sys/vfs_libzip.cpp
|
||||||
|
file_sys/vfs_libzip.h
|
||||||
file_sys/vfs_offset.cpp
|
file_sys/vfs_offset.cpp
|
||||||
file_sys/vfs_offset.h
|
file_sys/vfs_offset.h
|
||||||
file_sys/vfs_real.cpp
|
file_sys/vfs_real.cpp
|
||||||
|
@ -241,6 +249,9 @@ add_library(core STATIC
|
||||||
hle/service/audio/errors.h
|
hle/service/audio/errors.h
|
||||||
hle/service/audio/hwopus.cpp
|
hle/service/audio/hwopus.cpp
|
||||||
hle/service/audio/hwopus.h
|
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.cpp
|
||||||
hle/service/bcat/bcat.h
|
hle/service/bcat/bcat.h
|
||||||
hle/service/bcat/module.cpp
|
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 common PRIVATE audio_core video_core)
|
||||||
target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt json-headers mbedtls opus unicorn open_source_archives)
|
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)
|
if (ENABLE_WEB_SERVICE)
|
||||||
target_compile_definitions(core PRIVATE -DENABLE_WEB_SERVICE)
|
target_compile_definitions(core PRIVATE -DENABLE_WEB_SERVICE)
|
||||||
target_link_libraries(core PRIVATE 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<Memory::CheatEngine> cheat_engine;
|
||||||
std::unique_ptr<Tools::Freezer> memory_freezer;
|
std::unique_ptr<Tools::Freezer> memory_freezer;
|
||||||
|
std::array<u8, 0x20> build_id{};
|
||||||
|
|
||||||
/// Frontend applets
|
/// Frontend applets
|
||||||
Service::AM::Applets::AppletManager applet_manager;
|
Service::AM::Applets::AppletManager applet_manager;
|
||||||
|
@ -640,6 +641,14 @@ bool System::GetExitLock() const {
|
||||||
return impl->exit_lock;
|
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) {
|
System::ResultStatus System::Init(Frontend::EmuWindow& emu_window) {
|
||||||
return impl->Init(*this, emu_window);
|
return impl->Init(*this, emu_window);
|
||||||
}
|
}
|
||||||
|
|
|
@ -330,6 +330,10 @@ public:
|
||||||
|
|
||||||
bool GetExitLock() const;
|
bool GetExitLock() const;
|
||||||
|
|
||||||
|
void SetCurrentProcessBuildID(std::array<u8, 0x20> id);
|
||||||
|
|
||||||
|
const std::array<u8, 0x20>& GetCurrentProcessBuildID() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
System();
|
System();
|
||||||
|
|
||||||
|
|
|
@ -136,4 +136,9 @@ u64 BISFactory::GetFullNANDTotalSpace() const {
|
||||||
return static_cast<u64>(Settings::values.nand_total_size);
|
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
|
} // namespace FileSys
|
||||||
|
|
|
@ -61,6 +61,8 @@ public:
|
||||||
u64 GetUserNANDTotalSpace() const;
|
u64 GetUserNANDTotalSpace() const;
|
||||||
u64 GetFullNANDTotalSpace() const;
|
u64 GetFullNANDTotalSpace() const;
|
||||||
|
|
||||||
|
VirtualDir GetBCATDirectory(u64 title_id) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
VirtualDir nand_root;
|
VirtualDir nand_root;
|
||||||
VirtualDir load_root;
|
VirtualDir load_root;
|
||||||
|
|
|
@ -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
|
|
@ -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/am/tcap.h"
|
||||||
#include "core/hle/service/apm/controller.h"
|
#include "core/hle/service/apm/controller.h"
|
||||||
#include "core/hle/service/apm/interface.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/filesystem/filesystem.h"
|
||||||
#include "core/hle/service/ns/ns.h"
|
#include "core/hle/service/ns/ns.h"
|
||||||
#include "core/hle/service/nvflinger/nvflinger.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_NO_MESSAGES{ErrorModule::AM, 0x3};
|
||||||
constexpr ResultCode ERR_SIZE_OUT_OF_BOUNDS{ErrorModule::AM, 0x1F7};
|
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 magic;
|
||||||
u32_le is_account_selected;
|
u32_le is_account_selected;
|
||||||
u128 current_user;
|
u128 current_user;
|
||||||
INSERT_PADDING_BYTES(0x70);
|
INSERT_PADDING_BYTES(0x70);
|
||||||
};
|
};
|
||||||
static_assert(sizeof(LaunchParameters) == 0x88);
|
static_assert(sizeof(LaunchParameterAccountPreselectedUser) == 0x88);
|
||||||
|
|
||||||
IWindowController::IWindowController(Core::System& system_)
|
IWindowController::IWindowController(Core::System& system_)
|
||||||
: ServiceFramework("IWindowController"), system{system_} {
|
: ServiceFramework("IWindowController"), system{system_} {
|
||||||
|
@ -1128,26 +1134,55 @@ void IApplicationFunctions::EndBlockingHomeButton(Kernel::HLERequestContext& ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
void IApplicationFunctions::PopLaunchParameter(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;
|
if (kind == LaunchParameterKind::ApplicationSpecific && !launch_popped_application_specific) {
|
||||||
params.is_account_selected = 1;
|
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 data =
|
||||||
const auto uuid = profile_manager.GetUser(Settings::values.current_user);
|
backend->GetLaunchParameter({Core::CurrentProcess()->GetTitleID(), build_id});
|
||||||
ASSERT(uuid);
|
|
||||||
params.current_user = uuid->uuid;
|
|
||||||
|
|
||||||
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));
|
Account::ProfileManager profile_manager{};
|
||||||
std::memcpy(buffer.data(), ¶ms, buffer.size());
|
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(
|
void IApplicationFunctions::CreateApplicationAndRequestToStartForQuest(
|
||||||
|
|
|
@ -255,6 +255,8 @@ private:
|
||||||
void EnableApplicationCrashReport(Kernel::HLERequestContext& ctx);
|
void EnableApplicationCrashReport(Kernel::HLERequestContext& ctx);
|
||||||
void GetGpuErrorDetectedSystemEvent(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;
|
Kernel::EventPair gpu_error_detected_event;
|
||||||
Core::System& system;
|
Core::System& system;
|
||||||
};
|
};
|
||||||
|
|
|
@ -157,6 +157,10 @@ AppletManager::AppletManager(Core::System& system_) : system{system_} {}
|
||||||
|
|
||||||
AppletManager::~AppletManager() = default;
|
AppletManager::~AppletManager() = default;
|
||||||
|
|
||||||
|
const AppletFrontendSet& AppletManager::GetAppletFrontendSet() const {
|
||||||
|
return frontend;
|
||||||
|
}
|
||||||
|
|
||||||
void AppletManager::SetAppletFrontendSet(AppletFrontendSet set) {
|
void AppletManager::SetAppletFrontendSet(AppletFrontendSet set) {
|
||||||
if (set.parental_controls != nullptr)
|
if (set.parental_controls != nullptr)
|
||||||
frontend.parental_controls = std::move(set.parental_controls);
|
frontend.parental_controls = std::move(set.parental_controls);
|
||||||
|
|
|
@ -190,6 +190,8 @@ public:
|
||||||
explicit AppletManager(Core::System& system_);
|
explicit AppletManager(Core::System& system_);
|
||||||
~AppletManager();
|
~AppletManager();
|
||||||
|
|
||||||
|
const AppletFrontendSet& GetAppletFrontendSet() const;
|
||||||
|
|
||||||
void SetAppletFrontendSet(AppletFrontendSet set);
|
void SetAppletFrontendSet(AppletFrontendSet set);
|
||||||
void SetDefaultAppletFrontendSet();
|
void SetDefaultAppletFrontendSet();
|
||||||
void SetDefaultAppletsIfMissing();
|
void SetDefaultAppletsIfMissing();
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 {
|
namespace Service::BCAT {
|
||||||
|
|
||||||
BCAT::BCAT(std::shared_ptr<Module> module, const char* name)
|
BCAT::BCAT(std::shared_ptr<Module> module, FileSystem::FileSystemController& fsc, const char* name)
|
||||||
: Module::Interface(std::move(module), name) {
|
: Module::Interface(std::move(module), fsc, name) {
|
||||||
|
// clang-format off
|
||||||
static const FunctionInfo functions[] = {
|
static const FunctionInfo functions[] = {
|
||||||
{0, &BCAT::CreateBcatService, "CreateBcatService"},
|
{0, &BCAT::CreateBcatService, "CreateBcatService"},
|
||||||
|
{1, &BCAT::CreateDeliveryCacheStorageService, "CreateDeliveryCacheStorageService"},
|
||||||
|
{2, &BCAT::CreateDeliveryCacheStorageServiceWithApplicationId, "CreateDeliveryCacheStorageServiceWithApplicationId"},
|
||||||
};
|
};
|
||||||
|
// clang-format on
|
||||||
RegisterHandlers(functions);
|
RegisterHandlers(functions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,8 @@ namespace Service::BCAT {
|
||||||
|
|
||||||
class BCAT final : public Module::Interface {
|
class BCAT final : public Module::Interface {
|
||||||
public:
|
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;
|
~BCAT() override;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,34 +2,254 @@
|
||||||
// Licensed under GPLv2 or any later version
|
// Licensed under GPLv2 or any later version
|
||||||
// Refer to the license.txt file included.
|
// 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/logging/log.h"
|
||||||
|
#include "common/string_util.h"
|
||||||
|
#include "core/file_sys/vfs.h"
|
||||||
#include "core/hle/ipc_helpers.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/bcat.h"
|
||||||
#include "core/hle/service/bcat/module.h"
|
#include "core/hle/service/bcat/module.h"
|
||||||
|
#include "core/hle/service/filesystem/filesystem.h"
|
||||||
|
#include "core/settings.h"
|
||||||
|
|
||||||
namespace Service::BCAT {
|
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> {
|
class IBcatService final : public ServiceFramework<IBcatService> {
|
||||||
public:
|
public:
|
||||||
IBcatService() : ServiceFramework("IBcatService") {
|
IBcatService(Backend& backend) : ServiceFramework("IBcatService"), backend(backend) {
|
||||||
|
// clang-format off
|
||||||
static const FunctionInfo functions[] = {
|
static const FunctionInfo functions[] = {
|
||||||
{10100, nullptr, "RequestSyncDeliveryCache"},
|
{10100, &IBcatService::RequestSyncDeliveryCache, "RequestSyncDeliveryCache"},
|
||||||
{10101, nullptr, "RequestSyncDeliveryCacheWithDirectoryName"},
|
{10101, &IBcatService::RequestSyncDeliveryCacheWithDirectoryName, "RequestSyncDeliveryCacheWithDirectoryName"},
|
||||||
{10200, nullptr, "CancelSyncDeliveryCacheRequest"},
|
{10200, nullptr, "CancelSyncDeliveryCacheRequest"},
|
||||||
{20100, nullptr, "RequestSyncDeliveryCacheWithApplicationId"},
|
{20100, nullptr, "RequestSyncDeliveryCacheWithApplicationId"},
|
||||||
{20101, nullptr, "RequestSyncDeliveryCacheWithApplicationIdAndDirectoryName"},
|
{20101, nullptr, "RequestSyncDeliveryCacheWithApplicationIdAndDirectoryName"},
|
||||||
{30100, nullptr, "SetPassphrase"},
|
{30100, &IBcatService::SetPassphrase, "SetPassphrase"},
|
||||||
{30200, nullptr, "RegisterBackgroundDeliveryTask"},
|
{30200, nullptr, "RegisterBackgroundDeliveryTask"},
|
||||||
{30201, nullptr, "UnregisterBackgroundDeliveryTask"},
|
{30201, nullptr, "UnregisterBackgroundDeliveryTask"},
|
||||||
{30202, nullptr, "BlockDeliveryTask"},
|
{30202, nullptr, "BlockDeliveryTask"},
|
||||||
{30203, nullptr, "UnblockDeliveryTask"},
|
{30203, nullptr, "UnblockDeliveryTask"},
|
||||||
{90100, nullptr, "EnumerateBackgroundDeliveryTask"},
|
{90100, nullptr, "EnumerateBackgroundDeliveryTask"},
|
||||||
{90200, nullptr, "GetDeliveryList"},
|
{90200, nullptr, "GetDeliveryList"},
|
||||||
{90201, nullptr, "ClearDeliveryCacheStorage"},
|
{90201, &IBcatService::ClearDeliveryCacheStorage, "ClearDeliveryCacheStorage"},
|
||||||
{90300, nullptr, "GetPushNotificationLog"},
|
{90300, nullptr, "GetPushNotificationLog"},
|
||||||
};
|
};
|
||||||
|
// clang-format on
|
||||||
RegisterHandlers(functions);
|
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) {
|
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};
|
IPC::ResponseBuilder rb{ctx, 2, 0, 1};
|
||||||
rb.Push(RESULT_SUCCESS);
|
rb.Push(RESULT_SUCCESS);
|
||||||
rb.PushIpcInterface<IBcatService>();
|
rb.PushIpcInterface<IBcatService>(*backend);
|
||||||
}
|
}
|
||||||
|
|
||||||
Module::Interface::Interface(std::shared_ptr<Module> module, const char* name)
|
class IDeliveryCacheFileService final : public ServiceFramework<IDeliveryCacheFileService> {
|
||||||
: ServiceFramework(name), module(std::move(module)) {}
|
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;
|
Module::Interface::~Interface() = default;
|
||||||
|
|
||||||
void InstallInterfaces(SM::ServiceManager& service_manager) {
|
void InstallInterfaces(Core::System& system) {
|
||||||
auto module = std::make_shared<Module>();
|
auto module = std::make_shared<Module>();
|
||||||
std::make_shared<BCAT>(module, "bcat:a")->InstallAsService(service_manager);
|
std::make_shared<BCAT>(module, system.GetFileSystemController(), "bcat:a")
|
||||||
std::make_shared<BCAT>(module, "bcat:m")->InstallAsService(service_manager);
|
->InstallAsService(system.ServiceManager());
|
||||||
std::make_shared<BCAT>(module, "bcat:u")->InstallAsService(service_manager);
|
std::make_shared<BCAT>(module, system.GetFileSystemController(), "bcat:m")
|
||||||
std::make_shared<BCAT>(module, "bcat:s")->InstallAsService(service_manager);
|
->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
|
} // namespace Service::BCAT
|
||||||
|
|
|
@ -6,23 +6,39 @@
|
||||||
|
|
||||||
#include "core/hle/service/service.h"
|
#include "core/hle/service/service.h"
|
||||||
|
|
||||||
namespace Service::BCAT {
|
namespace Service {
|
||||||
|
|
||||||
|
namespace FileSystem {
|
||||||
|
class FileSystemController;
|
||||||
|
} // namespace FileSystem
|
||||||
|
|
||||||
|
namespace BCAT {
|
||||||
|
|
||||||
|
class Backend;
|
||||||
|
|
||||||
class Module final {
|
class Module final {
|
||||||
public:
|
public:
|
||||||
class Interface : public ServiceFramework<Interface> {
|
class Interface : public ServiceFramework<Interface> {
|
||||||
public:
|
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;
|
~Interface() override;
|
||||||
|
|
||||||
void CreateBcatService(Kernel::HLERequestContext& ctx);
|
void CreateBcatService(Kernel::HLERequestContext& ctx);
|
||||||
|
void CreateDeliveryCacheStorageService(Kernel::HLERequestContext& ctx);
|
||||||
|
void CreateDeliveryCacheStorageServiceWithApplicationId(Kernel::HLERequestContext& ctx);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
FileSystem::FileSystemController& fsc;
|
||||||
|
|
||||||
std::shared_ptr<Module> module;
|
std::shared_ptr<Module> module;
|
||||||
|
std::unique_ptr<Backend> backend;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Registers all BCAT services with the specified service manager.
|
/// 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);
|
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) {
|
void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite) {
|
||||||
if (overwrite) {
|
if (overwrite) {
|
||||||
bis_factory = nullptr;
|
bis_factory = nullptr;
|
||||||
|
|
|
@ -110,6 +110,8 @@ public:
|
||||||
FileSys::VirtualDir GetModificationLoadRoot(u64 title_id) const;
|
FileSys::VirtualDir GetModificationLoadRoot(u64 title_id) const;
|
||||||
FileSys::VirtualDir GetModificationDumpRoot(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
|
// Creates the SaveData, SDMC, and BIS Factories. Should be called once and before any function
|
||||||
// above is called.
|
// above is called.
|
||||||
void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite = true);
|
void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite = true);
|
||||||
|
|
|
@ -12,6 +12,13 @@
|
||||||
|
|
||||||
namespace Service::NIFM {
|
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> {
|
class IScanRequest final : public ServiceFramework<IScanRequest> {
|
||||||
public:
|
public:
|
||||||
explicit IScanRequest() : ServiceFramework("IScanRequest") {
|
explicit IScanRequest() : ServiceFramework("IScanRequest") {
|
||||||
|
@ -81,7 +88,7 @@ private:
|
||||||
|
|
||||||
IPC::ResponseBuilder rb{ctx, 3};
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
rb.Push(RESULT_SUCCESS);
|
rb.Push(RESULT_SUCCESS);
|
||||||
rb.Push<u32>(0);
|
rb.PushEnum(RequestState::Connected);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GetResult(Kernel::HLERequestContext& ctx) {
|
void GetResult(Kernel::HLERequestContext& ctx) {
|
||||||
|
@ -189,14 +196,14 @@ private:
|
||||||
|
|
||||||
IPC::ResponseBuilder rb{ctx, 3};
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
rb.Push(RESULT_SUCCESS);
|
rb.Push(RESULT_SUCCESS);
|
||||||
rb.Push<u8>(0);
|
rb.Push<u8>(1);
|
||||||
}
|
}
|
||||||
void IsAnyInternetRequestAccepted(Kernel::HLERequestContext& ctx) {
|
void IsAnyInternetRequestAccepted(Kernel::HLERequestContext& ctx) {
|
||||||
LOG_WARNING(Service_NIFM, "(STUBBED) called");
|
LOG_WARNING(Service_NIFM, "(STUBBED) called");
|
||||||
|
|
||||||
IPC::ResponseBuilder rb{ctx, 3};
|
IPC::ResponseBuilder rb{ctx, 3};
|
||||||
rb.Push(RESULT_SUCCESS);
|
rb.Push(RESULT_SUCCESS);
|
||||||
rb.Push<u8>(0);
|
rb.Push<u8>(1);
|
||||||
}
|
}
|
||||||
Core::System& system;
|
Core::System& system;
|
||||||
};
|
};
|
||||||
|
|
|
@ -208,7 +208,7 @@ void Init(std::shared_ptr<SM::ServiceManager>& sm, Core::System& system) {
|
||||||
AOC::InstallInterfaces(*sm, system);
|
AOC::InstallInterfaces(*sm, system);
|
||||||
APM::InstallInterfaces(system);
|
APM::InstallInterfaces(system);
|
||||||
Audio::InstallInterfaces(*sm, system);
|
Audio::InstallInterfaces(*sm, system);
|
||||||
BCAT::InstallInterfaces(*sm);
|
BCAT::InstallInterfaces(system);
|
||||||
BPC::InstallInterfaces(*sm);
|
BPC::InstallInterfaces(*sm);
|
||||||
BtDrv::InstallInterfaces(*sm, system);
|
BtDrv::InstallInterfaces(*sm, system);
|
||||||
BTM::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
|
// Apply cheats if they exist and the program has a valid title ID
|
||||||
if (pm) {
|
if (pm) {
|
||||||
auto& system = Core::System::GetInstance();
|
auto& system = Core::System::GetInstance();
|
||||||
|
system.SetCurrentProcessBuildID(nso_header.build_id);
|
||||||
const auto cheats = pm->CreateCheatList(system, nso_header.build_id);
|
const auto cheats = pm->CreateCheatList(system, nso_header.build_id);
|
||||||
if (!cheats.empty()) {
|
if (!cheats.empty()) {
|
||||||
system.RegisterCheatList(cheats, nso_header.build_id, load_base, image_size);
|
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_UseGdbstub", Settings::values.use_gdbstub);
|
||||||
LogSetting("Debugging_GdbstubPort", Settings::values.gdbstub_port);
|
LogSetting("Debugging_GdbstubPort", Settings::values.gdbstub_port);
|
||||||
LogSetting("Debugging_ProgramArgs", Settings::values.program_args);
|
LogSetting("Debugging_ProgramArgs", Settings::values.program_args);
|
||||||
|
LogSetting("Services_BCATBackend", Settings::values.bcat_backend);
|
||||||
|
LogSetting("Services_BCATBoxcatLocal", Settings::values.bcat_boxcat_local);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace Settings
|
} // namespace Settings
|
||||||
|
|
|
@ -448,6 +448,10 @@ struct Values {
|
||||||
bool reporting_services;
|
bool reporting_services;
|
||||||
bool quest_flag;
|
bool quest_flag;
|
||||||
|
|
||||||
|
// BCAT
|
||||||
|
std::string bcat_backend;
|
||||||
|
bool bcat_boxcat_local;
|
||||||
|
|
||||||
// WebService
|
// WebService
|
||||||
bool enable_telemetry;
|
bool enable_telemetry;
|
||||||
std::string web_api_url;
|
std::string web_api_url;
|
||||||
|
|
|
@ -66,6 +66,9 @@ add_executable(yuzu
|
||||||
configuration/configure_profile_manager.cpp
|
configuration/configure_profile_manager.cpp
|
||||||
configuration/configure_profile_manager.h
|
configuration/configure_profile_manager.h
|
||||||
configuration/configure_profile_manager.ui
|
configuration/configure_profile_manager.ui
|
||||||
|
configuration/configure_service.cpp
|
||||||
|
configuration/configure_service.h
|
||||||
|
configuration/configure_service.ui
|
||||||
configuration/configure_system.cpp
|
configuration/configure_system.cpp
|
||||||
configuration/configure_system.h
|
configuration/configure_system.h
|
||||||
configuration/configure_system.ui
|
configuration/configure_system.ui
|
||||||
|
@ -186,6 +189,10 @@ if (YUZU_USE_QT_WEB_ENGINE)
|
||||||
target_compile_definitions(yuzu PRIVATE -DYUZU_USE_QT_WEB_ENGINE)
|
target_compile_definitions(yuzu PRIVATE -DYUZU_USE_QT_WEB_ENGINE)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
if (YUZU_ENABLE_BOXCAT)
|
||||||
|
target_compile_definitions(yuzu PRIVATE -DYUZU_ENABLE_BOXCAT)
|
||||||
|
endif ()
|
||||||
|
|
||||||
if(UNIX AND NOT APPLE)
|
if(UNIX AND NOT APPLE)
|
||||||
install(TARGETS yuzu RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin")
|
install(TARGETS yuzu RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin")
|
||||||
endif()
|
endif()
|
||||||
|
|
|
@ -525,6 +525,17 @@ void Config::ReadDebuggingValues() {
|
||||||
qt_config->endGroup();
|
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() {
|
void Config::ReadDisabledAddOnValues() {
|
||||||
const auto size = qt_config->beginReadArray(QStringLiteral("DisabledAddOns"));
|
const auto size = qt_config->beginReadArray(QStringLiteral("DisabledAddOns"));
|
||||||
|
|
||||||
|
@ -769,6 +780,7 @@ void Config::ReadValues() {
|
||||||
ReadMiscellaneousValues();
|
ReadMiscellaneousValues();
|
||||||
ReadDebuggingValues();
|
ReadDebuggingValues();
|
||||||
ReadWebServiceValues();
|
ReadWebServiceValues();
|
||||||
|
ReadServiceValues();
|
||||||
ReadDisabledAddOnValues();
|
ReadDisabledAddOnValues();
|
||||||
ReadUIValues();
|
ReadUIValues();
|
||||||
}
|
}
|
||||||
|
@ -866,6 +878,7 @@ void Config::SaveValues() {
|
||||||
SaveMiscellaneousValues();
|
SaveMiscellaneousValues();
|
||||||
SaveDebuggingValues();
|
SaveDebuggingValues();
|
||||||
SaveWebServiceValues();
|
SaveWebServiceValues();
|
||||||
|
SaveServiceValues();
|
||||||
SaveDisabledAddOnValues();
|
SaveDisabledAddOnValues();
|
||||||
SaveUIValues();
|
SaveUIValues();
|
||||||
}
|
}
|
||||||
|
@ -963,6 +976,14 @@ void Config::SaveDebuggingValues() {
|
||||||
qt_config->endGroup();
|
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() {
|
void Config::SaveDisabledAddOnValues() {
|
||||||
qt_config->beginWriteArray(QStringLiteral("DisabledAddOns"));
|
qt_config->beginWriteArray(QStringLiteral("DisabledAddOns"));
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,7 @@ private:
|
||||||
void ReadCoreValues();
|
void ReadCoreValues();
|
||||||
void ReadDataStorageValues();
|
void ReadDataStorageValues();
|
||||||
void ReadDebuggingValues();
|
void ReadDebuggingValues();
|
||||||
|
void ReadServiceValues();
|
||||||
void ReadDisabledAddOnValues();
|
void ReadDisabledAddOnValues();
|
||||||
void ReadMiscellaneousValues();
|
void ReadMiscellaneousValues();
|
||||||
void ReadPathValues();
|
void ReadPathValues();
|
||||||
|
@ -65,6 +66,7 @@ private:
|
||||||
void SaveCoreValues();
|
void SaveCoreValues();
|
||||||
void SaveDataStorageValues();
|
void SaveDataStorageValues();
|
||||||
void SaveDebuggingValues();
|
void SaveDebuggingValues();
|
||||||
|
void SaveServiceValues();
|
||||||
void SaveDisabledAddOnValues();
|
void SaveDisabledAddOnValues();
|
||||||
void SaveMiscellaneousValues();
|
void SaveMiscellaneousValues();
|
||||||
void SavePathValues();
|
void SavePathValues();
|
||||||
|
|
|
@ -98,6 +98,11 @@
|
||||||
<string>Web</string>
|
<string>Web</string>
|
||||||
</attribute>
|
</attribute>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="ConfigureService" name="serviceTab">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Services</string>
|
||||||
|
</attribute>
|
||||||
|
</widget>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
@ -178,6 +183,12 @@
|
||||||
<header>configuration/configure_hotkeys.h</header>
|
<header>configuration/configure_hotkeys.h</header>
|
||||||
<container>1</container>
|
<container>1</container>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>ConfigureService</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>configuration/configure_service.h</header>
|
||||||
|
<container>1</container>
|
||||||
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources/>
|
<resources/>
|
||||||
<connections>
|
<connections>
|
||||||
|
|
|
@ -44,6 +44,7 @@ void ConfigureDialog::ApplyConfiguration() {
|
||||||
ui->audioTab->ApplyConfiguration();
|
ui->audioTab->ApplyConfiguration();
|
||||||
ui->debugTab->ApplyConfiguration();
|
ui->debugTab->ApplyConfiguration();
|
||||||
ui->webTab->ApplyConfiguration();
|
ui->webTab->ApplyConfiguration();
|
||||||
|
ui->serviceTab->ApplyConfiguration();
|
||||||
Settings::Apply();
|
Settings::Apply();
|
||||||
Settings::LogSettings();
|
Settings::LogSettings();
|
||||||
}
|
}
|
||||||
|
@ -74,7 +75,8 @@ Q_DECLARE_METATYPE(QList<QWidget*>);
|
||||||
void ConfigureDialog::PopulateSelectionList() {
|
void ConfigureDialog::PopulateSelectionList() {
|
||||||
const std::array<std::pair<QString, QList<QWidget*>>, 4> items{
|
const std::array<std::pair<QString, QList<QWidget*>>, 4> items{
|
||||||
{{tr("General"), {ui->generalTab, ui->webTab, ui->debugTab, ui->gameListTab}},
|
{{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("Graphics"), {ui->graphicsTab}},
|
||||||
{tr("Controls"), {ui->inputTab, ui->hotkeysTab}}},
|
{tr("Controls"), {ui->inputTab, ui->hotkeysTab}}},
|
||||||
};
|
};
|
||||||
|
@ -108,6 +110,7 @@ void ConfigureDialog::UpdateVisibleTabs() {
|
||||||
{ui->webTab, tr("Web")},
|
{ui->webTab, tr("Web")},
|
||||||
{ui->gameListTab, tr("Game List")},
|
{ui->gameListTab, tr("Game List")},
|
||||||
{ui->filesystemTab, tr("Filesystem")},
|
{ui->filesystemTab, tr("Filesystem")},
|
||||||
|
{ui->serviceTab, tr("Services")},
|
||||||
};
|
};
|
||||||
|
|
||||||
[[maybe_unused]] const QSignalBlocker blocker(ui->tabWidget);
|
[[maybe_unused]] const QSignalBlocker blocker(ui->tabWidget);
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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};
|
||||||
|
};
|
|
@ -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");
|
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_username = sdl2_config->Get("WebService", "yuzu_username", "");
|
||||||
Settings::values.yuzu_token = sdl2_config->Get("WebService", "yuzu_token", "");
|
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() {
|
void Config::Reload() {
|
||||||
|
|
|
@ -251,6 +251,11 @@ web_api_url = https://api.yuzu-emu.org
|
||||||
yuzu_username =
|
yuzu_username =
|
||||||
yuzu_token =
|
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]
|
[AddOns]
|
||||||
# Used to disable add-ons
|
# Used to disable add-ons
|
||||||
# List of title IDs of games that will have add-ons disabled (separated by '|'):
|
# List of title IDs of games that will have add-ons disabled (separated by '|'):
|
||||||
|
|
Loading…
Reference in New Issue