summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/core/CMakeLists.txt20
-rw-r--r--src/core/core.cpp9
-rw-r--r--src/core/core.h4
-rw-r--r--src/core/file_sys/bis_factory.cpp5
-rw-r--r--src/core/file_sys/bis_factory.h2
-rw-r--r--src/core/file_sys/vfs_libzip.cpp79
-rw-r--r--src/core/file_sys/vfs_libzip.h13
-rw-r--r--src/core/hle/service/am/am.cpp67
-rw-r--r--src/core/hle/service/am/am.h2
-rw-r--r--src/core/hle/service/am/applets/applets.cpp4
-rw-r--r--src/core/hle/service/am/applets/applets.h2
-rw-r--r--src/core/hle/service/bcat/backend/backend.cpp136
-rw-r--r--src/core/hle/service/bcat/backend/backend.h147
-rw-r--r--src/core/hle/service/bcat/backend/boxcat.cpp503
-rw-r--r--src/core/hle/service/bcat/backend/boxcat.h58
-rw-r--r--src/core/hle/service/bcat/bcat.cpp8
-rw-r--r--src/core/hle/service/bcat/bcat.h3
-rw-r--r--src/core/hle/service/bcat/module.cpp557
-rw-r--r--src/core/hle/service/bcat/module.h24
-rw-r--r--src/core/hle/service/filesystem/filesystem.cpp9
-rw-r--r--src/core/hle/service/filesystem/filesystem.h2
-rw-r--r--src/core/hle/service/nifm/nifm.cpp13
-rw-r--r--src/core/hle/service/service.cpp2
-rw-r--r--src/core/loader/nso.cpp1
-rw-r--r--src/core/settings.cpp2
-rw-r--r--src/core/settings.h4
-rw-r--r--src/yuzu/CMakeLists.txt7
-rw-r--r--src/yuzu/configuration/config.cpp21
-rw-r--r--src/yuzu/configuration/config.h2
-rw-r--r--src/yuzu/configuration/configure.ui11
-rw-r--r--src/yuzu/configuration/configure_dialog.cpp5
-rw-r--r--src/yuzu/configuration/configure_service.cpp136
-rw-r--r--src/yuzu/configuration/configure_service.h34
-rw-r--r--src/yuzu/configuration/configure_service.ui124
-rw-r--r--src/yuzu_cmd/config.cpp5
-rw-r--r--src/yuzu_cmd/default_ini.h5
36 files changed, 1985 insertions, 41 deletions
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index a6b56c9c6..3416854db 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -1,3 +1,9 @@
1if (YUZU_ENABLE_BOXCAT)
2 set(BCAT_BOXCAT_ADDITIONAL_SOURCES hle/service/bcat/backend/boxcat.cpp hle/service/bcat/backend/boxcat.h)
3else()
4 set(BCAT_BOXCAT_ADDITIONAL_SOURCES)
5endif()
6
1add_library(core STATIC 7add_library(core STATIC
2 arm/arm_interface.h 8 arm/arm_interface.h
3 arm/arm_interface.cpp 9 arm/arm_interface.cpp
@@ -82,6 +88,8 @@ add_library(core STATIC
82 file_sys/vfs_concat.h 88 file_sys/vfs_concat.h
83 file_sys/vfs_layered.cpp 89 file_sys/vfs_layered.cpp
84 file_sys/vfs_layered.h 90 file_sys/vfs_layered.h
91 file_sys/vfs_libzip.cpp
92 file_sys/vfs_libzip.h
85 file_sys/vfs_offset.cpp 93 file_sys/vfs_offset.cpp
86 file_sys/vfs_offset.h 94 file_sys/vfs_offset.h
87 file_sys/vfs_real.cpp 95 file_sys/vfs_real.cpp
@@ -241,6 +249,9 @@ add_library(core STATIC
241 hle/service/audio/errors.h 249 hle/service/audio/errors.h
242 hle/service/audio/hwopus.cpp 250 hle/service/audio/hwopus.cpp
243 hle/service/audio/hwopus.h 251 hle/service/audio/hwopus.h
252 hle/service/bcat/backend/backend.cpp
253 hle/service/bcat/backend/backend.h
254 ${BCAT_BOXCAT_ADDITIONAL_SOURCES}
244 hle/service/bcat/bcat.cpp 255 hle/service/bcat/bcat.cpp
245 hle/service/bcat/bcat.h 256 hle/service/bcat/bcat.h
246 hle/service/bcat/module.cpp 257 hle/service/bcat/module.cpp
@@ -499,6 +510,15 @@ create_target_directory_groups(core)
499 510
500target_link_libraries(core PUBLIC common PRIVATE audio_core video_core) 511target_link_libraries(core PUBLIC common PRIVATE audio_core video_core)
501target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt json-headers mbedtls opus unicorn open_source_archives) 512target_link_libraries(core PUBLIC Boost::boost PRIVATE fmt json-headers mbedtls opus unicorn open_source_archives)
513
514if (YUZU_ENABLE_BOXCAT)
515 get_directory_property(OPENSSL_LIBS
516 DIRECTORY ${PROJECT_SOURCE_DIR}/externals/libressl
517 DEFINITION OPENSSL_LIBS)
518 target_compile_definitions(core PRIVATE -DCPPHTTPLIB_OPENSSL_SUPPORT -DYUZU_ENABLE_BOXCAT)
519 target_link_libraries(core PRIVATE httplib json-headers ${OPENSSL_LIBS} zip)
520endif()
521
502if (ENABLE_WEB_SERVICE) 522if (ENABLE_WEB_SERVICE)
503 target_compile_definitions(core PRIVATE -DENABLE_WEB_SERVICE) 523 target_compile_definitions(core PRIVATE -DENABLE_WEB_SERVICE)
504 target_link_libraries(core PRIVATE web_service) 524 target_link_libraries(core PRIVATE web_service)
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 92ba42fb9..75a7ffb97 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -339,6 +339,7 @@ struct System::Impl {
339 339
340 std::unique_ptr<Memory::CheatEngine> cheat_engine; 340 std::unique_ptr<Memory::CheatEngine> cheat_engine;
341 std::unique_ptr<Tools::Freezer> memory_freezer; 341 std::unique_ptr<Tools::Freezer> memory_freezer;
342 std::array<u8, 0x20> build_id{};
342 343
343 /// Frontend applets 344 /// Frontend applets
344 Service::AM::Applets::AppletManager applet_manager; 345 Service::AM::Applets::AppletManager applet_manager;
@@ -640,6 +641,14 @@ bool System::GetExitLock() const {
640 return impl->exit_lock; 641 return impl->exit_lock;
641} 642}
642 643
644void System::SetCurrentProcessBuildID(std::array<u8, 32> id) {
645 impl->build_id = id;
646}
647
648const std::array<u8, 32>& System::GetCurrentProcessBuildID() const {
649 return impl->build_id;
650}
651
643System::ResultStatus System::Init(Frontend::EmuWindow& emu_window) { 652System::ResultStatus System::Init(Frontend::EmuWindow& emu_window) {
644 return impl->Init(*this, emu_window); 653 return impl->Init(*this, emu_window);
645} 654}
diff --git a/src/core/core.h b/src/core/core.h
index ff10ebe12..f49b7fbf9 100644
--- a/src/core/core.h
+++ b/src/core/core.h
@@ -330,6 +330,10 @@ public:
330 330
331 bool GetExitLock() const; 331 bool GetExitLock() const;
332 332
333 void SetCurrentProcessBuildID(std::array<u8, 0x20> id);
334
335 const std::array<u8, 0x20>& GetCurrentProcessBuildID() const;
336
333private: 337private:
334 System(); 338 System();
335 339
diff --git a/src/core/file_sys/bis_factory.cpp b/src/core/file_sys/bis_factory.cpp
index 8f758d6d9..0af44f340 100644
--- a/src/core/file_sys/bis_factory.cpp
+++ b/src/core/file_sys/bis_factory.cpp
@@ -136,4 +136,9 @@ u64 BISFactory::GetFullNANDTotalSpace() const {
136 return static_cast<u64>(Settings::values.nand_total_size); 136 return static_cast<u64>(Settings::values.nand_total_size);
137} 137}
138 138
139VirtualDir BISFactory::GetBCATDirectory(u64 title_id) const {
140 return GetOrCreateDirectoryRelative(nand_root,
141 fmt::format("/system/save/bcat/{:016X}", title_id));
142}
143
139} // namespace FileSys 144} // namespace FileSys
diff --git a/src/core/file_sys/bis_factory.h b/src/core/file_sys/bis_factory.h
index bdfe728c9..8f0451c98 100644
--- a/src/core/file_sys/bis_factory.h
+++ b/src/core/file_sys/bis_factory.h
@@ -61,6 +61,8 @@ public:
61 u64 GetUserNANDTotalSpace() const; 61 u64 GetUserNANDTotalSpace() const;
62 u64 GetFullNANDTotalSpace() const; 62 u64 GetFullNANDTotalSpace() const;
63 63
64 VirtualDir GetBCATDirectory(u64 title_id) const;
65
64private: 66private:
65 VirtualDir nand_root; 67 VirtualDir nand_root;
66 VirtualDir load_root; 68 VirtualDir load_root;
diff --git a/src/core/file_sys/vfs_libzip.cpp b/src/core/file_sys/vfs_libzip.cpp
new file mode 100644
index 000000000..8bdaa7e4a
--- /dev/null
+++ b/src/core/file_sys/vfs_libzip.cpp
@@ -0,0 +1,79 @@
1// Copyright 2019 yuzu emulator team
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5#include <string>
6#include <zip.h>
7#include "common/logging/backend.h"
8#include "core/file_sys/vfs.h"
9#include "core/file_sys/vfs_libzip.h"
10#include "core/file_sys/vfs_vector.h"
11
12namespace FileSys {
13
14VirtualDir ExtractZIP(VirtualFile file) {
15 zip_error_t error{};
16
17 const auto data = file->ReadAllBytes();
18 std::unique_ptr<zip_source_t, decltype(&zip_source_close)> src{
19 zip_source_buffer_create(data.data(), data.size(), 0, &error), zip_source_close};
20 if (src == nullptr)
21 return nullptr;
22
23 std::unique_ptr<zip_t, decltype(&zip_close)> zip{zip_open_from_source(src.get(), 0, &error),
24 zip_close};
25 if (zip == nullptr)
26 return nullptr;
27
28 std::shared_ptr<VectorVfsDirectory> out = std::make_shared<VectorVfsDirectory>();
29
30 const auto num_entries = zip_get_num_entries(zip.get(), 0);
31
32 zip_stat_t stat{};
33 zip_stat_init(&stat);
34
35 for (std::size_t i = 0; i < num_entries; ++i) {
36 const auto stat_res = zip_stat_index(zip.get(), i, 0, &stat);
37 if (stat_res == -1)
38 return nullptr;
39
40 const std::string name(stat.name);
41 if (name.empty())
42 continue;
43
44 if (name.back() != '/') {
45 std::unique_ptr<zip_file_t, decltype(&zip_fclose)> file{
46 zip_fopen_index(zip.get(), i, 0), zip_fclose};
47
48 std::vector<u8> buf(stat.size);
49 if (zip_fread(file.get(), buf.data(), buf.size()) != buf.size())
50 return nullptr;
51
52 const auto parts = FileUtil::SplitPathComponents(stat.name);
53 const auto new_file = std::make_shared<VectorVfsFile>(buf, parts.back());
54
55 std::shared_ptr<VectorVfsDirectory> dtrv = out;
56 for (std::size_t j = 0; j < parts.size() - 1; ++j) {
57 if (dtrv == nullptr)
58 return nullptr;
59 const auto subdir = dtrv->GetSubdirectory(parts[j]);
60 if (subdir == nullptr) {
61 const auto temp = std::make_shared<VectorVfsDirectory>(
62 std::vector<VirtualFile>{}, std::vector<VirtualDir>{}, parts[j]);
63 dtrv->AddDirectory(temp);
64 dtrv = temp;
65 } else {
66 dtrv = std::dynamic_pointer_cast<VectorVfsDirectory>(subdir);
67 }
68 }
69
70 if (dtrv == nullptr)
71 return nullptr;
72 dtrv->AddFile(new_file);
73 }
74 }
75
76 return out;
77}
78
79} // namespace FileSys
diff --git a/src/core/file_sys/vfs_libzip.h b/src/core/file_sys/vfs_libzip.h
new file mode 100644
index 000000000..f68af576a
--- /dev/null
+++ b/src/core/file_sys/vfs_libzip.h
@@ -0,0 +1,13 @@
1// Copyright 2019 yuzu emulator team
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5#pragma once
6
7#include "core/file_sys/vfs_types.h"
8
9namespace FileSys {
10
11VirtualDir ExtractZIP(VirtualFile zip);
12
13} // namespace FileSys
diff --git a/src/core/hle/service/am/am.cpp b/src/core/hle/service/am/am.cpp
index 797c9a06f..34409e0c3 100644
--- a/src/core/hle/service/am/am.cpp
+++ b/src/core/hle/service/am/am.cpp
@@ -31,6 +31,7 @@
31#include "core/hle/service/am/tcap.h" 31#include "core/hle/service/am/tcap.h"
32#include "core/hle/service/apm/controller.h" 32#include "core/hle/service/apm/controller.h"
33#include "core/hle/service/apm/interface.h" 33#include "core/hle/service/apm/interface.h"
34#include "core/hle/service/bcat/backend/backend.h"
34#include "core/hle/service/filesystem/filesystem.h" 35#include "core/hle/service/filesystem/filesystem.h"
35#include "core/hle/service/ns/ns.h" 36#include "core/hle/service/ns/ns.h"
36#include "core/hle/service/nvflinger/nvflinger.h" 37#include "core/hle/service/nvflinger/nvflinger.h"
@@ -46,15 +47,20 @@ constexpr ResultCode ERR_NO_DATA_IN_CHANNEL{ErrorModule::AM, 0x2};
46constexpr ResultCode ERR_NO_MESSAGES{ErrorModule::AM, 0x3}; 47constexpr ResultCode ERR_NO_MESSAGES{ErrorModule::AM, 0x3};
47constexpr ResultCode ERR_SIZE_OUT_OF_BOUNDS{ErrorModule::AM, 0x1F7}; 48constexpr ResultCode ERR_SIZE_OUT_OF_BOUNDS{ErrorModule::AM, 0x1F7};
48 49
49constexpr u32 POP_LAUNCH_PARAMETER_MAGIC = 0xC79497CA; 50enum class LaunchParameterKind : u32 {
51 ApplicationSpecific = 1,
52 AccountPreselectedUser = 2,
53};
54
55constexpr u32 LAUNCH_PARAMETER_ACCOUNT_PRESELECTED_USER_MAGIC = 0xC79497CA;
50 56
51struct LaunchParameters { 57struct LaunchParameterAccountPreselectedUser {
52 u32_le magic; 58 u32_le magic;
53 u32_le is_account_selected; 59 u32_le is_account_selected;
54 u128 current_user; 60 u128 current_user;
55 INSERT_PADDING_BYTES(0x70); 61 INSERT_PADDING_BYTES(0x70);
56}; 62};
57static_assert(sizeof(LaunchParameters) == 0x88); 63static_assert(sizeof(LaunchParameterAccountPreselectedUser) == 0x88);
58 64
59IWindowController::IWindowController(Core::System& system_) 65IWindowController::IWindowController(Core::System& system_)
60 : ServiceFramework("IWindowController"), system{system_} { 66 : ServiceFramework("IWindowController"), system{system_} {
@@ -1128,26 +1134,55 @@ void IApplicationFunctions::EndBlockingHomeButton(Kernel::HLERequestContext& ctx
1128} 1134}
1129 1135
1130void IApplicationFunctions::PopLaunchParameter(Kernel::HLERequestContext& ctx) { 1136void IApplicationFunctions::PopLaunchParameter(Kernel::HLERequestContext& ctx) {
1131 LOG_DEBUG(Service_AM, "called"); 1137 IPC::RequestParser rp{ctx};
1138 const auto kind = rp.PopEnum<LaunchParameterKind>();
1132 1139
1133 LaunchParameters params{}; 1140 LOG_DEBUG(Service_AM, "called, kind={:08X}", static_cast<u8>(kind));
1134 1141
1135 params.magic = POP_LAUNCH_PARAMETER_MAGIC; 1142 if (kind == LaunchParameterKind::ApplicationSpecific && !launch_popped_application_specific) {
1136 params.is_account_selected = 1; 1143 const auto backend = BCAT::CreateBackendFromSettings(
1144 [this](u64 tid) { return system.GetFileSystemController().GetBCATDirectory(tid); });
1145 const auto build_id_full = Core::System::GetInstance().GetCurrentProcessBuildID();
1146 u64 build_id{};
1147 std::memcpy(&build_id, build_id_full.data(), sizeof(u64));
1137 1148
1138 Account::ProfileManager profile_manager{}; 1149 const auto data =
1139 const auto uuid = profile_manager.GetUser(Settings::values.current_user); 1150 backend->GetLaunchParameter({Core::CurrentProcess()->GetTitleID(), build_id});
1140 ASSERT(uuid);
1141 params.current_user = uuid->uuid;
1142 1151
1143 IPC::ResponseBuilder rb{ctx, 2, 0, 1}; 1152 if (data.has_value()) {
1153 IPC::ResponseBuilder rb{ctx, 2, 0, 1};
1154 rb.Push(RESULT_SUCCESS);
1155 rb.PushIpcInterface<AM::IStorage>(*data);
1156 launch_popped_application_specific = true;
1157 return;
1158 }
1159 } else if (kind == LaunchParameterKind::AccountPreselectedUser &&
1160 !launch_popped_account_preselect) {
1161 LaunchParameterAccountPreselectedUser params{};
1144 1162
1145 rb.Push(RESULT_SUCCESS); 1163 params.magic = LAUNCH_PARAMETER_ACCOUNT_PRESELECTED_USER_MAGIC;
1164 params.is_account_selected = 1;
1146 1165
1147 std::vector<u8> buffer(sizeof(LaunchParameters)); 1166 Account::ProfileManager profile_manager{};
1148 std::memcpy(buffer.data(), &params, buffer.size()); 1167 const auto uuid = profile_manager.GetUser(Settings::values.current_user);
1168 ASSERT(uuid);
1169 params.current_user = uuid->uuid;
1149 1170
1150 rb.PushIpcInterface<AM::IStorage>(buffer); 1171 IPC::ResponseBuilder rb{ctx, 2, 0, 1};
1172
1173 rb.Push(RESULT_SUCCESS);
1174
1175 std::vector<u8> buffer(sizeof(LaunchParameterAccountPreselectedUser));
1176 std::memcpy(buffer.data(), &params, buffer.size());
1177
1178 rb.PushIpcInterface<AM::IStorage>(buffer);
1179 launch_popped_account_preselect = true;
1180 return;
1181 }
1182
1183 LOG_ERROR(Service_AM, "Attempted to load launch parameter but none was found!");
1184 IPC::ResponseBuilder rb{ctx, 2};
1185 rb.Push(ERR_NO_DATA_IN_CHANNEL);
1151} 1186}
1152 1187
1153void IApplicationFunctions::CreateApplicationAndRequestToStartForQuest( 1188void IApplicationFunctions::CreateApplicationAndRequestToStartForQuest(
diff --git a/src/core/hle/service/am/am.h b/src/core/hle/service/am/am.h
index a3baeb673..9169eb2bd 100644
--- a/src/core/hle/service/am/am.h
+++ b/src/core/hle/service/am/am.h
@@ -255,6 +255,8 @@ private:
255 void EnableApplicationCrashReport(Kernel::HLERequestContext& ctx); 255 void EnableApplicationCrashReport(Kernel::HLERequestContext& ctx);
256 void GetGpuErrorDetectedSystemEvent(Kernel::HLERequestContext& ctx); 256 void GetGpuErrorDetectedSystemEvent(Kernel::HLERequestContext& ctx);
257 257
258 bool launch_popped_application_specific = false;
259 bool launch_popped_account_preselect = false;
258 Kernel::EventPair gpu_error_detected_event; 260 Kernel::EventPair gpu_error_detected_event;
259 Core::System& system; 261 Core::System& system;
260}; 262};
diff --git a/src/core/hle/service/am/applets/applets.cpp b/src/core/hle/service/am/applets/applets.cpp
index d2e35362f..720fe766f 100644
--- a/src/core/hle/service/am/applets/applets.cpp
+++ b/src/core/hle/service/am/applets/applets.cpp
@@ -157,6 +157,10 @@ AppletManager::AppletManager(Core::System& system_) : system{system_} {}
157 157
158AppletManager::~AppletManager() = default; 158AppletManager::~AppletManager() = default;
159 159
160const AppletFrontendSet& AppletManager::GetAppletFrontendSet() const {
161 return frontend;
162}
163
160void AppletManager::SetAppletFrontendSet(AppletFrontendSet set) { 164void AppletManager::SetAppletFrontendSet(AppletFrontendSet set) {
161 if (set.parental_controls != nullptr) 165 if (set.parental_controls != nullptr)
162 frontend.parental_controls = std::move(set.parental_controls); 166 frontend.parental_controls = std::move(set.parental_controls);
diff --git a/src/core/hle/service/am/applets/applets.h b/src/core/hle/service/am/applets/applets.h
index 764c3418c..226be88b1 100644
--- a/src/core/hle/service/am/applets/applets.h
+++ b/src/core/hle/service/am/applets/applets.h
@@ -190,6 +190,8 @@ public:
190 explicit AppletManager(Core::System& system_); 190 explicit AppletManager(Core::System& system_);
191 ~AppletManager(); 191 ~AppletManager();
192 192
193 const AppletFrontendSet& GetAppletFrontendSet() const;
194
193 void SetAppletFrontendSet(AppletFrontendSet set); 195 void SetAppletFrontendSet(AppletFrontendSet set);
194 void SetDefaultAppletFrontendSet(); 196 void SetDefaultAppletFrontendSet();
195 void SetDefaultAppletsIfMissing(); 197 void SetDefaultAppletsIfMissing();
diff --git a/src/core/hle/service/bcat/backend/backend.cpp b/src/core/hle/service/bcat/backend/backend.cpp
new file mode 100644
index 000000000..9b677debe
--- /dev/null
+++ b/src/core/hle/service/bcat/backend/backend.cpp
@@ -0,0 +1,136 @@
1// Copyright 2019 yuzu emulator team
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5#include "common/hex_util.h"
6#include "common/logging/log.h"
7#include "core/core.h"
8#include "core/hle/lock.h"
9#include "core/hle/service/bcat/backend/backend.h"
10
11namespace Service::BCAT {
12
13ProgressServiceBackend::ProgressServiceBackend(std::string event_name) : impl{} {
14 auto& kernel{Core::System::GetInstance().Kernel()};
15 event = Kernel::WritableEvent::CreateEventPair(
16 kernel, Kernel::ResetType::Automatic, "ProgressServiceBackend:UpdateEvent:" + event_name);
17}
18
19Kernel::SharedPtr<Kernel::ReadableEvent> ProgressServiceBackend::GetEvent() {
20 return event.readable;
21}
22
23DeliveryCacheProgressImpl& ProgressServiceBackend::GetImpl() {
24 return impl;
25}
26
27void ProgressServiceBackend::SetNeedHLELock(bool need) {
28 need_hle_lock = need;
29}
30
31void ProgressServiceBackend::SetTotalSize(u64 size) {
32 impl.total_bytes = size;
33 SignalUpdate();
34}
35
36void ProgressServiceBackend::StartConnecting() {
37 impl.status = DeliveryCacheProgressImpl::Status::Connecting;
38 SignalUpdate();
39}
40
41void ProgressServiceBackend::StartProcessingDataList() {
42 impl.status = DeliveryCacheProgressImpl::Status::ProcessingDataList;
43 SignalUpdate();
44}
45
46void ProgressServiceBackend::StartDownloadingFile(std::string_view dir_name,
47 std::string_view file_name, u64 file_size) {
48 impl.status = DeliveryCacheProgressImpl::Status::Downloading;
49 impl.current_downloaded_bytes = 0;
50 impl.current_total_bytes = file_size;
51 std::memcpy(impl.current_directory.data(), dir_name.data(),
52 std::min<u64>(dir_name.size(), 0x31ull));
53 std::memcpy(impl.current_file.data(), file_name.data(),
54 std::min<u64>(file_name.size(), 0x31ull));
55 SignalUpdate();
56}
57
58void ProgressServiceBackend::UpdateFileProgress(u64 downloaded) {
59 impl.current_downloaded_bytes = downloaded;
60 SignalUpdate();
61}
62
63void ProgressServiceBackend::FinishDownloadingFile() {
64 impl.total_downloaded_bytes += impl.current_total_bytes;
65 SignalUpdate();
66}
67
68void ProgressServiceBackend::CommitDirectory(std::string_view dir_name) {
69 impl.status = DeliveryCacheProgressImpl::Status::Committing;
70 impl.current_file.fill(0);
71 impl.current_downloaded_bytes = 0;
72 impl.current_total_bytes = 0;
73 std::memcpy(impl.current_directory.data(), dir_name.data(),
74 std::min<u64>(dir_name.size(), 0x31ull));
75 SignalUpdate();
76}
77
78void ProgressServiceBackend::FinishDownload(ResultCode result) {
79 impl.total_downloaded_bytes = impl.total_bytes;
80 impl.status = DeliveryCacheProgressImpl::Status::Done;
81 impl.result = result;
82 SignalUpdate();
83}
84
85void ProgressServiceBackend::SignalUpdate() const {
86 if (need_hle_lock) {
87 std::lock_guard<std::recursive_mutex> lock(HLE::g_hle_lock);
88 event.writable->Signal();
89 } else {
90 event.writable->Signal();
91 }
92}
93
94Backend::Backend(DirectoryGetter getter) : dir_getter(std::move(getter)) {}
95
96Backend::~Backend() = default;
97
98NullBackend::NullBackend(const DirectoryGetter& getter) : Backend(std::move(getter)) {}
99
100NullBackend::~NullBackend() = default;
101
102bool NullBackend::Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) {
103 LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, build_id={:016X}", title.title_id,
104 title.build_id);
105
106 progress.FinishDownload(RESULT_SUCCESS);
107 return true;
108}
109
110bool NullBackend::SynchronizeDirectory(TitleIDVersion title, std::string name,
111 ProgressServiceBackend& progress) {
112 LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, build_id={:016X}, name={}", title.title_id,
113 title.build_id, name);
114
115 progress.FinishDownload(RESULT_SUCCESS);
116 return true;
117}
118
119bool NullBackend::Clear(u64 title_id) {
120 LOG_DEBUG(Service_BCAT, "called, title_id={:016X}");
121
122 return true;
123}
124
125void NullBackend::SetPassphrase(u64 title_id, const Passphrase& passphrase) {
126 LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, passphrase = {}", title_id,
127 Common::HexToString(passphrase));
128}
129
130std::optional<std::vector<u8>> NullBackend::GetLaunchParameter(TitleIDVersion title) {
131 LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, build_id={:016X}", title.title_id,
132 title.build_id);
133 return std::nullopt;
134}
135
136} // namespace Service::BCAT
diff --git a/src/core/hle/service/bcat/backend/backend.h b/src/core/hle/service/bcat/backend/backend.h
new file mode 100644
index 000000000..3f5d8b5dd
--- /dev/null
+++ b/src/core/hle/service/bcat/backend/backend.h
@@ -0,0 +1,147 @@
1// Copyright 2019 yuzu emulator team
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5#pragma once
6
7#include <functional>
8#include <optional>
9#include "common/common_types.h"
10#include "core/file_sys/vfs_types.h"
11#include "core/hle/kernel/readable_event.h"
12#include "core/hle/kernel/writable_event.h"
13#include "core/hle/result.h"
14
15namespace Service::BCAT {
16
17struct DeliveryCacheProgressImpl;
18
19using DirectoryGetter = std::function<FileSys::VirtualDir(u64)>;
20using Passphrase = std::array<u8, 0x20>;
21
22struct TitleIDVersion {
23 u64 title_id;
24 u64 build_id;
25};
26
27using DirectoryName = std::array<char, 0x20>;
28using FileName = std::array<char, 0x20>;
29
30struct DeliveryCacheProgressImpl {
31 enum class Status : s32 {
32 None = 0x0,
33 Queued = 0x1,
34 Connecting = 0x2,
35 ProcessingDataList = 0x3,
36 Downloading = 0x4,
37 Committing = 0x5,
38 Done = 0x9,
39 };
40
41 Status status;
42 ResultCode result = RESULT_SUCCESS;
43 DirectoryName current_directory;
44 FileName current_file;
45 s64 current_downloaded_bytes; ///< Bytes downloaded on current file.
46 s64 current_total_bytes; ///< Bytes total on current file.
47 s64 total_downloaded_bytes; ///< Bytes downloaded on overall download.
48 s64 total_bytes; ///< Bytes total on overall download.
49 INSERT_PADDING_BYTES(
50 0x198); ///< Appears to be unused in official code, possibly reserved for future use.
51};
52static_assert(sizeof(DeliveryCacheProgressImpl) == 0x200,
53 "DeliveryCacheProgressImpl has incorrect size.");
54
55// A class to manage the signalling to the game about BCAT download progress.
56// Some of this class is implemented in module.cpp to avoid exposing the implementation structure.
57class ProgressServiceBackend {
58 friend class IBcatService;
59
60public:
61 // Clients should call this with true if any of the functions are going to be called from a
62 // non-HLE thread and this class need to lock the hle mutex. (default is false)
63 void SetNeedHLELock(bool need);
64
65 // Sets the number of bytes total in the entire download.
66 void SetTotalSize(u64 size);
67
68 // Notifies the application that the backend has started connecting to the server.
69 void StartConnecting();
70 // Notifies the application that the backend has begun accumulating and processing metadata.
71 void StartProcessingDataList();
72
73 // Notifies the application that a file is starting to be downloaded.
74 void StartDownloadingFile(std::string_view dir_name, std::string_view file_name, u64 file_size);
75 // Updates the progress of the current file to the size passed.
76 void UpdateFileProgress(u64 downloaded);
77 // Notifies the application that the current file has completed download.
78 void FinishDownloadingFile();
79
80 // Notifies the application that all files in this directory have completed and are being
81 // finalized.
82 void CommitDirectory(std::string_view dir_name);
83
84 // Notifies the application that the operation completed with result code result.
85 void FinishDownload(ResultCode result);
86
87private:
88 explicit ProgressServiceBackend(std::string event_name);
89
90 Kernel::SharedPtr<Kernel::ReadableEvent> GetEvent();
91 DeliveryCacheProgressImpl& GetImpl();
92
93 void SignalUpdate() const;
94
95 DeliveryCacheProgressImpl impl;
96 Kernel::EventPair event;
97 bool need_hle_lock = false;
98};
99
100// A class representing an abstract backend for BCAT functionality.
101class Backend {
102public:
103 explicit Backend(DirectoryGetter getter);
104 virtual ~Backend();
105
106 // Called when the backend is needed to synchronize the data for the game with title ID and
107 // version in title. A ProgressServiceBackend object is provided to alert the application of
108 // status.
109 virtual bool Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) = 0;
110 // Very similar to Synchronize, but only for the directory provided. Backends should not alter
111 // the data for any other directories.
112 virtual bool SynchronizeDirectory(TitleIDVersion title, std::string name,
113 ProgressServiceBackend& progress) = 0;
114
115 // Removes all cached data associated with title id provided.
116 virtual bool Clear(u64 title_id) = 0;
117
118 // Sets the BCAT Passphrase to be used with the associated title ID.
119 virtual void SetPassphrase(u64 title_id, const Passphrase& passphrase) = 0;
120
121 // Gets the launch parameter used by AM associated with the title ID and version provided.
122 virtual std::optional<std::vector<u8>> GetLaunchParameter(TitleIDVersion title) = 0;
123
124protected:
125 DirectoryGetter dir_getter;
126};
127
128// A backend of BCAT that provides no operation.
129class NullBackend : public Backend {
130public:
131 explicit NullBackend(const DirectoryGetter& getter);
132 ~NullBackend() override;
133
134 bool Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) override;
135 bool SynchronizeDirectory(TitleIDVersion title, std::string name,
136 ProgressServiceBackend& progress) override;
137
138 bool Clear(u64 title_id) override;
139
140 void SetPassphrase(u64 title_id, const Passphrase& passphrase) override;
141
142 std::optional<std::vector<u8>> GetLaunchParameter(TitleIDVersion title) override;
143};
144
145std::unique_ptr<Backend> CreateBackendFromSettings(DirectoryGetter getter);
146
147} // namespace Service::BCAT
diff --git a/src/core/hle/service/bcat/backend/boxcat.cpp b/src/core/hle/service/bcat/backend/boxcat.cpp
new file mode 100644
index 000000000..e6ee0810b
--- /dev/null
+++ b/src/core/hle/service/bcat/backend/boxcat.cpp
@@ -0,0 +1,503 @@
1// Copyright 2019 yuzu emulator team
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5#include <fmt/ostream.h>
6#include <httplib.h>
7#include <json.hpp>
8#include <mbedtls/sha256.h>
9#include "common/hex_util.h"
10#include "common/logging/backend.h"
11#include "common/logging/log.h"
12#include "core/core.h"
13#include "core/file_sys/vfs.h"
14#include "core/file_sys/vfs_libzip.h"
15#include "core/file_sys/vfs_vector.h"
16#include "core/frontend/applets/error.h"
17#include "core/hle/service/am/applets/applets.h"
18#include "core/hle/service/bcat/backend/boxcat.h"
19#include "core/settings.h"
20
21namespace {
22
23// Prevents conflicts with windows macro called CreateFile
24FileSys::VirtualFile VfsCreateFileWrap(FileSys::VirtualDir dir, std::string_view name) {
25 return dir->CreateFile(name);
26}
27
28// Prevents conflicts with windows macro called DeleteFile
29bool VfsDeleteFileWrap(FileSys::VirtualDir dir, std::string_view name) {
30 return dir->DeleteFile(name);
31}
32
33} // Anonymous namespace
34
35namespace Service::BCAT {
36
37constexpr ResultCode ERROR_GENERAL_BCAT_FAILURE{ErrorModule::BCAT, 1};
38
39constexpr char BOXCAT_HOSTNAME[] = "api.yuzu-emu.org";
40
41// Formatted using fmt with arg[0] = hex title id
42constexpr char BOXCAT_PATHNAME_DATA[] = "/game-assets/{:016X}/boxcat";
43constexpr char BOXCAT_PATHNAME_LAUNCHPARAM[] = "/game-assets/{:016X}/launchparam";
44
45constexpr char BOXCAT_PATHNAME_EVENTS[] = "/game-assets/boxcat/events";
46
47constexpr char BOXCAT_API_VERSION[] = "1";
48constexpr char BOXCAT_CLIENT_TYPE[] = "yuzu";
49
50// HTTP status codes for Boxcat
51enum class ResponseStatus {
52 Ok = 200, ///< Operation completed successfully.
53 BadClientVersion = 301, ///< The Boxcat-Client-Version doesn't match the server.
54 NoUpdate = 304, ///< The digest provided would match the new data, no need to update.
55 NoMatchTitleId = 404, ///< The title ID provided doesn't have a boxcat implementation.
56 NoMatchBuildId = 406, ///< The build ID provided is blacklisted (potentially because of format
57 ///< issues or whatnot) and has no data.
58};
59
60enum class DownloadResult {
61 Success = 0,
62 NoResponse,
63 GeneralWebError,
64 NoMatchTitleId,
65 NoMatchBuildId,
66 InvalidContentType,
67 GeneralFSError,
68 BadClientVersion,
69};
70
71constexpr std::array<const char*, 8> DOWNLOAD_RESULT_LOG_MESSAGES{
72 "Success",
73 "There was no response from the server.",
74 "There was a general web error code returned from the server.",
75 "The title ID of the current game doesn't have a boxcat implementation. If you believe an "
76 "implementation should be added, contact yuzu support.",
77 "The build ID of the current version of the game is marked as incompatible with the current "
78 "BCAT distribution. Try upgrading or downgrading your game version or contacting yuzu support.",
79 "The content type of the web response was invalid.",
80 "There was a general filesystem error while saving the zip file.",
81 "The server is either too new or too old to serve the request. Try using the latest version of "
82 "an official release of yuzu.",
83};
84
85std::ostream& operator<<(std::ostream& os, DownloadResult result) {
86 return os << DOWNLOAD_RESULT_LOG_MESSAGES.at(static_cast<std::size_t>(result));
87}
88
89constexpr u32 PORT = 443;
90constexpr u32 TIMEOUT_SECONDS = 30;
91constexpr u64 VFS_COPY_BLOCK_SIZE = 1ull << 24; // 4MB
92
93namespace {
94
95std::string GetBINFilePath(u64 title_id) {
96 return fmt::format("{}bcat/{:016X}/launchparam.bin",
97 FileUtil::GetUserPath(FileUtil::UserPath::CacheDir), title_id);
98}
99
100std::string GetZIPFilePath(u64 title_id) {
101 return fmt::format("{}bcat/{:016X}/data.zip",
102 FileUtil::GetUserPath(FileUtil::UserPath::CacheDir), title_id);
103}
104
105// If the error is something the user should know about (build ID mismatch, bad client version),
106// display an error.
107void HandleDownloadDisplayResult(DownloadResult res) {
108 if (res == DownloadResult::Success || res == DownloadResult::NoResponse ||
109 res == DownloadResult::GeneralWebError || res == DownloadResult::GeneralFSError ||
110 res == DownloadResult::NoMatchTitleId || res == DownloadResult::InvalidContentType) {
111 return;
112 }
113
114 const auto& frontend{Core::System::GetInstance().GetAppletManager().GetAppletFrontendSet()};
115 frontend.error->ShowCustomErrorText(
116 ResultCode(-1), "There was an error while attempting to use Boxcat.",
117 DOWNLOAD_RESULT_LOG_MESSAGES[static_cast<std::size_t>(res)], [] {});
118}
119
120bool VfsRawCopyProgress(FileSys::VirtualFile src, FileSys::VirtualFile dest,
121 std::string_view dir_name, ProgressServiceBackend& progress,
122 std::size_t block_size = 0x1000) {
123 if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable())
124 return false;
125 if (!dest->Resize(src->GetSize()))
126 return false;
127
128 progress.StartDownloadingFile(dir_name, src->GetName(), src->GetSize());
129
130 std::vector<u8> temp(std::min(block_size, src->GetSize()));
131 for (std::size_t i = 0; i < src->GetSize(); i += block_size) {
132 const auto read = std::min(block_size, src->GetSize() - i);
133
134 if (src->Read(temp.data(), read, i) != read) {
135 return false;
136 }
137
138 if (dest->Write(temp.data(), read, i) != read) {
139 return false;
140 }
141
142 progress.UpdateFileProgress(i);
143 }
144
145 progress.FinishDownloadingFile();
146
147 return true;
148}
149
150bool VfsRawCopyDProgressSingle(FileSys::VirtualDir src, FileSys::VirtualDir dest,
151 ProgressServiceBackend& progress, std::size_t block_size = 0x1000) {
152 if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable())
153 return false;
154
155 for (const auto& file : src->GetFiles()) {
156 const auto out_file = VfsCreateFileWrap(dest, file->GetName());
157 if (!VfsRawCopyProgress(file, out_file, src->GetName(), progress, block_size)) {
158 return false;
159 }
160 }
161 progress.CommitDirectory(src->GetName());
162
163 return true;
164}
165
166bool VfsRawCopyDProgress(FileSys::VirtualDir src, FileSys::VirtualDir dest,
167 ProgressServiceBackend& progress, std::size_t block_size = 0x1000) {
168 if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable())
169 return false;
170
171 for (const auto& dir : src->GetSubdirectories()) {
172 const auto out = dest->CreateSubdirectory(dir->GetName());
173 if (!VfsRawCopyDProgressSingle(dir, out, progress, block_size)) {
174 return false;
175 }
176 }
177
178 return true;
179}
180
181} // Anonymous namespace
182
183class Boxcat::Client {
184public:
185 Client(std::string path, u64 title_id, u64 build_id)
186 : path(std::move(path)), title_id(title_id), build_id(build_id) {}
187
188 DownloadResult DownloadDataZip() {
189 return DownloadInternal(fmt::format(BOXCAT_PATHNAME_DATA, title_id), TIMEOUT_SECONDS,
190 "application/zip");
191 }
192
193 DownloadResult DownloadLaunchParam() {
194 return DownloadInternal(fmt::format(BOXCAT_PATHNAME_LAUNCHPARAM, title_id),
195 TIMEOUT_SECONDS / 3, "application/octet-stream");
196 }
197
198private:
199 DownloadResult DownloadInternal(const std::string& resolved_path, u32 timeout_seconds,
200 const std::string& content_type_name) {
201 if (client == nullptr) {
202 client = std::make_unique<httplib::SSLClient>(BOXCAT_HOSTNAME, PORT, timeout_seconds);
203 }
204
205 httplib::Headers headers{
206 {std::string("Game-Assets-API-Version"), std::string(BOXCAT_API_VERSION)},
207 {std::string("Boxcat-Client-Type"), std::string(BOXCAT_CLIENT_TYPE)},
208 {std::string("Game-Build-Id"), fmt::format("{:016X}", build_id)},
209 };
210
211 if (FileUtil::Exists(path)) {
212 FileUtil::IOFile file{path, "rb"};
213 if (file.IsOpen()) {
214 std::vector<u8> bytes(file.GetSize());
215 file.ReadBytes(bytes.data(), bytes.size());
216 const auto digest = DigestFile(bytes);
217 headers.insert({std::string("If-None-Match"), Common::HexToString(digest, false)});
218 }
219 }
220
221 const auto response = client->Get(resolved_path.c_str(), headers);
222 if (response == nullptr)
223 return DownloadResult::NoResponse;
224
225 if (response->status == static_cast<int>(ResponseStatus::NoUpdate))
226 return DownloadResult::Success;
227 if (response->status == static_cast<int>(ResponseStatus::BadClientVersion))
228 return DownloadResult::BadClientVersion;
229 if (response->status == static_cast<int>(ResponseStatus::NoMatchTitleId))
230 return DownloadResult::NoMatchTitleId;
231 if (response->status == static_cast<int>(ResponseStatus::NoMatchBuildId))
232 return DownloadResult::NoMatchBuildId;
233 if (response->status != static_cast<int>(ResponseStatus::Ok))
234 return DownloadResult::GeneralWebError;
235
236 const auto content_type = response->headers.find("content-type");
237 if (content_type == response->headers.end() ||
238 content_type->second.find(content_type_name) == std::string::npos) {
239 return DownloadResult::InvalidContentType;
240 }
241
242 FileUtil::CreateFullPath(path);
243 FileUtil::IOFile file{path, "wb"};
244 if (!file.IsOpen())
245 return DownloadResult::GeneralFSError;
246 if (!file.Resize(response->body.size()))
247 return DownloadResult::GeneralFSError;
248 if (file.WriteBytes(response->body.data(), response->body.size()) != response->body.size())
249 return DownloadResult::GeneralFSError;
250
251 return DownloadResult::Success;
252 }
253
254 using Digest = std::array<u8, 0x20>;
255 static Digest DigestFile(std::vector<u8> bytes) {
256 Digest out{};
257 mbedtls_sha256(bytes.data(), bytes.size(), out.data(), 0);
258 return out;
259 }
260
261 std::unique_ptr<httplib::Client> client;
262 std::string path;
263 u64 title_id;
264 u64 build_id;
265};
266
267Boxcat::Boxcat(DirectoryGetter getter) : Backend(std::move(getter)) {}
268
269Boxcat::~Boxcat() = default;
270
271void SynchronizeInternal(DirectoryGetter dir_getter, TitleIDVersion title,
272 ProgressServiceBackend& progress,
273 std::optional<std::string> dir_name = {}) {
274 progress.SetNeedHLELock(true);
275
276 if (Settings::values.bcat_boxcat_local) {
277 LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping download.");
278 const auto dir = dir_getter(title.title_id);
279 if (dir)
280 progress.SetTotalSize(dir->GetSize());
281 progress.FinishDownload(RESULT_SUCCESS);
282 return;
283 }
284
285 const auto zip_path{GetZIPFilePath(title.title_id)};
286 Boxcat::Client client{zip_path, title.title_id, title.build_id};
287
288 progress.StartConnecting();
289
290 const auto res = client.DownloadDataZip();
291 if (res != DownloadResult::Success) {
292 LOG_ERROR(Service_BCAT, "Boxcat synchronization failed with error '{}'!", res);
293
294 if (res == DownloadResult::NoMatchBuildId || res == DownloadResult::NoMatchTitleId) {
295 FileUtil::Delete(zip_path);
296 }
297
298 HandleDownloadDisplayResult(res);
299 progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
300 return;
301 }
302
303 progress.StartProcessingDataList();
304
305 FileUtil::IOFile zip{zip_path, "rb"};
306 const auto size = zip.GetSize();
307 std::vector<u8> bytes(size);
308 if (!zip.IsOpen() || size == 0 || zip.ReadBytes(bytes.data(), bytes.size()) != bytes.size()) {
309 LOG_ERROR(Service_BCAT, "Boxcat failed to read ZIP file at path '{}'!", zip_path);
310 progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
311 return;
312 }
313
314 const auto extracted = FileSys::ExtractZIP(std::make_shared<FileSys::VectorVfsFile>(bytes));
315 if (extracted == nullptr) {
316 LOG_ERROR(Service_BCAT, "Boxcat failed to extract ZIP file!");
317 progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
318 return;
319 }
320
321 if (dir_name == std::nullopt) {
322 progress.SetTotalSize(extracted->GetSize());
323
324 const auto target_dir = dir_getter(title.title_id);
325 if (target_dir == nullptr || !VfsRawCopyDProgress(extracted, target_dir, progress)) {
326 LOG_ERROR(Service_BCAT, "Boxcat failed to copy extracted ZIP to target directory!");
327 progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
328 return;
329 }
330 } else {
331 const auto target_dir = dir_getter(title.title_id);
332 if (target_dir == nullptr) {
333 LOG_ERROR(Service_BCAT, "Boxcat failed to get directory for title ID!");
334 progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
335 return;
336 }
337
338 const auto target_sub = target_dir->GetSubdirectory(*dir_name);
339 const auto source_sub = extracted->GetSubdirectory(*dir_name);
340
341 progress.SetTotalSize(source_sub->GetSize());
342
343 std::vector<std::string> filenames;
344 {
345 const auto files = target_sub->GetFiles();
346 std::transform(files.begin(), files.end(), std::back_inserter(filenames),
347 [](const auto& vfile) { return vfile->GetName(); });
348 }
349
350 for (const auto& filename : filenames) {
351 VfsDeleteFileWrap(target_sub, filename);
352 }
353
354 if (target_sub == nullptr || source_sub == nullptr ||
355 !VfsRawCopyDProgressSingle(source_sub, target_sub, progress)) {
356 LOG_ERROR(Service_BCAT, "Boxcat failed to copy extracted ZIP to target directory!");
357 progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
358 return;
359 }
360 }
361
362 progress.FinishDownload(RESULT_SUCCESS);
363}
364
365bool Boxcat::Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) {
366 is_syncing.exchange(true);
367 std::thread([this, title, &progress] { SynchronizeInternal(dir_getter, title, progress); })
368 .detach();
369 return true;
370}
371
372bool Boxcat::SynchronizeDirectory(TitleIDVersion title, std::string name,
373 ProgressServiceBackend& progress) {
374 is_syncing.exchange(true);
375 std::thread(
376 [this, title, name, &progress] { SynchronizeInternal(dir_getter, title, progress, name); })
377 .detach();
378 return true;
379}
380
381bool Boxcat::Clear(u64 title_id) {
382 if (Settings::values.bcat_boxcat_local) {
383 LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping clear.");
384 return true;
385 }
386
387 const auto dir = dir_getter(title_id);
388
389 std::vector<std::string> dirnames;
390
391 for (const auto& subdir : dir->GetSubdirectories())
392 dirnames.push_back(subdir->GetName());
393
394 for (const auto& subdir : dirnames) {
395 if (!dir->DeleteSubdirectoryRecursive(subdir))
396 return false;
397 }
398
399 return true;
400}
401
402void Boxcat::SetPassphrase(u64 title_id, const Passphrase& passphrase) {
403 LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, passphrase={}", title_id,
404 Common::HexToString(passphrase));
405}
406
407std::optional<std::vector<u8>> Boxcat::GetLaunchParameter(TitleIDVersion title) {
408 const auto path{GetBINFilePath(title.title_id)};
409
410 if (Settings::values.bcat_boxcat_local) {
411 LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping download.");
412 } else {
413 Boxcat::Client client{path, title.title_id, title.build_id};
414
415 const auto res = client.DownloadLaunchParam();
416 if (res != DownloadResult::Success) {
417 LOG_ERROR(Service_BCAT, "Boxcat synchronization failed with error '{}'!", res);
418
419 if (res == DownloadResult::NoMatchBuildId || res == DownloadResult::NoMatchTitleId) {
420 FileUtil::Delete(path);
421 }
422
423 HandleDownloadDisplayResult(res);
424 return std::nullopt;
425 }
426 }
427
428 FileUtil::IOFile bin{path, "rb"};
429 const auto size = bin.GetSize();
430 std::vector<u8> bytes(size);
431 if (!bin.IsOpen() || size == 0 || bin.ReadBytes(bytes.data(), bytes.size()) != bytes.size()) {
432 LOG_ERROR(Service_BCAT, "Boxcat failed to read launch parameter binary at path '{}'!",
433 path);
434 return std::nullopt;
435 }
436
437 return bytes;
438}
439
440Boxcat::StatusResult Boxcat::GetStatus(std::optional<std::string>& global,
441 std::map<std::string, EventStatus>& games) {
442 httplib::SSLClient client{BOXCAT_HOSTNAME, static_cast<int>(PORT),
443 static_cast<int>(TIMEOUT_SECONDS)};
444
445 httplib::Headers headers{
446 {std::string("Game-Assets-API-Version"), std::string(BOXCAT_API_VERSION)},
447 {std::string("Boxcat-Client-Type"), std::string(BOXCAT_CLIENT_TYPE)},
448 };
449
450 const auto response = client.Get(BOXCAT_PATHNAME_EVENTS, headers);
451 if (response == nullptr)
452 return StatusResult::Offline;
453
454 if (response->status == static_cast<int>(ResponseStatus::BadClientVersion))
455 return StatusResult::BadClientVersion;
456
457 try {
458 nlohmann::json json = nlohmann::json::parse(response->body);
459
460 if (!json["online"].get<bool>())
461 return StatusResult::Offline;
462
463 if (json["global"].is_null())
464 global = std::nullopt;
465 else
466 global = json["global"].get<std::string>();
467
468 if (json["games"].is_array()) {
469 for (const auto object : json["games"]) {
470 if (object.is_object() && object.find("name") != object.end()) {
471 EventStatus detail{};
472 if (object["header"].is_string()) {
473 detail.header = object["header"].get<std::string>();
474 } else {
475 detail.header = std::nullopt;
476 }
477
478 if (object["footer"].is_string()) {
479 detail.footer = object["footer"].get<std::string>();
480 } else {
481 detail.footer = std::nullopt;
482 }
483
484 if (object["events"].is_array()) {
485 for (const auto& event : object["events"]) {
486 if (!event.is_string())
487 continue;
488 detail.events.push_back(event.get<std::string>());
489 }
490 }
491
492 games.insert_or_assign(object["name"], std::move(detail));
493 }
494 }
495 }
496
497 return StatusResult::Success;
498 } catch (const nlohmann::json::parse_error& e) {
499 return StatusResult::ParseError;
500 }
501}
502
503} // namespace Service::BCAT
diff --git a/src/core/hle/service/bcat/backend/boxcat.h b/src/core/hle/service/bcat/backend/boxcat.h
new file mode 100644
index 000000000..601151189
--- /dev/null
+++ b/src/core/hle/service/bcat/backend/boxcat.h
@@ -0,0 +1,58 @@
1// Copyright 2019 yuzu emulator team
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5#pragma once
6
7#include <atomic>
8#include <map>
9#include <optional>
10#include "core/hle/service/bcat/backend/backend.h"
11
12namespace Service::BCAT {
13
14struct EventStatus {
15 std::optional<std::string> header;
16 std::optional<std::string> footer;
17 std::vector<std::string> events;
18};
19
20/// Boxcat is yuzu's custom backend implementation of Nintendo's BCAT service. It is free to use and
21/// doesn't require a switch or nintendo account. The content is controlled by the yuzu team.
22class Boxcat final : public Backend {
23 friend void SynchronizeInternal(DirectoryGetter dir_getter, TitleIDVersion title,
24 ProgressServiceBackend& progress,
25 std::optional<std::string> dir_name);
26
27public:
28 explicit Boxcat(DirectoryGetter getter);
29 ~Boxcat() override;
30
31 bool Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) override;
32 bool SynchronizeDirectory(TitleIDVersion title, std::string name,
33 ProgressServiceBackend& progress) override;
34
35 bool Clear(u64 title_id) override;
36
37 void SetPassphrase(u64 title_id, const Passphrase& passphrase) override;
38
39 std::optional<std::vector<u8>> GetLaunchParameter(TitleIDVersion title) override;
40
41 enum class StatusResult {
42 Success,
43 Offline,
44 ParseError,
45 BadClientVersion,
46 };
47
48 static StatusResult GetStatus(std::optional<std::string>& global,
49 std::map<std::string, EventStatus>& games);
50
51private:
52 std::atomic_bool is_syncing{false};
53
54 class Client;
55 std::unique_ptr<Client> client;
56};
57
58} // namespace Service::BCAT
diff --git a/src/core/hle/service/bcat/bcat.cpp b/src/core/hle/service/bcat/bcat.cpp
index 179aa4949..c2f946424 100644
--- a/src/core/hle/service/bcat/bcat.cpp
+++ b/src/core/hle/service/bcat/bcat.cpp
@@ -6,11 +6,15 @@
6 6
7namespace Service::BCAT { 7namespace Service::BCAT {
8 8
9BCAT::BCAT(std::shared_ptr<Module> module, const char* name) 9BCAT::BCAT(std::shared_ptr<Module> module, FileSystem::FileSystemController& fsc, const char* name)
10 : Module::Interface(std::move(module), name) { 10 : Module::Interface(std::move(module), fsc, name) {
11 // clang-format off
11 static const FunctionInfo functions[] = { 12 static const FunctionInfo functions[] = {
12 {0, &BCAT::CreateBcatService, "CreateBcatService"}, 13 {0, &BCAT::CreateBcatService, "CreateBcatService"},
14 {1, &BCAT::CreateDeliveryCacheStorageService, "CreateDeliveryCacheStorageService"},
15 {2, &BCAT::CreateDeliveryCacheStorageServiceWithApplicationId, "CreateDeliveryCacheStorageServiceWithApplicationId"},
13 }; 16 };
17 // clang-format on
14 RegisterHandlers(functions); 18 RegisterHandlers(functions);
15} 19}
16 20
diff --git a/src/core/hle/service/bcat/bcat.h b/src/core/hle/service/bcat/bcat.h
index 802bd689a..813073658 100644
--- a/src/core/hle/service/bcat/bcat.h
+++ b/src/core/hle/service/bcat/bcat.h
@@ -10,7 +10,8 @@ namespace Service::BCAT {
10 10
11class BCAT final : public Module::Interface { 11class BCAT final : public Module::Interface {
12public: 12public:
13 explicit BCAT(std::shared_ptr<Module> module, const char* name); 13 explicit BCAT(std::shared_ptr<Module> module, FileSystem::FileSystemController& fsc,
14 const char* name);
14 ~BCAT() override; 15 ~BCAT() override;
15}; 16};
16 17
diff --git a/src/core/hle/service/bcat/module.cpp b/src/core/hle/service/bcat/module.cpp
index b7bd738fc..b3fed56c7 100644
--- a/src/core/hle/service/bcat/module.cpp
+++ b/src/core/hle/service/bcat/module.cpp
@@ -2,34 +2,254 @@
2// Licensed under GPLv2 or any later version 2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included. 3// Refer to the license.txt file included.
4 4
5#include <cctype>
6#include <mbedtls/md5.h>
7#include "backend/boxcat.h"
8#include "common/hex_util.h"
5#include "common/logging/log.h" 9#include "common/logging/log.h"
10#include "common/string_util.h"
11#include "core/file_sys/vfs.h"
6#include "core/hle/ipc_helpers.h" 12#include "core/hle/ipc_helpers.h"
13#include "core/hle/kernel/process.h"
14#include "core/hle/kernel/readable_event.h"
15#include "core/hle/kernel/writable_event.h"
16#include "core/hle/service/bcat/backend/backend.h"
7#include "core/hle/service/bcat/bcat.h" 17#include "core/hle/service/bcat/bcat.h"
8#include "core/hle/service/bcat/module.h" 18#include "core/hle/service/bcat/module.h"
19#include "core/hle/service/filesystem/filesystem.h"
20#include "core/settings.h"
9 21
10namespace Service::BCAT { 22namespace Service::BCAT {
11 23
24constexpr ResultCode ERROR_INVALID_ARGUMENT{ErrorModule::BCAT, 1};
25constexpr ResultCode ERROR_FAILED_OPEN_ENTITY{ErrorModule::BCAT, 2};
26constexpr ResultCode ERROR_ENTITY_ALREADY_OPEN{ErrorModule::BCAT, 6};
27constexpr ResultCode ERROR_NO_OPEN_ENTITY{ErrorModule::BCAT, 7};
28
29// The command to clear the delivery cache just calls fs IFileSystem DeleteFile on all of the files
30// and if any of them have a non-zero result it just forwards that result. This is the FS error code
31// for permission denied, which is the closest approximation of this scenario.
32constexpr ResultCode ERROR_FAILED_CLEAR_CACHE{ErrorModule::FS, 6400};
33
34using BCATDigest = std::array<u8, 0x10>;
35
36namespace {
37
38u64 GetCurrentBuildID() {
39 const auto& id = Core::System::GetInstance().GetCurrentProcessBuildID();
40 u64 out{};
41 std::memcpy(&out, id.data(), sizeof(u64));
42 return out;
43}
44
45// The digest is only used to determine if a file is unique compared to others of the same name.
46// Since the algorithm isn't ever checked in game, MD5 is safe.
47BCATDigest DigestFile(const FileSys::VirtualFile& file) {
48 BCATDigest out{};
49 const auto bytes = file->ReadAllBytes();
50 mbedtls_md5(bytes.data(), bytes.size(), out.data());
51 return out;
52}
53
54// For a name to be valid it must be non-empty, must have a null terminating character as the final
55// char, can only contain numbers, letters, underscores and a hyphen if directory and a period if
56// file.
57bool VerifyNameValidInternal(Kernel::HLERequestContext& ctx, std::array<char, 0x20> name,
58 char match_char) {
59 const auto null_chars = std::count(name.begin(), name.end(), 0);
60 const auto bad_chars = std::count_if(name.begin(), name.end(), [match_char](char c) {
61 return !std::isalnum(static_cast<u8>(c)) && c != '_' && c != match_char && c != '\0';
62 });
63 if (null_chars == 0x20 || null_chars == 0 || bad_chars != 0 || name[0x1F] != '\0') {
64 LOG_ERROR(Service_BCAT, "Name passed was invalid!");
65 IPC::ResponseBuilder rb{ctx, 2};
66 rb.Push(ERROR_INVALID_ARGUMENT);
67 return false;
68 }
69
70 return true;
71}
72
73bool VerifyNameValidDir(Kernel::HLERequestContext& ctx, DirectoryName name) {
74 return VerifyNameValidInternal(ctx, name, '-');
75}
76
77bool VerifyNameValidFile(Kernel::HLERequestContext& ctx, FileName name) {
78 return VerifyNameValidInternal(ctx, name, '.');
79}
80
81} // Anonymous namespace
82
83struct DeliveryCacheDirectoryEntry {
84 FileName name;
85 u64 size;
86 BCATDigest digest;
87};
88
89class IDeliveryCacheProgressService final : public ServiceFramework<IDeliveryCacheProgressService> {
90public:
91 IDeliveryCacheProgressService(Kernel::SharedPtr<Kernel::ReadableEvent> event,
92 const DeliveryCacheProgressImpl& impl)
93 : ServiceFramework{"IDeliveryCacheProgressService"}, event(std::move(event)), impl(impl) {
94 // clang-format off
95 static const FunctionInfo functions[] = {
96 {0, &IDeliveryCacheProgressService::GetEvent, "GetEvent"},
97 {1, &IDeliveryCacheProgressService::GetImpl, "GetImpl"},
98 };
99 // clang-format on
100
101 RegisterHandlers(functions);
102 }
103
104private:
105 void GetEvent(Kernel::HLERequestContext& ctx) {
106 LOG_DEBUG(Service_BCAT, "called");
107
108 IPC::ResponseBuilder rb{ctx, 2, 1};
109 rb.Push(RESULT_SUCCESS);
110 rb.PushCopyObjects(event);
111 }
112
113 void GetImpl(Kernel::HLERequestContext& ctx) {
114 LOG_DEBUG(Service_BCAT, "called");
115
116 ctx.WriteBuffer(&impl, sizeof(DeliveryCacheProgressImpl));
117
118 IPC::ResponseBuilder rb{ctx, 2};
119 rb.Push(RESULT_SUCCESS);
120 }
121
122 Kernel::SharedPtr<Kernel::ReadableEvent> event;
123 const DeliveryCacheProgressImpl& impl;
124};
125
12class IBcatService final : public ServiceFramework<IBcatService> { 126class IBcatService final : public ServiceFramework<IBcatService> {
13public: 127public:
14 IBcatService() : ServiceFramework("IBcatService") { 128 IBcatService(Backend& backend) : ServiceFramework("IBcatService"), backend(backend) {
129 // clang-format off
15 static const FunctionInfo functions[] = { 130 static const FunctionInfo functions[] = {
16 {10100, nullptr, "RequestSyncDeliveryCache"}, 131 {10100, &IBcatService::RequestSyncDeliveryCache, "RequestSyncDeliveryCache"},
17 {10101, nullptr, "RequestSyncDeliveryCacheWithDirectoryName"}, 132 {10101, &IBcatService::RequestSyncDeliveryCacheWithDirectoryName, "RequestSyncDeliveryCacheWithDirectoryName"},
18 {10200, nullptr, "CancelSyncDeliveryCacheRequest"}, 133 {10200, nullptr, "CancelSyncDeliveryCacheRequest"},
19 {20100, nullptr, "RequestSyncDeliveryCacheWithApplicationId"}, 134 {20100, nullptr, "RequestSyncDeliveryCacheWithApplicationId"},
20 {20101, nullptr, "RequestSyncDeliveryCacheWithApplicationIdAndDirectoryName"}, 135 {20101, nullptr, "RequestSyncDeliveryCacheWithApplicationIdAndDirectoryName"},
21 {30100, nullptr, "SetPassphrase"}, 136 {30100, &IBcatService::SetPassphrase, "SetPassphrase"},
22 {30200, nullptr, "RegisterBackgroundDeliveryTask"}, 137 {30200, nullptr, "RegisterBackgroundDeliveryTask"},
23 {30201, nullptr, "UnregisterBackgroundDeliveryTask"}, 138 {30201, nullptr, "UnregisterBackgroundDeliveryTask"},
24 {30202, nullptr, "BlockDeliveryTask"}, 139 {30202, nullptr, "BlockDeliveryTask"},
25 {30203, nullptr, "UnblockDeliveryTask"}, 140 {30203, nullptr, "UnblockDeliveryTask"},
26 {90100, nullptr, "EnumerateBackgroundDeliveryTask"}, 141 {90100, nullptr, "EnumerateBackgroundDeliveryTask"},
27 {90200, nullptr, "GetDeliveryList"}, 142 {90200, nullptr, "GetDeliveryList"},
28 {90201, nullptr, "ClearDeliveryCacheStorage"}, 143 {90201, &IBcatService::ClearDeliveryCacheStorage, "ClearDeliveryCacheStorage"},
29 {90300, nullptr, "GetPushNotificationLog"}, 144 {90300, nullptr, "GetPushNotificationLog"},
30 }; 145 };
146 // clang-format on
31 RegisterHandlers(functions); 147 RegisterHandlers(functions);
32 } 148 }
149
150private:
151 enum class SyncType {
152 Normal,
153 Directory,
154 Count,
155 };
156
157 std::shared_ptr<IDeliveryCacheProgressService> CreateProgressService(SyncType type) {
158 auto& backend{progress.at(static_cast<std::size_t>(type))};
159 return std::make_shared<IDeliveryCacheProgressService>(backend.GetEvent(),
160 backend.GetImpl());
161 }
162
163 void RequestSyncDeliveryCache(Kernel::HLERequestContext& ctx) {
164 LOG_DEBUG(Service_BCAT, "called");
165
166 backend.Synchronize({Core::CurrentProcess()->GetTitleID(), GetCurrentBuildID()},
167 progress.at(static_cast<std::size_t>(SyncType::Normal)));
168
169 IPC::ResponseBuilder rb{ctx, 2, 0, 1};
170 rb.Push(RESULT_SUCCESS);
171 rb.PushIpcInterface(CreateProgressService(SyncType::Normal));
172 }
173
174 void RequestSyncDeliveryCacheWithDirectoryName(Kernel::HLERequestContext& ctx) {
175 IPC::RequestParser rp{ctx};
176 const auto name_raw = rp.PopRaw<DirectoryName>();
177 const auto name =
178 Common::StringFromFixedZeroTerminatedBuffer(name_raw.data(), name_raw.size());
179
180 LOG_DEBUG(Service_BCAT, "called, name={}", name);
181
182 backend.SynchronizeDirectory({Core::CurrentProcess()->GetTitleID(), GetCurrentBuildID()},
183 name,
184 progress.at(static_cast<std::size_t>(SyncType::Directory)));
185
186 IPC::ResponseBuilder rb{ctx, 2, 0, 1};
187 rb.Push(RESULT_SUCCESS);
188 rb.PushIpcInterface(CreateProgressService(SyncType::Directory));
189 }
190
191 void SetPassphrase(Kernel::HLERequestContext& ctx) {
192 IPC::RequestParser rp{ctx};
193 const auto title_id = rp.PopRaw<u64>();
194
195 const auto passphrase_raw = ctx.ReadBuffer();
196
197 LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, passphrase={}", title_id,
198 Common::HexToString(passphrase_raw));
199
200 if (title_id == 0) {
201 LOG_ERROR(Service_BCAT, "Invalid title ID!");
202 IPC::ResponseBuilder rb{ctx, 2};
203 rb.Push(ERROR_INVALID_ARGUMENT);
204 }
205
206 if (passphrase_raw.size() > 0x40) {
207 LOG_ERROR(Service_BCAT, "Passphrase too large!");
208 IPC::ResponseBuilder rb{ctx, 2};
209 rb.Push(ERROR_INVALID_ARGUMENT);
210 return;
211 }
212
213 Passphrase passphrase{};
214 std::memcpy(passphrase.data(), passphrase_raw.data(),
215 std::min(passphrase.size(), passphrase_raw.size()));
216
217 backend.SetPassphrase(title_id, passphrase);
218
219 IPC::ResponseBuilder rb{ctx, 2};
220 rb.Push(RESULT_SUCCESS);
221 }
222
223 void ClearDeliveryCacheStorage(Kernel::HLERequestContext& ctx) {
224 IPC::RequestParser rp{ctx};
225 const auto title_id = rp.PopRaw<u64>();
226
227 LOG_DEBUG(Service_BCAT, "called, title_id={:016X}", title_id);
228
229 if (title_id == 0) {
230 LOG_ERROR(Service_BCAT, "Invalid title ID!");
231 IPC::ResponseBuilder rb{ctx, 2};
232 rb.Push(ERROR_INVALID_ARGUMENT);
233 return;
234 }
235
236 if (!backend.Clear(title_id)) {
237 LOG_ERROR(Service_BCAT, "Could not clear the directory successfully!");
238 IPC::ResponseBuilder rb{ctx, 2};
239 rb.Push(ERROR_FAILED_CLEAR_CACHE);
240 return;
241 }
242
243 IPC::ResponseBuilder rb{ctx, 2};
244 rb.Push(RESULT_SUCCESS);
245 }
246
247 Backend& backend;
248
249 std::array<ProgressServiceBackend, static_cast<std::size_t>(SyncType::Count)> progress{
250 ProgressServiceBackend{"Normal"},
251 ProgressServiceBackend{"Directory"},
252 };
33}; 253};
34 254
35void Module::Interface::CreateBcatService(Kernel::HLERequestContext& ctx) { 255void Module::Interface::CreateBcatService(Kernel::HLERequestContext& ctx) {
@@ -37,20 +257,331 @@ void Module::Interface::CreateBcatService(Kernel::HLERequestContext& ctx) {
37 257
38 IPC::ResponseBuilder rb{ctx, 2, 0, 1}; 258 IPC::ResponseBuilder rb{ctx, 2, 0, 1};
39 rb.Push(RESULT_SUCCESS); 259 rb.Push(RESULT_SUCCESS);
40 rb.PushIpcInterface<IBcatService>(); 260 rb.PushIpcInterface<IBcatService>(*backend);
261}
262
263class IDeliveryCacheFileService final : public ServiceFramework<IDeliveryCacheFileService> {
264public:
265 IDeliveryCacheFileService(FileSys::VirtualDir root_)
266 : ServiceFramework{"IDeliveryCacheFileService"}, root(std::move(root_)) {
267 // clang-format off
268 static const FunctionInfo functions[] = {
269 {0, &IDeliveryCacheFileService::Open, "Open"},
270 {1, &IDeliveryCacheFileService::Read, "Read"},
271 {2, &IDeliveryCacheFileService::GetSize, "GetSize"},
272 {3, &IDeliveryCacheFileService::GetDigest, "GetDigest"},
273 };
274 // clang-format on
275
276 RegisterHandlers(functions);
277 }
278
279private:
280 void Open(Kernel::HLERequestContext& ctx) {
281 IPC::RequestParser rp{ctx};
282 const auto dir_name_raw = rp.PopRaw<DirectoryName>();
283 const auto file_name_raw = rp.PopRaw<FileName>();
284
285 const auto dir_name =
286 Common::StringFromFixedZeroTerminatedBuffer(dir_name_raw.data(), dir_name_raw.size());
287 const auto file_name =
288 Common::StringFromFixedZeroTerminatedBuffer(file_name_raw.data(), file_name_raw.size());
289
290 LOG_DEBUG(Service_BCAT, "called, dir_name={}, file_name={}", dir_name, file_name);
291
292 if (!VerifyNameValidDir(ctx, dir_name_raw) || !VerifyNameValidFile(ctx, file_name_raw))
293 return;
294
295 if (current_file != nullptr) {
296 LOG_ERROR(Service_BCAT, "A file has already been opened on this interface!");
297 IPC::ResponseBuilder rb{ctx, 2};
298 rb.Push(ERROR_ENTITY_ALREADY_OPEN);
299 return;
300 }
301
302 const auto dir = root->GetSubdirectory(dir_name);
303
304 if (dir == nullptr) {
305 LOG_ERROR(Service_BCAT, "The directory of name={} couldn't be opened!", dir_name);
306 IPC::ResponseBuilder rb{ctx, 2};
307 rb.Push(ERROR_FAILED_OPEN_ENTITY);
308 return;
309 }
310
311 current_file = dir->GetFile(file_name);
312
313 if (current_file == nullptr) {
314 LOG_ERROR(Service_BCAT, "The file of name={} couldn't be opened!", file_name);
315 IPC::ResponseBuilder rb{ctx, 2};
316 rb.Push(ERROR_FAILED_OPEN_ENTITY);
317 return;
318 }
319
320 IPC::ResponseBuilder rb{ctx, 2};
321 rb.Push(RESULT_SUCCESS);
322 }
323
324 void Read(Kernel::HLERequestContext& ctx) {
325 IPC::RequestParser rp{ctx};
326 const auto offset{rp.PopRaw<u64>()};
327
328 auto size = ctx.GetWriteBufferSize();
329
330 LOG_DEBUG(Service_BCAT, "called, offset={:016X}, size={:016X}", offset, size);
331
332 if (current_file == nullptr) {
333 LOG_ERROR(Service_BCAT, "There is no file currently open!");
334 IPC::ResponseBuilder rb{ctx, 2};
335 rb.Push(ERROR_NO_OPEN_ENTITY);
336 }
337
338 size = std::min<u64>(current_file->GetSize() - offset, size);
339 const auto buffer = current_file->ReadBytes(size, offset);
340 ctx.WriteBuffer(buffer);
341
342 IPC::ResponseBuilder rb{ctx, 4};
343 rb.Push(RESULT_SUCCESS);
344 rb.Push<u64>(buffer.size());
345 }
346
347 void GetSize(Kernel::HLERequestContext& ctx) {
348 LOG_DEBUG(Service_BCAT, "called");
349
350 if (current_file == nullptr) {
351 LOG_ERROR(Service_BCAT, "There is no file currently open!");
352 IPC::ResponseBuilder rb{ctx, 2};
353 rb.Push(ERROR_NO_OPEN_ENTITY);
354 }
355
356 IPC::ResponseBuilder rb{ctx, 4};
357 rb.Push(RESULT_SUCCESS);
358 rb.Push<u64>(current_file->GetSize());
359 }
360
361 void GetDigest(Kernel::HLERequestContext& ctx) {
362 LOG_DEBUG(Service_BCAT, "called");
363
364 if (current_file == nullptr) {
365 LOG_ERROR(Service_BCAT, "There is no file currently open!");
366 IPC::ResponseBuilder rb{ctx, 2};
367 rb.Push(ERROR_NO_OPEN_ENTITY);
368 }
369
370 IPC::ResponseBuilder rb{ctx, 6};
371 rb.Push(RESULT_SUCCESS);
372 rb.PushRaw(DigestFile(current_file));
373 }
374
375 FileSys::VirtualDir root;
376 FileSys::VirtualFile current_file;
377};
378
379class IDeliveryCacheDirectoryService final
380 : public ServiceFramework<IDeliveryCacheDirectoryService> {
381public:
382 IDeliveryCacheDirectoryService(FileSys::VirtualDir root_)
383 : ServiceFramework{"IDeliveryCacheDirectoryService"}, root(std::move(root_)) {
384 // clang-format off
385 static const FunctionInfo functions[] = {
386 {0, &IDeliveryCacheDirectoryService::Open, "Open"},
387 {1, &IDeliveryCacheDirectoryService::Read, "Read"},
388 {2, &IDeliveryCacheDirectoryService::GetCount, "GetCount"},
389 };
390 // clang-format on
391
392 RegisterHandlers(functions);
393 }
394
395private:
396 void Open(Kernel::HLERequestContext& ctx) {
397 IPC::RequestParser rp{ctx};
398 const auto name_raw = rp.PopRaw<DirectoryName>();
399 const auto name =
400 Common::StringFromFixedZeroTerminatedBuffer(name_raw.data(), name_raw.size());
401
402 LOG_DEBUG(Service_BCAT, "called, name={}", name);
403
404 if (!VerifyNameValidDir(ctx, name_raw))
405 return;
406
407 if (current_dir != nullptr) {
408 LOG_ERROR(Service_BCAT, "A file has already been opened on this interface!");
409 IPC::ResponseBuilder rb{ctx, 2};
410 rb.Push(ERROR_ENTITY_ALREADY_OPEN);
411 return;
412 }
413
414 current_dir = root->GetSubdirectory(name);
415
416 if (current_dir == nullptr) {
417 LOG_ERROR(Service_BCAT, "Failed to open the directory name={}!", name);
418 IPC::ResponseBuilder rb{ctx, 2};
419 rb.Push(ERROR_FAILED_OPEN_ENTITY);
420 return;
421 }
422
423 IPC::ResponseBuilder rb{ctx, 2};
424 rb.Push(RESULT_SUCCESS);
425 }
426
427 void Read(Kernel::HLERequestContext& ctx) {
428 auto write_size = ctx.GetWriteBufferSize() / sizeof(DeliveryCacheDirectoryEntry);
429
430 LOG_DEBUG(Service_BCAT, "called, write_size={:016X}", write_size);
431
432 if (current_dir == nullptr) {
433 LOG_ERROR(Service_BCAT, "There is no open directory!");
434 IPC::ResponseBuilder rb{ctx, 2};
435 rb.Push(ERROR_NO_OPEN_ENTITY);
436 return;
437 }
438
439 const auto files = current_dir->GetFiles();
440 write_size = std::min<u64>(write_size, files.size());
441 std::vector<DeliveryCacheDirectoryEntry> entries(write_size);
442 std::transform(
443 files.begin(), files.begin() + write_size, entries.begin(), [](const auto& file) {
444 FileName name{};
445 std::memcpy(name.data(), file->GetName().data(),
446 std::min(file->GetName().size(), name.size()));
447 return DeliveryCacheDirectoryEntry{name, file->GetSize(), DigestFile(file)};
448 });
449
450 ctx.WriteBuffer(entries);
451
452 IPC::ResponseBuilder rb{ctx, 3};
453 rb.Push(RESULT_SUCCESS);
454 rb.Push<u32>(write_size * sizeof(DeliveryCacheDirectoryEntry));
455 }
456
457 void GetCount(Kernel::HLERequestContext& ctx) {
458 LOG_DEBUG(Service_BCAT, "called");
459
460 if (current_dir == nullptr) {
461 LOG_ERROR(Service_BCAT, "There is no open directory!");
462 IPC::ResponseBuilder rb{ctx, 2};
463 rb.Push(ERROR_NO_OPEN_ENTITY);
464 return;
465 }
466
467 const auto files = current_dir->GetFiles();
468
469 IPC::ResponseBuilder rb{ctx, 3};
470 rb.Push(RESULT_SUCCESS);
471 rb.Push<u32>(files.size());
472 }
473
474 FileSys::VirtualDir root;
475 FileSys::VirtualDir current_dir;
476};
477
478class IDeliveryCacheStorageService final : public ServiceFramework<IDeliveryCacheStorageService> {
479public:
480 IDeliveryCacheStorageService(FileSys::VirtualDir root_)
481 : ServiceFramework{"IDeliveryCacheStorageService"}, root(std::move(root_)) {
482 // clang-format off
483 static const FunctionInfo functions[] = {
484 {0, &IDeliveryCacheStorageService::CreateFileService, "CreateFileService"},
485 {1, &IDeliveryCacheStorageService::CreateDirectoryService, "CreateDirectoryService"},
486 {10, &IDeliveryCacheStorageService::EnumerateDeliveryCacheDirectory, "EnumerateDeliveryCacheDirectory"},
487 };
488 // clang-format on
489
490 RegisterHandlers(functions);
491
492 for (const auto& subdir : root->GetSubdirectories()) {
493 DirectoryName name{};
494 std::memcpy(name.data(), subdir->GetName().data(),
495 std::min(sizeof(DirectoryName) - 1, subdir->GetName().size()));
496 entries.push_back(name);
497 }
498 }
499
500private:
501 void CreateFileService(Kernel::HLERequestContext& ctx) {
502 LOG_DEBUG(Service_BCAT, "called");
503
504 IPC::ResponseBuilder rb{ctx, 2, 0, 1};
505 rb.Push(RESULT_SUCCESS);
506 rb.PushIpcInterface<IDeliveryCacheFileService>(root);
507 }
508
509 void CreateDirectoryService(Kernel::HLERequestContext& ctx) {
510 LOG_DEBUG(Service_BCAT, "called");
511
512 IPC::ResponseBuilder rb{ctx, 2, 0, 1};
513 rb.Push(RESULT_SUCCESS);
514 rb.PushIpcInterface<IDeliveryCacheDirectoryService>(root);
515 }
516
517 void EnumerateDeliveryCacheDirectory(Kernel::HLERequestContext& ctx) {
518 auto size = ctx.GetWriteBufferSize() / sizeof(DirectoryName);
519
520 LOG_DEBUG(Service_BCAT, "called, size={:016X}", size);
521
522 size = std::min<u64>(size, entries.size() - next_read_index);
523 ctx.WriteBuffer(entries.data() + next_read_index, size * sizeof(DirectoryName));
524 next_read_index += size;
525
526 IPC::ResponseBuilder rb{ctx, 3};
527 rb.Push(RESULT_SUCCESS);
528 rb.Push<u32>(size);
529 }
530
531 FileSys::VirtualDir root;
532 std::vector<DirectoryName> entries;
533 u64 next_read_index = 0;
534};
535
536void Module::Interface::CreateDeliveryCacheStorageService(Kernel::HLERequestContext& ctx) {
537 LOG_DEBUG(Service_BCAT, "called");
538
539 IPC::ResponseBuilder rb{ctx, 2, 0, 1};
540 rb.Push(RESULT_SUCCESS);
541 rb.PushIpcInterface<IDeliveryCacheStorageService>(
542 fsc.GetBCATDirectory(Core::CurrentProcess()->GetTitleID()));
543}
544
545void Module::Interface::CreateDeliveryCacheStorageServiceWithApplicationId(
546 Kernel::HLERequestContext& ctx) {
547 IPC::RequestParser rp{ctx};
548 const auto title_id = rp.PopRaw<u64>();
549
550 LOG_DEBUG(Service_BCAT, "called, title_id={:016X}", title_id);
551
552 IPC::ResponseBuilder rb{ctx, 2, 0, 1};
553 rb.Push(RESULT_SUCCESS);
554 rb.PushIpcInterface<IDeliveryCacheStorageService>(fsc.GetBCATDirectory(title_id));
555}
556
557std::unique_ptr<Backend> CreateBackendFromSettings(DirectoryGetter getter) {
558 const auto backend = Settings::values.bcat_backend;
559
560#ifdef YUZU_ENABLE_BOXCAT
561 if (backend == "boxcat")
562 return std::make_unique<Boxcat>(std::move(getter));
563#endif
564
565 return std::make_unique<NullBackend>(std::move(getter));
41} 566}
42 567
43Module::Interface::Interface(std::shared_ptr<Module> module, const char* name) 568Module::Interface::Interface(std::shared_ptr<Module> module, FileSystem::FileSystemController& fsc,
44 : ServiceFramework(name), module(std::move(module)) {} 569 const char* name)
570 : ServiceFramework(name), module(std::move(module)), fsc(fsc),
571 backend(CreateBackendFromSettings([&fsc](u64 tid) { return fsc.GetBCATDirectory(tid); })) {}
45 572
46Module::Interface::~Interface() = default; 573Module::Interface::~Interface() = default;
47 574
48void InstallInterfaces(SM::ServiceManager& service_manager) { 575void InstallInterfaces(Core::System& system) {
49 auto module = std::make_shared<Module>(); 576 auto module = std::make_shared<Module>();
50 std::make_shared<BCAT>(module, "bcat:a")->InstallAsService(service_manager); 577 std::make_shared<BCAT>(module, system.GetFileSystemController(), "bcat:a")
51 std::make_shared<BCAT>(module, "bcat:m")->InstallAsService(service_manager); 578 ->InstallAsService(system.ServiceManager());
52 std::make_shared<BCAT>(module, "bcat:u")->InstallAsService(service_manager); 579 std::make_shared<BCAT>(module, system.GetFileSystemController(), "bcat:m")
53 std::make_shared<BCAT>(module, "bcat:s")->InstallAsService(service_manager); 580 ->InstallAsService(system.ServiceManager());
581 std::make_shared<BCAT>(module, system.GetFileSystemController(), "bcat:u")
582 ->InstallAsService(system.ServiceManager());
583 std::make_shared<BCAT>(module, system.GetFileSystemController(), "bcat:s")
584 ->InstallAsService(system.ServiceManager());
54} 585}
55 586
56} // namespace Service::BCAT 587} // namespace Service::BCAT
diff --git a/src/core/hle/service/bcat/module.h b/src/core/hle/service/bcat/module.h
index f0d63cab0..27469926a 100644
--- a/src/core/hle/service/bcat/module.h
+++ b/src/core/hle/service/bcat/module.h
@@ -6,23 +6,39 @@
6 6
7#include "core/hle/service/service.h" 7#include "core/hle/service/service.h"
8 8
9namespace Service::BCAT { 9namespace Service {
10
11namespace FileSystem {
12class FileSystemController;
13} // namespace FileSystem
14
15namespace BCAT {
16
17class Backend;
10 18
11class Module final { 19class Module final {
12public: 20public:
13 class Interface : public ServiceFramework<Interface> { 21 class Interface : public ServiceFramework<Interface> {
14 public: 22 public:
15 explicit Interface(std::shared_ptr<Module> module, const char* name); 23 explicit Interface(std::shared_ptr<Module> module, FileSystem::FileSystemController& fsc,
24 const char* name);
16 ~Interface() override; 25 ~Interface() override;
17 26
18 void CreateBcatService(Kernel::HLERequestContext& ctx); 27 void CreateBcatService(Kernel::HLERequestContext& ctx);
28 void CreateDeliveryCacheStorageService(Kernel::HLERequestContext& ctx);
29 void CreateDeliveryCacheStorageServiceWithApplicationId(Kernel::HLERequestContext& ctx);
19 30
20 protected: 31 protected:
32 FileSystem::FileSystemController& fsc;
33
21 std::shared_ptr<Module> module; 34 std::shared_ptr<Module> module;
35 std::unique_ptr<Backend> backend;
22 }; 36 };
23}; 37};
24 38
25/// Registers all BCAT services with the specified service manager. 39/// Registers all BCAT services with the specified service manager.
26void InstallInterfaces(SM::ServiceManager& service_manager); 40void InstallInterfaces(Core::System& system);
41
42} // namespace BCAT
27 43
28} // namespace Service::BCAT 44} // namespace Service
diff --git a/src/core/hle/service/filesystem/filesystem.cpp b/src/core/hle/service/filesystem/filesystem.cpp
index 14cd0e322..7fa4e820b 100644
--- a/src/core/hle/service/filesystem/filesystem.cpp
+++ b/src/core/hle/service/filesystem/filesystem.cpp
@@ -674,6 +674,15 @@ FileSys::VirtualDir FileSystemController::GetModificationDumpRoot(u64 title_id)
674 return bis_factory->GetModificationDumpRoot(title_id); 674 return bis_factory->GetModificationDumpRoot(title_id);
675} 675}
676 676
677FileSys::VirtualDir FileSystemController::GetBCATDirectory(u64 title_id) const {
678 LOG_TRACE(Service_FS, "Opening BCAT root for tid={:016X}", title_id);
679
680 if (bis_factory == nullptr)
681 return nullptr;
682
683 return bis_factory->GetBCATDirectory(title_id);
684}
685
677void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite) { 686void FileSystemController::CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite) {
678 if (overwrite) { 687 if (overwrite) {
679 bis_factory = nullptr; 688 bis_factory = nullptr;
diff --git a/src/core/hle/service/filesystem/filesystem.h b/src/core/hle/service/filesystem/filesystem.h
index 3e0c03ec0..e6b49d8a2 100644
--- a/src/core/hle/service/filesystem/filesystem.h
+++ b/src/core/hle/service/filesystem/filesystem.h
@@ -110,6 +110,8 @@ public:
110 FileSys::VirtualDir GetModificationLoadRoot(u64 title_id) const; 110 FileSys::VirtualDir GetModificationLoadRoot(u64 title_id) const;
111 FileSys::VirtualDir GetModificationDumpRoot(u64 title_id) const; 111 FileSys::VirtualDir GetModificationDumpRoot(u64 title_id) const;
112 112
113 FileSys::VirtualDir GetBCATDirectory(u64 title_id) const;
114
113 // Creates the SaveData, SDMC, and BIS Factories. Should be called once and before any function 115 // Creates the SaveData, SDMC, and BIS Factories. Should be called once and before any function
114 // above is called. 116 // above is called.
115 void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite = true); 117 void CreateFactories(FileSys::VfsFilesystem& vfs, bool overwrite = true);
diff --git a/src/core/hle/service/nifm/nifm.cpp b/src/core/hle/service/nifm/nifm.cpp
index 24d1813a7..756a2af57 100644
--- a/src/core/hle/service/nifm/nifm.cpp
+++ b/src/core/hle/service/nifm/nifm.cpp
@@ -12,6 +12,13 @@
12 12
13namespace Service::NIFM { 13namespace Service::NIFM {
14 14
15enum class RequestState : u32 {
16 NotSubmitted = 1,
17 Error = 1, ///< The duplicate 1 is intentional; it means both not submitted and error on HW.
18 Pending = 2,
19 Connected = 3,
20};
21
15class IScanRequest final : public ServiceFramework<IScanRequest> { 22class IScanRequest final : public ServiceFramework<IScanRequest> {
16public: 23public:
17 explicit IScanRequest() : ServiceFramework("IScanRequest") { 24 explicit IScanRequest() : ServiceFramework("IScanRequest") {
@@ -81,7 +88,7 @@ private:
81 88
82 IPC::ResponseBuilder rb{ctx, 3}; 89 IPC::ResponseBuilder rb{ctx, 3};
83 rb.Push(RESULT_SUCCESS); 90 rb.Push(RESULT_SUCCESS);
84 rb.Push<u32>(0); 91 rb.PushEnum(RequestState::Connected);
85 } 92 }
86 93
87 void GetResult(Kernel::HLERequestContext& ctx) { 94 void GetResult(Kernel::HLERequestContext& ctx) {
@@ -189,14 +196,14 @@ private:
189 196
190 IPC::ResponseBuilder rb{ctx, 3}; 197 IPC::ResponseBuilder rb{ctx, 3};
191 rb.Push(RESULT_SUCCESS); 198 rb.Push(RESULT_SUCCESS);
192 rb.Push<u8>(0); 199 rb.Push<u8>(1);
193 } 200 }
194 void IsAnyInternetRequestAccepted(Kernel::HLERequestContext& ctx) { 201 void IsAnyInternetRequestAccepted(Kernel::HLERequestContext& ctx) {
195 LOG_WARNING(Service_NIFM, "(STUBBED) called"); 202 LOG_WARNING(Service_NIFM, "(STUBBED) called");
196 203
197 IPC::ResponseBuilder rb{ctx, 3}; 204 IPC::ResponseBuilder rb{ctx, 3};
198 rb.Push(RESULT_SUCCESS); 205 rb.Push(RESULT_SUCCESS);
199 rb.Push<u8>(0); 206 rb.Push<u8>(1);
200 } 207 }
201 Core::System& system; 208 Core::System& system;
202}; 209};
diff --git a/src/core/hle/service/service.cpp b/src/core/hle/service/service.cpp
index 831a427de..f2c6fe9dc 100644
--- a/src/core/hle/service/service.cpp
+++ b/src/core/hle/service/service.cpp
@@ -208,7 +208,7 @@ void Init(std::shared_ptr<SM::ServiceManager>& sm, Core::System& system) {
208 AOC::InstallInterfaces(*sm, system); 208 AOC::InstallInterfaces(*sm, system);
209 APM::InstallInterfaces(system); 209 APM::InstallInterfaces(system);
210 Audio::InstallInterfaces(*sm, system); 210 Audio::InstallInterfaces(*sm, system);
211 BCAT::InstallInterfaces(*sm); 211 BCAT::InstallInterfaces(system);
212 BPC::InstallInterfaces(*sm); 212 BPC::InstallInterfaces(*sm);
213 BtDrv::InstallInterfaces(*sm, system); 213 BtDrv::InstallInterfaces(*sm, system);
214 BTM::InstallInterfaces(*sm, system); 214 BTM::InstallInterfaces(*sm, system);
diff --git a/src/core/loader/nso.cpp b/src/core/loader/nso.cpp
index e75c700ad..f629892ae 100644
--- a/src/core/loader/nso.cpp
+++ b/src/core/loader/nso.cpp
@@ -150,6 +150,7 @@ std::optional<VAddr> AppLoader_NSO::LoadModule(Kernel::Process& process,
150 // Apply cheats if they exist and the program has a valid title ID 150 // Apply cheats if they exist and the program has a valid title ID
151 if (pm) { 151 if (pm) {
152 auto& system = Core::System::GetInstance(); 152 auto& system = Core::System::GetInstance();
153 system.SetCurrentProcessBuildID(nso_header.build_id);
153 const auto cheats = pm->CreateCheatList(system, nso_header.build_id); 154 const auto cheats = pm->CreateCheatList(system, nso_header.build_id);
154 if (!cheats.empty()) { 155 if (!cheats.empty()) {
155 system.RegisterCheatList(cheats, nso_header.build_id, load_base, image_size); 156 system.RegisterCheatList(cheats, nso_header.build_id, load_base, image_size);
diff --git a/src/core/settings.cpp b/src/core/settings.cpp
index 7de3fd1e5..d1fc94060 100644
--- a/src/core/settings.cpp
+++ b/src/core/settings.cpp
@@ -103,6 +103,8 @@ void LogSettings() {
103 LogSetting("Debugging_UseGdbstub", Settings::values.use_gdbstub); 103 LogSetting("Debugging_UseGdbstub", Settings::values.use_gdbstub);
104 LogSetting("Debugging_GdbstubPort", Settings::values.gdbstub_port); 104 LogSetting("Debugging_GdbstubPort", Settings::values.gdbstub_port);
105 LogSetting("Debugging_ProgramArgs", Settings::values.program_args); 105 LogSetting("Debugging_ProgramArgs", Settings::values.program_args);
106 LogSetting("Services_BCATBackend", Settings::values.bcat_backend);
107 LogSetting("Services_BCATBoxcatLocal", Settings::values.bcat_boxcat_local);
106} 108}
107 109
108} // namespace Settings 110} // namespace Settings
diff --git a/src/core/settings.h b/src/core/settings.h
index 47bddfb30..9c98a9287 100644
--- a/src/core/settings.h
+++ b/src/core/settings.h
@@ -448,6 +448,10 @@ struct Values {
448 bool reporting_services; 448 bool reporting_services;
449 bool quest_flag; 449 bool quest_flag;
450 450
451 // BCAT
452 std::string bcat_backend;
453 bool bcat_boxcat_local;
454
451 // WebService 455 // WebService
452 bool enable_telemetry; 456 bool enable_telemetry;
453 std::string web_api_url; 457 std::string web_api_url;
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index dc6fa07fc..ff1c1d985 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -66,6 +66,9 @@ add_executable(yuzu
66 configuration/configure_profile_manager.cpp 66 configuration/configure_profile_manager.cpp
67 configuration/configure_profile_manager.h 67 configuration/configure_profile_manager.h
68 configuration/configure_profile_manager.ui 68 configuration/configure_profile_manager.ui
69 configuration/configure_service.cpp
70 configuration/configure_service.h
71 configuration/configure_service.ui
69 configuration/configure_system.cpp 72 configuration/configure_system.cpp
70 configuration/configure_system.h 73 configuration/configure_system.h
71 configuration/configure_system.ui 74 configuration/configure_system.ui
@@ -186,6 +189,10 @@ if (YUZU_USE_QT_WEB_ENGINE)
186 target_compile_definitions(yuzu PRIVATE -DYUZU_USE_QT_WEB_ENGINE) 189 target_compile_definitions(yuzu PRIVATE -DYUZU_USE_QT_WEB_ENGINE)
187endif () 190endif ()
188 191
192if (YUZU_ENABLE_BOXCAT)
193 target_compile_definitions(yuzu PRIVATE -DYUZU_ENABLE_BOXCAT)
194endif ()
195
189if(UNIX AND NOT APPLE) 196if(UNIX AND NOT APPLE)
190 install(TARGETS yuzu RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin") 197 install(TARGETS yuzu RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}/bin")
191endif() 198endif()
diff --git a/src/yuzu/configuration/config.cpp b/src/yuzu/configuration/config.cpp
index 92d9fb161..4cb27ddb2 100644
--- a/src/yuzu/configuration/config.cpp
+++ b/src/yuzu/configuration/config.cpp
@@ -525,6 +525,17 @@ void Config::ReadDebuggingValues() {
525 qt_config->endGroup(); 525 qt_config->endGroup();
526} 526}
527 527
528void Config::ReadServiceValues() {
529 qt_config->beginGroup(QStringLiteral("Services"));
530 Settings::values.bcat_backend =
531 ReadSetting(QStringLiteral("bcat_backend"), QStringLiteral("boxcat"))
532 .toString()
533 .toStdString();
534 Settings::values.bcat_boxcat_local =
535 ReadSetting(QStringLiteral("bcat_boxcat_local"), false).toBool();
536 qt_config->endGroup();
537}
538
528void Config::ReadDisabledAddOnValues() { 539void Config::ReadDisabledAddOnValues() {
529 const auto size = qt_config->beginReadArray(QStringLiteral("DisabledAddOns")); 540 const auto size = qt_config->beginReadArray(QStringLiteral("DisabledAddOns"));
530 541
@@ -769,6 +780,7 @@ void Config::ReadValues() {
769 ReadMiscellaneousValues(); 780 ReadMiscellaneousValues();
770 ReadDebuggingValues(); 781 ReadDebuggingValues();
771 ReadWebServiceValues(); 782 ReadWebServiceValues();
783 ReadServiceValues();
772 ReadDisabledAddOnValues(); 784 ReadDisabledAddOnValues();
773 ReadUIValues(); 785 ReadUIValues();
774} 786}
@@ -866,6 +878,7 @@ void Config::SaveValues() {
866 SaveMiscellaneousValues(); 878 SaveMiscellaneousValues();
867 SaveDebuggingValues(); 879 SaveDebuggingValues();
868 SaveWebServiceValues(); 880 SaveWebServiceValues();
881 SaveServiceValues();
869 SaveDisabledAddOnValues(); 882 SaveDisabledAddOnValues();
870 SaveUIValues(); 883 SaveUIValues();
871} 884}
@@ -963,6 +976,14 @@ void Config::SaveDebuggingValues() {
963 qt_config->endGroup(); 976 qt_config->endGroup();
964} 977}
965 978
979void Config::SaveServiceValues() {
980 qt_config->beginGroup(QStringLiteral("Services"));
981 WriteSetting(QStringLiteral("bcat_backend"),
982 QString::fromStdString(Settings::values.bcat_backend), QStringLiteral("null"));
983 WriteSetting(QStringLiteral("bcat_boxcat_local"), Settings::values.bcat_boxcat_local, false);
984 qt_config->endGroup();
985}
986
966void Config::SaveDisabledAddOnValues() { 987void Config::SaveDisabledAddOnValues() {
967 qt_config->beginWriteArray(QStringLiteral("DisabledAddOns")); 988 qt_config->beginWriteArray(QStringLiteral("DisabledAddOns"));
968 989
diff --git a/src/yuzu/configuration/config.h b/src/yuzu/configuration/config.h
index 6b523ecdd..ba6888004 100644
--- a/src/yuzu/configuration/config.h
+++ b/src/yuzu/configuration/config.h
@@ -42,6 +42,7 @@ private:
42 void ReadCoreValues(); 42 void ReadCoreValues();
43 void ReadDataStorageValues(); 43 void ReadDataStorageValues();
44 void ReadDebuggingValues(); 44 void ReadDebuggingValues();
45 void ReadServiceValues();
45 void ReadDisabledAddOnValues(); 46 void ReadDisabledAddOnValues();
46 void ReadMiscellaneousValues(); 47 void ReadMiscellaneousValues();
47 void ReadPathValues(); 48 void ReadPathValues();
@@ -65,6 +66,7 @@ private:
65 void SaveCoreValues(); 66 void SaveCoreValues();
66 void SaveDataStorageValues(); 67 void SaveDataStorageValues();
67 void SaveDebuggingValues(); 68 void SaveDebuggingValues();
69 void SaveServiceValues();
68 void SaveDisabledAddOnValues(); 70 void SaveDisabledAddOnValues();
69 void SaveMiscellaneousValues(); 71 void SaveMiscellaneousValues();
70 void SavePathValues(); 72 void SavePathValues();
diff --git a/src/yuzu/configuration/configure.ui b/src/yuzu/configuration/configure.ui
index 49fadd0ef..372427ae2 100644
--- a/src/yuzu/configuration/configure.ui
+++ b/src/yuzu/configuration/configure.ui
@@ -98,6 +98,11 @@
98 <string>Web</string> 98 <string>Web</string>
99 </attribute> 99 </attribute>
100 </widget> 100 </widget>
101 <widget class="ConfigureService" name="serviceTab">
102 <attribute name="title">
103 <string>Services</string>
104 </attribute>
105 </widget>
101 </widget> 106 </widget>
102 </item> 107 </item>
103 </layout> 108 </layout>
@@ -178,6 +183,12 @@
178 <header>configuration/configure_hotkeys.h</header> 183 <header>configuration/configure_hotkeys.h</header>
179 <container>1</container> 184 <container>1</container>
180 </customwidget> 185 </customwidget>
186 <customwidget>
187 <class>ConfigureService</class>
188 <extends>QWidget</extends>
189 <header>configuration/configure_service.h</header>
190 <container>1</container>
191 </customwidget>
181 </customwidgets> 192 </customwidgets>
182 <resources/> 193 <resources/>
183 <connections> 194 <connections>
diff --git a/src/yuzu/configuration/configure_dialog.cpp b/src/yuzu/configuration/configure_dialog.cpp
index 7c875ae87..25b2e1b05 100644
--- a/src/yuzu/configuration/configure_dialog.cpp
+++ b/src/yuzu/configuration/configure_dialog.cpp
@@ -44,6 +44,7 @@ void ConfigureDialog::ApplyConfiguration() {
44 ui->audioTab->ApplyConfiguration(); 44 ui->audioTab->ApplyConfiguration();
45 ui->debugTab->ApplyConfiguration(); 45 ui->debugTab->ApplyConfiguration();
46 ui->webTab->ApplyConfiguration(); 46 ui->webTab->ApplyConfiguration();
47 ui->serviceTab->ApplyConfiguration();
47 Settings::Apply(); 48 Settings::Apply();
48 Settings::LogSettings(); 49 Settings::LogSettings();
49} 50}
@@ -74,7 +75,8 @@ Q_DECLARE_METATYPE(QList<QWidget*>);
74void ConfigureDialog::PopulateSelectionList() { 75void ConfigureDialog::PopulateSelectionList() {
75 const std::array<std::pair<QString, QList<QWidget*>>, 4> items{ 76 const std::array<std::pair<QString, QList<QWidget*>>, 4> items{
76 {{tr("General"), {ui->generalTab, ui->webTab, ui->debugTab, ui->gameListTab}}, 77 {{tr("General"), {ui->generalTab, ui->webTab, ui->debugTab, ui->gameListTab}},
77 {tr("System"), {ui->systemTab, ui->profileManagerTab, ui->filesystemTab, ui->audioTab}}, 78 {tr("System"),
79 {ui->systemTab, ui->profileManagerTab, ui->serviceTab, ui->filesystemTab, ui->audioTab}},
78 {tr("Graphics"), {ui->graphicsTab}}, 80 {tr("Graphics"), {ui->graphicsTab}},
79 {tr("Controls"), {ui->inputTab, ui->hotkeysTab}}}, 81 {tr("Controls"), {ui->inputTab, ui->hotkeysTab}}},
80 }; 82 };
@@ -108,6 +110,7 @@ void ConfigureDialog::UpdateVisibleTabs() {
108 {ui->webTab, tr("Web")}, 110 {ui->webTab, tr("Web")},
109 {ui->gameListTab, tr("Game List")}, 111 {ui->gameListTab, tr("Game List")},
110 {ui->filesystemTab, tr("Filesystem")}, 112 {ui->filesystemTab, tr("Filesystem")},
113 {ui->serviceTab, tr("Services")},
111 }; 114 };
112 115
113 [[maybe_unused]] const QSignalBlocker blocker(ui->tabWidget); 116 [[maybe_unused]] const QSignalBlocker blocker(ui->tabWidget);
diff --git a/src/yuzu/configuration/configure_service.cpp b/src/yuzu/configuration/configure_service.cpp
new file mode 100644
index 000000000..81c9e933f
--- /dev/null
+++ b/src/yuzu/configuration/configure_service.cpp
@@ -0,0 +1,136 @@
1// Copyright 2019 yuzu Emulator Project
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5#include <QGraphicsItem>
6#include <QtConcurrent/QtConcurrent>
7#include "core/hle/service/bcat/backend/boxcat.h"
8#include "core/settings.h"
9#include "ui_configure_service.h"
10#include "yuzu/configuration/configure_service.h"
11
12namespace {
13QString FormatEventStatusString(const Service::BCAT::EventStatus& status) {
14 QString out;
15
16 if (status.header.has_value()) {
17 out += QStringLiteral("<i>%1</i><br>").arg(QString::fromStdString(*status.header));
18 }
19
20 if (status.events.size() == 1) {
21 out += QStringLiteral("%1<br>").arg(QString::fromStdString(status.events.front()));
22 } else {
23 for (const auto& event : status.events) {
24 out += QStringLiteral("- %1<br>").arg(QString::fromStdString(event));
25 }
26 }
27
28 if (status.footer.has_value()) {
29 out += QStringLiteral("<i>%1</i><br>").arg(QString::fromStdString(*status.footer));
30 }
31
32 return out;
33}
34} // Anonymous namespace
35
36ConfigureService::ConfigureService(QWidget* parent)
37 : QWidget(parent), ui(std::make_unique<Ui::ConfigureService>()) {
38 ui->setupUi(this);
39
40 ui->bcat_source->addItem(QStringLiteral("None"));
41 ui->bcat_empty_label->setHidden(true);
42 ui->bcat_empty_header->setHidden(true);
43
44#ifdef YUZU_ENABLE_BOXCAT
45 ui->bcat_source->addItem(QStringLiteral("Boxcat"), QStringLiteral("boxcat"));
46#endif
47
48 connect(ui->bcat_source, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
49 &ConfigureService::OnBCATImplChanged);
50
51 this->SetConfiguration();
52}
53
54ConfigureService::~ConfigureService() = default;
55
56void ConfigureService::ApplyConfiguration() {
57 Settings::values.bcat_backend = ui->bcat_source->currentText().toLower().toStdString();
58}
59
60void ConfigureService::RetranslateUi() {
61 ui->retranslateUi(this);
62}
63
64void ConfigureService::SetConfiguration() {
65 const int index =
66 ui->bcat_source->findData(QString::fromStdString(Settings::values.bcat_backend));
67 ui->bcat_source->setCurrentIndex(index == -1 ? 0 : index);
68}
69
70std::pair<QString, QString> ConfigureService::BCATDownloadEvents() {
71 std::optional<std::string> global;
72 std::map<std::string, Service::BCAT::EventStatus> map;
73 const auto res = Service::BCAT::Boxcat::GetStatus(global, map);
74
75 switch (res) {
76 case Service::BCAT::Boxcat::StatusResult::Offline:
77 return {QString{},
78 tr("The boxcat service is offline or you are not connected to the internet.")};
79 case Service::BCAT::Boxcat::StatusResult::ParseError:
80 return {QString{},
81 tr("There was an error while processing the boxcat event data. Contact the yuzu "
82 "developers.")};
83 case Service::BCAT::Boxcat::StatusResult::BadClientVersion:
84 return {QString{},
85 tr("The version of yuzu you are using is either too new or too old for the server. "
86 "Try updating to the latest official release of yuzu.")};
87 }
88
89 if (map.empty()) {
90 return {QStringLiteral("Current Boxcat Events"),
91 tr("There are currently no events on boxcat.")};
92 }
93
94 QString out;
95
96 if (global.has_value()) {
97 out += QStringLiteral("%1<br>").arg(QString::fromStdString(*global));
98 }
99
100 for (const auto& [key, value] : map) {
101 out += QStringLiteral("%1<b>%2</b><br>%3")
102 .arg(out.isEmpty() ? QString{} : QStringLiteral("<br>"))
103 .arg(QString::fromStdString(key))
104 .arg(FormatEventStatusString(value));
105 }
106 return {QStringLiteral("Current Boxcat Events"), std::move(out)};
107}
108
109void ConfigureService::OnBCATImplChanged() {
110#ifdef YUZU_ENABLE_BOXCAT
111 const auto boxcat = ui->bcat_source->currentText() == QStringLiteral("Boxcat");
112 ui->bcat_empty_header->setHidden(!boxcat);
113 ui->bcat_empty_label->setHidden(!boxcat);
114 ui->bcat_empty_header->setText(QString{});
115 ui->bcat_empty_label->setText(tr("Yuzu is retrieving the latest boxcat status..."));
116
117 if (!boxcat)
118 return;
119
120 const auto future = QtConcurrent::run([this] { return BCATDownloadEvents(); });
121
122 watcher.setFuture(future);
123 connect(&watcher, &QFutureWatcher<std::pair<QString, QString>>::finished, this,
124 [this] { OnUpdateBCATEmptyLabel(watcher.result()); });
125#endif
126}
127
128void ConfigureService::OnUpdateBCATEmptyLabel(std::pair<QString, QString> string) {
129#ifdef YUZU_ENABLE_BOXCAT
130 const auto boxcat = ui->bcat_source->currentText() == QStringLiteral("Boxcat");
131 if (boxcat) {
132 ui->bcat_empty_header->setText(string.first);
133 ui->bcat_empty_label->setText(string.second);
134 }
135#endif
136}
diff --git a/src/yuzu/configuration/configure_service.h b/src/yuzu/configuration/configure_service.h
new file mode 100644
index 000000000..f5c1b703a
--- /dev/null
+++ b/src/yuzu/configuration/configure_service.h
@@ -0,0 +1,34 @@
1// Copyright 2019 yuzu Emulator Project
2// Licensed under GPLv2 or any later version
3// Refer to the license.txt file included.
4
5#pragma once
6
7#include <memory>
8#include <QFutureWatcher>
9#include <QWidget>
10
11namespace Ui {
12class ConfigureService;
13}
14
15class ConfigureService : public QWidget {
16 Q_OBJECT
17
18public:
19 explicit ConfigureService(QWidget* parent = nullptr);
20 ~ConfigureService() override;
21
22 void ApplyConfiguration();
23 void RetranslateUi();
24
25private:
26 void SetConfiguration();
27
28 std::pair<QString, QString> BCATDownloadEvents();
29 void OnBCATImplChanged();
30 void OnUpdateBCATEmptyLabel(std::pair<QString, QString> string);
31
32 std::unique_ptr<Ui::ConfigureService> ui;
33 QFutureWatcher<std::pair<QString, QString>> watcher{this};
34};
diff --git a/src/yuzu/configuration/configure_service.ui b/src/yuzu/configuration/configure_service.ui
new file mode 100644
index 000000000..9668dd557
--- /dev/null
+++ b/src/yuzu/configuration/configure_service.ui
@@ -0,0 +1,124 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<ui version="4.0">
3 <class>ConfigureService</class>
4 <widget class="QWidget" name="ConfigureService">
5 <property name="geometry">
6 <rect>
7 <x>0</x>
8 <y>0</y>
9 <width>433</width>
10 <height>561</height>
11 </rect>
12 </property>
13 <property name="windowTitle">
14 <string>Form</string>
15 </property>
16 <layout class="QVBoxLayout" name="verticalLayout">
17 <item>
18 <layout class="QVBoxLayout" name="verticalLayout_3">
19 <item>
20 <widget class="QGroupBox" name="groupBox">
21 <property name="title">
22 <string>BCAT</string>
23 </property>
24 <layout class="QGridLayout" name="gridLayout">
25 <item row="1" column="1" colspan="2">
26 <widget class="QLabel" name="label_2">
27 <property name="maximumSize">
28 <size>
29 <width>260</width>
30 <height>16777215</height>
31 </size>
32 </property>
33 <property name="text">
34 <string>BCAT is Nintendo's way of sending data to games to engage its community and unlock additional content.</string>
35 </property>
36 <property name="wordWrap">
37 <bool>true</bool>
38 </property>
39 </widget>
40 </item>
41 <item row="0" column="0">
42 <widget class="QLabel" name="label">
43 <property name="maximumSize">
44 <size>
45 <width>16777215</width>
46 <height>16777215</height>
47 </size>
48 </property>
49 <property name="text">
50 <string>BCAT Backend</string>
51 </property>
52 </widget>
53 </item>
54 <item row="3" column="1" colspan="2">
55 <widget class="QLabel" name="bcat_empty_label">
56 <property name="enabled">
57 <bool>true</bool>
58 </property>
59 <property name="maximumSize">
60 <size>
61 <width>260</width>
62 <height>16777215</height>
63 </size>
64 </property>
65 <property name="text">
66 <string/>
67 </property>
68 <property name="alignment">
69 <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
70 </property>
71 <property name="wordWrap">
72 <bool>true</bool>
73 </property>
74 </widget>
75 </item>
76 <item row="2" column="1" colspan="2">
77 <widget class="QLabel" name="label_3">
78 <property name="text">
79 <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://yuzu-emu.org/help/feature/boxcat&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#0000ff;&quot;&gt;Learn more about BCAT, Boxcat, and Current Events&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
80 </property>
81 <property name="openExternalLinks">
82 <bool>true</bool>
83 </property>
84 </widget>
85 </item>
86 <item row="0" column="1" colspan="2">
87 <widget class="QComboBox" name="bcat_source"/>
88 </item>
89 <item row="3" column="0">
90 <widget class="QLabel" name="bcat_empty_header">
91 <property name="text">
92 <string/>
93 </property>
94 <property name="alignment">
95 <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
96 </property>
97 <property name="wordWrap">
98 <bool>true</bool>
99 </property>
100 </widget>
101 </item>
102 </layout>
103 </widget>
104 </item>
105 </layout>
106 </item>
107 <item>
108 <spacer name="verticalSpacer">
109 <property name="orientation">
110 <enum>Qt::Vertical</enum>
111 </property>
112 <property name="sizeHint" stdset="0">
113 <size>
114 <width>20</width>
115 <height>40</height>
116 </size>
117 </property>
118 </spacer>
119 </item>
120 </layout>
121 </widget>
122 <resources/>
123 <connections/>
124</ui>
diff --git a/src/yuzu_cmd/config.cpp b/src/yuzu_cmd/config.cpp
index d82438502..1a812cb87 100644
--- a/src/yuzu_cmd/config.cpp
+++ b/src/yuzu_cmd/config.cpp
@@ -433,6 +433,11 @@ void Config::ReadValues() {
433 sdl2_config->Get("WebService", "web_api_url", "https://api.yuzu-emu.org"); 433 sdl2_config->Get("WebService", "web_api_url", "https://api.yuzu-emu.org");
434 Settings::values.yuzu_username = sdl2_config->Get("WebService", "yuzu_username", ""); 434 Settings::values.yuzu_username = sdl2_config->Get("WebService", "yuzu_username", "");
435 Settings::values.yuzu_token = sdl2_config->Get("WebService", "yuzu_token", ""); 435 Settings::values.yuzu_token = sdl2_config->Get("WebService", "yuzu_token", "");
436
437 // Services
438 Settings::values.bcat_backend = sdl2_config->Get("Services", "bcat_backend", "boxcat");
439 Settings::values.bcat_boxcat_local =
440 sdl2_config->GetBoolean("Services", "bcat_boxcat_local", false);
436} 441}
437 442
438void Config::Reload() { 443void Config::Reload() {
diff --git a/src/yuzu_cmd/default_ini.h b/src/yuzu_cmd/default_ini.h
index a6171c3ed..8d18a4a5a 100644
--- a/src/yuzu_cmd/default_ini.h
+++ b/src/yuzu_cmd/default_ini.h
@@ -251,6 +251,11 @@ web_api_url = https://api.yuzu-emu.org
251yuzu_username = 251yuzu_username =
252yuzu_token = 252yuzu_token =
253 253
254[Services]
255# The name of the backend to use for BCAT
256# If this is set to 'boxcat' boxcat will be used, otherwise a null implementation will be used
257bcat_backend =
258
254[AddOns] 259[AddOns]
255# Used to disable add-ons 260# Used to disable add-ons
256# List of title IDs of games that will have add-ons disabled (separated by '|'): 261# List of title IDs of games that will have add-ons disabled (separated by '|'):