Merge pull request #2539 from DarkLordZach/bcat
bcat: Implement BCAT service and connect to yuzu Boxcat servermaster
commit
9aac7fbc22
@ -0,0 +1 @@
|
||||
Subproject commit bd7a8103e96bc6d50164447f6b7b57bb786d8e2a
|
@ -0,0 +1 @@
|
||||
Subproject commit 094ed57db392170130bc710293568de7b576306d
|
@ -0,0 +1,79 @@
|
||||
// Copyright 2019 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <string>
|
||||
#include <zip.h>
|
||||
#include "common/logging/backend.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
#include "core/file_sys/vfs_libzip.h"
|
||||
#include "core/file_sys/vfs_vector.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
VirtualDir ExtractZIP(VirtualFile file) {
|
||||
zip_error_t error{};
|
||||
|
||||
const auto data = file->ReadAllBytes();
|
||||
std::unique_ptr<zip_source_t, decltype(&zip_source_close)> src{
|
||||
zip_source_buffer_create(data.data(), data.size(), 0, &error), zip_source_close};
|
||||
if (src == nullptr)
|
||||
return nullptr;
|
||||
|
||||
std::unique_ptr<zip_t, decltype(&zip_close)> zip{zip_open_from_source(src.get(), 0, &error),
|
||||
zip_close};
|
||||
if (zip == nullptr)
|
||||
return nullptr;
|
||||
|
||||
std::shared_ptr<VectorVfsDirectory> out = std::make_shared<VectorVfsDirectory>();
|
||||
|
||||
const auto num_entries = zip_get_num_entries(zip.get(), 0);
|
||||
|
||||
zip_stat_t stat{};
|
||||
zip_stat_init(&stat);
|
||||
|
||||
for (std::size_t i = 0; i < num_entries; ++i) {
|
||||
const auto stat_res = zip_stat_index(zip.get(), i, 0, &stat);
|
||||
if (stat_res == -1)
|
||||
return nullptr;
|
||||
|
||||
const std::string name(stat.name);
|
||||
if (name.empty())
|
||||
continue;
|
||||
|
||||
if (name.back() != '/') {
|
||||
std::unique_ptr<zip_file_t, decltype(&zip_fclose)> file{
|
||||
zip_fopen_index(zip.get(), i, 0), zip_fclose};
|
||||
|
||||
std::vector<u8> buf(stat.size);
|
||||
if (zip_fread(file.get(), buf.data(), buf.size()) != buf.size())
|
||||
return nullptr;
|
||||
|
||||
const auto parts = FileUtil::SplitPathComponents(stat.name);
|
||||
const auto new_file = std::make_shared<VectorVfsFile>(buf, parts.back());
|
||||
|
||||
std::shared_ptr<VectorVfsDirectory> dtrv = out;
|
||||
for (std::size_t j = 0; j < parts.size() - 1; ++j) {
|
||||
if (dtrv == nullptr)
|
||||
return nullptr;
|
||||
const auto subdir = dtrv->GetSubdirectory(parts[j]);
|
||||
if (subdir == nullptr) {
|
||||
const auto temp = std::make_shared<VectorVfsDirectory>(
|
||||
std::vector<VirtualFile>{}, std::vector<VirtualDir>{}, parts[j]);
|
||||
dtrv->AddDirectory(temp);
|
||||
dtrv = temp;
|
||||
} else {
|
||||
dtrv = std::dynamic_pointer_cast<VectorVfsDirectory>(subdir);
|
||||
}
|
||||
}
|
||||
|
||||
if (dtrv == nullptr)
|
||||
return nullptr;
|
||||
dtrv->AddFile(new_file);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace FileSys
|
@ -0,0 +1,13 @@
|
||||
// Copyright 2019 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "core/file_sys/vfs_types.h"
|
||||
|
||||
namespace FileSys {
|
||||
|
||||
VirtualDir ExtractZIP(VirtualFile zip);
|
||||
|
||||
} // namespace FileSys
|
@ -0,0 +1,136 @@
|
||||
// Copyright 2019 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include "common/hex_util.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/core.h"
|
||||
#include "core/hle/lock.h"
|
||||
#include "core/hle/service/bcat/backend/backend.h"
|
||||
|
||||
namespace Service::BCAT {
|
||||
|
||||
ProgressServiceBackend::ProgressServiceBackend(std::string event_name) : impl{} {
|
||||
auto& kernel{Core::System::GetInstance().Kernel()};
|
||||
event = Kernel::WritableEvent::CreateEventPair(
|
||||
kernel, Kernel::ResetType::Automatic, "ProgressServiceBackend:UpdateEvent:" + event_name);
|
||||
}
|
||||
|
||||
Kernel::SharedPtr<Kernel::ReadableEvent> ProgressServiceBackend::GetEvent() {
|
||||
return event.readable;
|
||||
}
|
||||
|
||||
DeliveryCacheProgressImpl& ProgressServiceBackend::GetImpl() {
|
||||
return impl;
|
||||
}
|
||||
|
||||
void ProgressServiceBackend::SetNeedHLELock(bool need) {
|
||||
need_hle_lock = need;
|
||||
}
|
||||
|
||||
void ProgressServiceBackend::SetTotalSize(u64 size) {
|
||||
impl.total_bytes = size;
|
||||
SignalUpdate();
|
||||
}
|
||||
|
||||
void ProgressServiceBackend::StartConnecting() {
|
||||
impl.status = DeliveryCacheProgressImpl::Status::Connecting;
|
||||
SignalUpdate();
|
||||
}
|
||||
|
||||
void ProgressServiceBackend::StartProcessingDataList() {
|
||||
impl.status = DeliveryCacheProgressImpl::Status::ProcessingDataList;
|
||||
SignalUpdate();
|
||||
}
|
||||
|
||||
void ProgressServiceBackend::StartDownloadingFile(std::string_view dir_name,
|
||||
std::string_view file_name, u64 file_size) {
|
||||
impl.status = DeliveryCacheProgressImpl::Status::Downloading;
|
||||
impl.current_downloaded_bytes = 0;
|
||||
impl.current_total_bytes = file_size;
|
||||
std::memcpy(impl.current_directory.data(), dir_name.data(),
|
||||
std::min<u64>(dir_name.size(), 0x31ull));
|
||||
std::memcpy(impl.current_file.data(), file_name.data(),
|
||||
std::min<u64>(file_name.size(), 0x31ull));
|
||||
SignalUpdate();
|
||||
}
|
||||
|
||||
void ProgressServiceBackend::UpdateFileProgress(u64 downloaded) {
|
||||
impl.current_downloaded_bytes = downloaded;
|
||||
SignalUpdate();
|
||||
}
|
||||
|
||||
void ProgressServiceBackend::FinishDownloadingFile() {
|
||||
impl.total_downloaded_bytes += impl.current_total_bytes;
|
||||
SignalUpdate();
|
||||
}
|
||||
|
||||
void ProgressServiceBackend::CommitDirectory(std::string_view dir_name) {
|
||||
impl.status = DeliveryCacheProgressImpl::Status::Committing;
|
||||
impl.current_file.fill(0);
|
||||
impl.current_downloaded_bytes = 0;
|
||||
impl.current_total_bytes = 0;
|
||||
std::memcpy(impl.current_directory.data(), dir_name.data(),
|
||||
std::min<u64>(dir_name.size(), 0x31ull));
|
||||
SignalUpdate();
|
||||
}
|
||||
|
||||
void ProgressServiceBackend::FinishDownload(ResultCode result) {
|
||||
impl.total_downloaded_bytes = impl.total_bytes;
|
||||
impl.status = DeliveryCacheProgressImpl::Status::Done;
|
||||
impl.result = result;
|
||||
SignalUpdate();
|
||||
}
|
||||
|
||||
void ProgressServiceBackend::SignalUpdate() const {
|
||||
if (need_hle_lock) {
|
||||
std::lock_guard<std::recursive_mutex> lock(HLE::g_hle_lock);
|
||||
event.writable->Signal();
|
||||
} else {
|
||||
event.writable->Signal();
|
||||
}
|
||||
}
|
||||
|
||||
Backend::Backend(DirectoryGetter getter) : dir_getter(std::move(getter)) {}
|
||||
|
||||
Backend::~Backend() = default;
|
||||
|
||||
NullBackend::NullBackend(const DirectoryGetter& getter) : Backend(std::move(getter)) {}
|
||||
|
||||
NullBackend::~NullBackend() = default;
|
||||
|
||||
bool NullBackend::Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) {
|
||||
LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, build_id={:016X}", title.title_id,
|
||||
title.build_id);
|
||||
|
||||
progress.FinishDownload(RESULT_SUCCESS);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NullBackend::SynchronizeDirectory(TitleIDVersion title, std::string name,
|
||||
ProgressServiceBackend& progress) {
|
||||
LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, build_id={:016X}, name={}", title.title_id,
|
||||
title.build_id, name);
|
||||
|
||||
progress.FinishDownload(RESULT_SUCCESS);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool NullBackend::Clear(u64 title_id) {
|
||||
LOG_DEBUG(Service_BCAT, "called, title_id={:016X}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void NullBackend::SetPassphrase(u64 title_id, const Passphrase& passphrase) {
|
||||
LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, passphrase = {}", title_id,
|
||||
Common::HexToString(passphrase));
|
||||
}
|
||||
|
||||
std::optional<std::vector<u8>> NullBackend::GetLaunchParameter(TitleIDVersion title) {
|
||||
LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, build_id={:016X}", title.title_id,
|
||||
title.build_id);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace Service::BCAT
|
@ -0,0 +1,147 @@
|
||||
// Copyright 2019 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include "common/common_types.h"
|
||||
#include "core/file_sys/vfs_types.h"
|
||||
#include "core/hle/kernel/readable_event.h"
|
||||
#include "core/hle/kernel/writable_event.h"
|
||||
#include "core/hle/result.h"
|
||||
|
||||
namespace Service::BCAT {
|
||||
|
||||
struct DeliveryCacheProgressImpl;
|
||||
|
||||
using DirectoryGetter = std::function<FileSys::VirtualDir(u64)>;
|
||||
using Passphrase = std::array<u8, 0x20>;
|
||||
|
||||
struct TitleIDVersion {
|
||||
u64 title_id;
|
||||
u64 build_id;
|
||||
};
|
||||
|
||||
using DirectoryName = std::array<char, 0x20>;
|
||||
using FileName = std::array<char, 0x20>;
|
||||
|
||||
struct DeliveryCacheProgressImpl {
|
||||
enum class Status : s32 {
|
||||
None = 0x0,
|
||||
Queued = 0x1,
|
||||
Connecting = 0x2,
|
||||
ProcessingDataList = 0x3,
|
||||
Downloading = 0x4,
|
||||
Committing = 0x5,
|
||||
Done = 0x9,
|
||||
};
|
||||
|
||||
Status status;
|
||||
ResultCode result = RESULT_SUCCESS;
|
||||
DirectoryName current_directory;
|
||||
FileName current_file;
|
||||
s64 current_downloaded_bytes; ///< Bytes downloaded on current file.
|
||||
s64 current_total_bytes; ///< Bytes total on current file.
|
||||
s64 total_downloaded_bytes; ///< Bytes downloaded on overall download.
|
||||
s64 total_bytes; ///< Bytes total on overall download.
|
||||
INSERT_PADDING_BYTES(
|
||||
0x198); ///< Appears to be unused in official code, possibly reserved for future use.
|
||||
};
|
||||
static_assert(sizeof(DeliveryCacheProgressImpl) == 0x200,
|
||||
"DeliveryCacheProgressImpl has incorrect size.");
|
||||
|
||||
// A class to manage the signalling to the game about BCAT download progress.
|
||||
// Some of this class is implemented in module.cpp to avoid exposing the implementation structure.
|
||||
class ProgressServiceBackend {
|
||||
friend class IBcatService;
|
||||
|
||||
public:
|
||||
// Clients should call this with true if any of the functions are going to be called from a
|
||||
// non-HLE thread and this class need to lock the hle mutex. (default is false)
|
||||
void SetNeedHLELock(bool need);
|
||||
|
||||
// Sets the number of bytes total in the entire download.
|
||||
void SetTotalSize(u64 size);
|
||||
|
||||
// Notifies the application that the backend has started connecting to the server.
|
||||
void StartConnecting();
|
||||
// Notifies the application that the backend has begun accumulating and processing metadata.
|
||||
void StartProcessingDataList();
|
||||
|
||||
// Notifies the application that a file is starting to be downloaded.
|
||||
void StartDownloadingFile(std::string_view dir_name, std::string_view file_name, u64 file_size);
|
||||
// Updates the progress of the current file to the size passed.
|
||||
void UpdateFileProgress(u64 downloaded);
|
||||
// Notifies the application that the current file has completed download.
|
||||
void FinishDownloadingFile();
|
||||
|
||||
// Notifies the application that all files in this directory have completed and are being
|
||||
// finalized.
|
||||
void CommitDirectory(std::string_view dir_name);
|
||||
|
||||
// Notifies the application that the operation completed with result code result.
|
||||
void FinishDownload(ResultCode result);
|
||||
|
||||
private:
|
||||
explicit ProgressServiceBackend(std::string event_name);
|
||||
|
||||
Kernel::SharedPtr<Kernel::ReadableEvent> GetEvent();
|
||||
DeliveryCacheProgressImpl& GetImpl();
|
||||
|
||||
void SignalUpdate() const;
|
||||
|
||||
DeliveryCacheProgressImpl impl;
|
||||
Kernel::EventPair event;
|
||||
bool need_hle_lock = false;
|
||||
};
|
||||
|
||||
// A class representing an abstract backend for BCAT functionality.
|
||||
class Backend {
|
||||
public:
|
||||
explicit Backend(DirectoryGetter getter);
|
||||
virtual ~Backend();
|
||||
|
||||
// Called when the backend is needed to synchronize the data for the game with title ID and
|
||||
// version in title. A ProgressServiceBackend object is provided to alert the application of
|
||||
// status.
|
||||
virtual bool Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) = 0;
|
||||
// Very similar to Synchronize, but only for the directory provided. Backends should not alter
|
||||
// the data for any other directories.
|
||||
virtual bool SynchronizeDirectory(TitleIDVersion title, std::string name,
|
||||
ProgressServiceBackend& progress) = 0;
|
||||
|
||||
// Removes all cached data associated with title id provided.
|
||||
virtual bool Clear(u64 title_id) = 0;
|
||||
|
||||
// Sets the BCAT Passphrase to be used with the associated title ID.
|
||||
virtual void SetPassphrase(u64 title_id, const Passphrase& passphrase) = 0;
|
||||
|
||||
// Gets the launch parameter used by AM associated with the title ID and version provided.
|
||||
virtual std::optional<std::vector<u8>> GetLaunchParameter(TitleIDVersion title) = 0;
|
||||
|
||||
protected:
|
||||
DirectoryGetter dir_getter;
|
||||
};
|
||||
|
||||
// A backend of BCAT that provides no operation.
|
||||
class NullBackend : public Backend {
|
||||
public:
|
||||
explicit NullBackend(const DirectoryGetter& getter);
|
||||
~NullBackend() override;
|
||||
|
||||
bool Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) override;
|
||||
bool SynchronizeDirectory(TitleIDVersion title, std::string name,
|
||||
ProgressServiceBackend& progress) override;
|
||||
|
||||
bool Clear(u64 title_id) override;
|
||||
|
||||
void SetPassphrase(u64 title_id, const Passphrase& passphrase) override;
|
||||
|
||||
std::optional<std::vector<u8>> GetLaunchParameter(TitleIDVersion title) override;
|
||||
};
|
||||
|
||||
std::unique_ptr<Backend> CreateBackendFromSettings(DirectoryGetter getter);
|
||||
|
||||
} // namespace Service::BCAT
|
@ -0,0 +1,503 @@
|
||||
// Copyright 2019 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <fmt/ostream.h>
|
||||
#include <httplib.h>
|
||||
#include <json.hpp>
|
||||
#include <mbedtls/sha256.h>
|
||||
#include "common/hex_util.h"
|
||||
#include "common/logging/backend.h"
|
||||
#include "common/logging/log.h"
|
||||
#include "core/core.h"
|
||||
#include "core/file_sys/vfs.h"
|
||||
#include "core/file_sys/vfs_libzip.h"
|
||||
#include "core/file_sys/vfs_vector.h"
|
||||
#include "core/frontend/applets/error.h"
|
||||
#include "core/hle/service/am/applets/applets.h"
|
||||
#include "core/hle/service/bcat/backend/boxcat.h"
|
||||
#include "core/settings.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Prevents conflicts with windows macro called CreateFile
|
||||
FileSys::VirtualFile VfsCreateFileWrap(FileSys::VirtualDir dir, std::string_view name) {
|
||||
return dir->CreateFile(name);
|
||||
}
|
||||
|
||||
// Prevents conflicts with windows macro called DeleteFile
|
||||
bool VfsDeleteFileWrap(FileSys::VirtualDir dir, std::string_view name) {
|
||||
return dir->DeleteFile(name);
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
namespace Service::BCAT {
|
||||
|
||||
constexpr ResultCode ERROR_GENERAL_BCAT_FAILURE{ErrorModule::BCAT, 1};
|
||||
|
||||
constexpr char BOXCAT_HOSTNAME[] = "api.yuzu-emu.org";
|
||||
|
||||
// Formatted using fmt with arg[0] = hex title id
|
||||
constexpr char BOXCAT_PATHNAME_DATA[] = "/game-assets/{:016X}/boxcat";
|
||||
constexpr char BOXCAT_PATHNAME_LAUNCHPARAM[] = "/game-assets/{:016X}/launchparam";
|
||||
|
||||
constexpr char BOXCAT_PATHNAME_EVENTS[] = "/game-assets/boxcat/events";
|
||||
|
||||
constexpr char BOXCAT_API_VERSION[] = "1";
|
||||
constexpr char BOXCAT_CLIENT_TYPE[] = "yuzu";
|
||||
|
||||
// HTTP status codes for Boxcat
|
||||
enum class ResponseStatus {
|
||||
Ok = 200, ///< Operation completed successfully.
|
||||
BadClientVersion = 301, ///< The Boxcat-Client-Version doesn't match the server.
|
||||
NoUpdate = 304, ///< The digest provided would match the new data, no need to update.
|
||||
NoMatchTitleId = 404, ///< The title ID provided doesn't have a boxcat implementation.
|
||||
NoMatchBuildId = 406, ///< The build ID provided is blacklisted (potentially because of format
|
||||
///< issues or whatnot) and has no data.
|
||||
};
|
||||
|
||||
enum class DownloadResult {
|
||||
Success = 0,
|
||||
NoResponse,
|
||||
GeneralWebError,
|
||||
NoMatchTitleId,
|
||||
NoMatchBuildId,
|
||||
InvalidContentType,
|
||||
GeneralFSError,
|
||||
BadClientVersion,
|
||||
};
|
||||
|
||||
constexpr std::array<const char*, 8> DOWNLOAD_RESULT_LOG_MESSAGES{
|
||||
"Success",
|
||||
"There was no response from the server.",
|
||||
"There was a general web error code returned from the server.",
|
||||
"The title ID of the current game doesn't have a boxcat implementation. If you believe an "
|
||||
"implementation should be added, contact yuzu support.",
|
||||
"The build ID of the current version of the game is marked as incompatible with the current "
|
||||
"BCAT distribution. Try upgrading or downgrading your game version or contacting yuzu support.",
|
||||
"The content type of the web response was invalid.",
|
||||
"There was a general filesystem error while saving the zip file.",
|
||||
"The server is either too new or too old to serve the request. Try using the latest version of "
|
||||
"an official release of yuzu.",
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, DownloadResult result) {
|
||||
return os << DOWNLOAD_RESULT_LOG_MESSAGES.at(static_cast<std::size_t>(result));
|
||||
}
|
||||
|
||||
constexpr u32 PORT = 443;
|
||||
constexpr u32 TIMEOUT_SECONDS = 30;
|
||||
constexpr u64 VFS_COPY_BLOCK_SIZE = 1ull << 24; // 4MB
|
||||
|
||||
namespace {
|
||||
|
||||
std::string GetBINFilePath(u64 title_id) {
|
||||
return fmt::format("{}bcat/{:016X}/launchparam.bin",
|
||||
FileUtil::GetUserPath(FileUtil::UserPath::CacheDir), title_id);
|
||||
}
|
||||
|
||||
std::string GetZIPFilePath(u64 title_id) {
|
||||
return fmt::format("{}bcat/{:016X}/data.zip",
|
||||
FileUtil::GetUserPath(FileUtil::UserPath::CacheDir), title_id);
|
||||
}
|
||||
|
||||
// If the error is something the user should know about (build ID mismatch, bad client version),
|
||||
// display an error.
|
||||
void HandleDownloadDisplayResult(DownloadResult res) {
|
||||
if (res == DownloadResult::Success || res == DownloadResult::NoResponse ||
|
||||
res == DownloadResult::GeneralWebError || res == DownloadResult::GeneralFSError ||
|
||||
res == DownloadResult::NoMatchTitleId || res == DownloadResult::InvalidContentType) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& frontend{Core::System::GetInstance().GetAppletManager().GetAppletFrontendSet()};
|
||||
frontend.error->ShowCustomErrorText(
|
||||
ResultCode(-1), "There was an error while attempting to use Boxcat.",
|
||||
DOWNLOAD_RESULT_LOG_MESSAGES[static_cast<std::size_t>(res)], [] {});
|
||||
}
|
||||
|
||||
bool VfsRawCopyProgress(FileSys::VirtualFile src, FileSys::VirtualFile dest,
|
||||
std::string_view dir_name, ProgressServiceBackend& progress,
|
||||
std::size_t block_size = 0x1000) {
|
||||
if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable())
|
||||
return false;
|
||||
if (!dest->Resize(src->GetSize()))
|
||||
return false;
|
||||
|
||||
progress.StartDownloadingFile(dir_name, src->GetName(), src->GetSize());
|
||||
|
||||
std::vector<u8> temp(std::min(block_size, src->GetSize()));
|
||||
for (std::size_t i = 0; i < src->GetSize(); i += block_size) {
|
||||
const auto read = std::min(block_size, src->GetSize() - i);
|
||||
|
||||
if (src->Read(temp.data(), read, i) != read) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dest->Write(temp.data(), read, i) != read) {
|
||||
return false;
|
||||
}
|
||||
|
||||
progress.UpdateFileProgress(i);
|
||||
}
|
||||
|
||||
progress.FinishDownloadingFile();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VfsRawCopyDProgressSingle(FileSys::VirtualDir src, FileSys::VirtualDir dest,
|
||||
ProgressServiceBackend& progress, std::size_t block_size = 0x1000) {
|
||||
if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable())
|
||||
return false;
|
||||
|
||||
for (const auto& file : src->GetFiles()) {
|
||||
const auto out_file = VfsCreateFileWrap(dest, file->GetName());
|
||||
if (!VfsRawCopyProgress(file, out_file, src->GetName(), progress, block_size)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
progress.CommitDirectory(src->GetName());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool VfsRawCopyDProgress(FileSys::VirtualDir src, FileSys::VirtualDir dest,
|
||||
ProgressServiceBackend& progress, std::size_t block_size = 0x1000) {
|
||||
if (src == nullptr || dest == nullptr || !src->IsReadable() || !dest->IsWritable())
|
||||
return false;
|
||||
|
||||
for (const auto& dir : src->GetSubdirectories()) {
|
||||
const auto out = dest->CreateSubdirectory(dir->GetName());
|
||||
if (!VfsRawCopyDProgressSingle(dir, out, progress, block_size)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // Anonymous namespace
|
||||
|
||||
class Boxcat::Client {
|
||||
public:
|
||||
Client(std::string path, u64 title_id, u64 build_id)
|
||||
: path(std::move(path)), title_id(title_id), build_id(build_id) {}
|
||||
|
||||
DownloadResult DownloadDataZip() {
|
||||
return DownloadInternal(fmt::format(BOXCAT_PATHNAME_DATA, title_id), TIMEOUT_SECONDS,
|
||||
"application/zip");
|
||||
}
|
||||
|
||||
DownloadResult DownloadLaunchParam() {
|
||||
return DownloadInternal(fmt::format(BOXCAT_PATHNAME_LAUNCHPARAM, title_id),
|
||||
TIMEOUT_SECONDS / 3, "application/octet-stream");
|
||||
}
|
||||
|
||||
private:
|
||||
DownloadResult DownloadInternal(const std::string& resolved_path, u32 timeout_seconds,
|
||||
const std::string& content_type_name) {
|
||||
if (client == nullptr) {
|
||||
client = std::make_unique<httplib::SSLClient>(BOXCAT_HOSTNAME, PORT, timeout_seconds);
|
||||
}
|
||||
|
||||
httplib::Headers headers{
|
||||
{std::string("Game-Assets-API-Version"), std::string(BOXCAT_API_VERSION)},
|
||||
{std::string("Boxcat-Client-Type"), std::string(BOXCAT_CLIENT_TYPE)},
|
||||
{std::string("Game-Build-Id"), fmt::format("{:016X}", build_id)},
|
||||
};
|
||||
|
||||
if (FileUtil::Exists(path)) {
|
||||
FileUtil::IOFile file{path, "rb"};
|
||||
if (file.IsOpen()) {
|
||||
std::vector<u8> bytes(file.GetSize());
|
||||
file.ReadBytes(bytes.data(), bytes.size());
|
||||
const auto digest = DigestFile(bytes);
|
||||
headers.insert({std::string("If-None-Match"), Common::HexToString(digest, false)});
|
||||
}
|
||||
}
|
||||
|
||||
const auto response = client->Get(resolved_path.c_str(), headers);
|
||||
if (response == nullptr)
|
||||
return DownloadResult::NoResponse;
|
||||
|
||||
if (response->status == static_cast<int>(ResponseStatus::NoUpdate))
|
||||
return DownloadResult::Success;
|
||||
if (response->status == static_cast<int>(ResponseStatus::BadClientVersion))
|
||||
return DownloadResult::BadClientVersion;
|
||||
if (response->status == static_cast<int>(ResponseStatus::NoMatchTitleId))
|
||||
return DownloadResult::NoMatchTitleId;
|
||||
if (response->status == static_cast<int>(ResponseStatus::NoMatchBuildId))
|
||||
return DownloadResult::NoMatchBuildId;
|
||||
if (response->status != static_cast<int>(ResponseStatus::Ok))
|
||||
return DownloadResult::GeneralWebError;
|
||||
|
||||
const auto content_type = response->headers.find("content-type");
|
||||
if (content_type == response->headers.end() ||
|
||||
content_type->second.find(content_type_name) == std::string::npos) {
|
||||
return DownloadResult::InvalidContentType;
|
||||
}
|
||||
|
||||
FileUtil::CreateFullPath(path);
|
||||
FileUtil::IOFile file{path, "wb"};
|
||||
if (!file.IsOpen())
|
||||
return DownloadResult::GeneralFSError;
|
||||
if (!file.Resize(response->body.size()))
|
||||
return DownloadResult::GeneralFSError;
|
||||
if (file.WriteBytes(response->body.data(), response->body.size()) != response->body.size())
|
||||
return DownloadResult::GeneralFSError;
|
||||
|
||||
return DownloadResult::Success;
|
||||
}
|
||||
|
||||
using Digest = std::array<u8, 0x20>;
|
||||
static Digest DigestFile(std::vector<u8> bytes) {
|
||||
Digest out{};
|
||||
mbedtls_sha256(bytes.data(), bytes.size(), out.data(), 0);
|
||||
return out;
|
||||
}
|
||||
|
||||
std::unique_ptr<httplib::Client> client;
|
||||
std::string path;
|
||||
u64 title_id;
|
||||
u64 build_id;
|
||||
};
|
||||
|
||||
Boxcat::Boxcat(DirectoryGetter getter) : Backend(std::move(getter)) {}
|
||||
|
||||
Boxcat::~Boxcat() = default;
|
||||
|
||||
void SynchronizeInternal(DirectoryGetter dir_getter, TitleIDVersion title,
|
||||
ProgressServiceBackend& progress,
|
||||
std::optional<std::string> dir_name = {}) {
|
||||
progress.SetNeedHLELock(true);
|
||||
|
||||
if (Settings::values.bcat_boxcat_local) {
|
||||
LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping download.");
|
||||
const auto dir = dir_getter(title.title_id);
|
||||
if (dir)
|
||||
progress.SetTotalSize(dir->GetSize());
|
||||
progress.FinishDownload(RESULT_SUCCESS);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto zip_path{GetZIPFilePath(title.title_id)};
|
||||
Boxcat::Client client{zip_path, title.title_id, title.build_id};
|
||||
|
||||
progress.StartConnecting();
|
||||
|
||||
const auto res = client.DownloadDataZip();
|
||||
if (res != DownloadResult::Success) {
|
||||
LOG_ERROR(Service_BCAT, "Boxcat synchronization failed with error '{}'!", res);
|
||||
|
||||
if (res == DownloadResult::NoMatchBuildId || res == DownloadResult::NoMatchTitleId) {
|
||||
FileUtil::Delete(zip_path);
|
||||
}
|
||||
|
||||
HandleDownloadDisplayResult(res);
|
||||
progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
progress.StartProcessingDataList();
|
||||
|
||||
FileUtil::IOFile zip{zip_path, "rb"};
|
||||
const auto size = zip.GetSize();
|
||||
std::vector<u8> bytes(size);
|
||||
if (!zip.IsOpen() || size == 0 || zip.ReadBytes(bytes.data(), bytes.size()) != bytes.size()) {
|
||||
LOG_ERROR(Service_BCAT, "Boxcat failed to read ZIP file at path '{}'!", zip_path);
|
||||
progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto extracted = FileSys::ExtractZIP(std::make_shared<FileSys::VectorVfsFile>(bytes));
|
||||
if (extracted == nullptr) {
|
||||
LOG_ERROR(Service_BCAT, "Boxcat failed to extract ZIP file!");
|
||||
progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
if (dir_name == std::nullopt) {
|
||||
progress.SetTotalSize(extracted->GetSize());
|
||||
|
||||
const auto target_dir = dir_getter(title.title_id);
|
||||
if (target_dir == nullptr || !VfsRawCopyDProgress(extracted, target_dir, progress)) {
|
||||
LOG_ERROR(Service_BCAT, "Boxcat failed to copy extracted ZIP to target directory!");
|
||||
progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const auto target_dir = dir_getter(title.title_id);
|
||||
if (target_dir == nullptr) {
|
||||
LOG_ERROR(Service_BCAT, "Boxcat failed to get directory for title ID!");
|
||||
progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto target_sub = target_dir->GetSubdirectory(*dir_name);
|
||||
const auto source_sub = extracted->GetSubdirectory(*dir_name);
|
||||
|
||||
progress.SetTotalSize(source_sub->GetSize());
|
||||
|
||||
std::vector<std::string> filenames;
|
||||
{
|
||||
const auto files = target_sub->GetFiles();
|
||||
std::transform(files.begin(), files.end(), std::back_inserter(filenames),
|
||||
[](const auto& vfile) { return vfile->GetName(); });
|
||||
}
|
||||
|
||||
for (const auto& filename : filenames) {
|
||||
VfsDeleteFileWrap(target_sub, filename);
|
||||
}
|
||||
|
||||
if (target_sub == nullptr || source_sub == nullptr ||
|
||||
!VfsRawCopyDProgressSingle(source_sub, target_sub, progress)) {
|
||||
LOG_ERROR(Service_BCAT, "Boxcat failed to copy extracted ZIP to target directory!");
|
||||
progress.FinishDownload(ERROR_GENERAL_BCAT_FAILURE);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
progress.FinishDownload(RESULT_SUCCESS);
|
||||
}
|
||||
|
||||
bool Boxcat::Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) {
|
||||
is_syncing.exchange(true);
|
||||
std::thread([this, title, &progress] { SynchronizeInternal(dir_getter, title, progress); })
|
||||
.detach();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Boxcat::SynchronizeDirectory(TitleIDVersion title, std::string name,
|
||||
ProgressServiceBackend& progress) {
|
||||
is_syncing.exchange(true);
|
||||
std::thread(
|
||||
[this, title, name, &progress] { SynchronizeInternal(dir_getter, title, progress, name); })
|
||||
.detach();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Boxcat::Clear(u64 title_id) {
|
||||
if (Settings::values.bcat_boxcat_local) {
|
||||
LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping clear.");
|
||||
return true;
|
||||
}
|
||||
|
||||
const auto dir = dir_getter(title_id);
|
||||
|
||||
std::vector<std::string> dirnames;
|
||||
|
||||
for (const auto& subdir : dir->GetSubdirectories())
|
||||
dirnames.push_back(subdir->GetName());
|
||||
|
||||
for (const auto& subdir : dirnames) {
|
||||
if (!dir->DeleteSubdirectoryRecursive(subdir))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Boxcat::SetPassphrase(u64 title_id, const Passphrase& passphrase) {
|
||||
LOG_DEBUG(Service_BCAT, "called, title_id={:016X}, passphrase={}", title_id,
|
||||
Common::HexToString(passphrase));
|
||||
}
|
||||
|
||||
std::optional<std::vector<u8>> Boxcat::GetLaunchParameter(TitleIDVersion title) {
|
||||
const auto path{GetBINFilePath(title.title_id)};
|
||||
|
||||
if (Settings::values.bcat_boxcat_local) {
|
||||
LOG_INFO(Service_BCAT, "Boxcat using local data by override, skipping download.");
|
||||
} else {
|
||||
Boxcat::Client client{path, title.title_id, title.build_id};
|
||||
|
||||
const auto res = client.DownloadLaunchParam();
|
||||
if (res != DownloadResult::Success) {
|
||||
LOG_ERROR(Service_BCAT, "Boxcat synchronization failed with error '{}'!", res);
|
||||
|
||||
if (res == DownloadResult::NoMatchBuildId || res == DownloadResult::NoMatchTitleId) {
|
||||
FileUtil::Delete(path);
|
||||
}
|
||||
|
||||
HandleDownloadDisplayResult(res);
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
FileUtil::IOFile bin{path, "rb"};
|
||||
const auto size = bin.GetSize();
|
||||
std::vector<u8> bytes(size);
|
||||
if (!bin.IsOpen() || size == 0 || bin.ReadBytes(bytes.data(), bytes.size()) != bytes.size()) {
|
||||
LOG_ERROR(Service_BCAT, "Boxcat failed to read launch parameter binary at path '{}'!",
|
||||
path);
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
Boxcat::StatusResult Boxcat::GetStatus(std::optional<std::string>& global,
|
||||
std::map<std::string, EventStatus>& games) {
|
||||
httplib::SSLClient client{BOXCAT_HOSTNAME, static_cast<int>(PORT),
|
||||
static_cast<int>(TIMEOUT_SECONDS)};
|
||||
|
||||
httplib::Headers headers{
|
||||
{std::string("Game-Assets-API-Version"), std::string(BOXCAT_API_VERSION)},
|
||||
{std::string("Boxcat-Client-Type"), std::string(BOXCAT_CLIENT_TYPE)},
|
||||
};
|
||||
|
||||
const auto response = client.Get(BOXCAT_PATHNAME_EVENTS, headers);
|
||||
if (response == nullptr)
|
||||
return StatusResult::Offline;
|
||||
|
||||
if (response->status == static_cast<int>(ResponseStatus::BadClientVersion))
|
||||
return StatusResult::BadClientVersion;
|
||||
|
||||
try {
|
||||
nlohmann::json json = nlohmann::json::parse(response->body);
|
||||
|
||||
if (!json["online"].get<bool>())
|
||||
return StatusResult::Offline;
|
||||
|
||||
if (json["global"].is_null())
|
||||
global = std::nullopt;
|
||||
else
|
||||
global = json["global"].get<std::string>();
|
||||
|
||||
if (json["games"].is_array()) {
|
||||
for (const auto object : json["games"]) {
|
||||
if (object.is_object() && object.find("name") != object.end()) {
|
||||
EventStatus detail{};
|
||||
if (object["header"].is_string()) {
|
||||
detail.header = object["header"].get<std::string>();
|
||||
} else {
|
||||
detail.header = std::nullopt;
|
||||
}
|
||||
|
||||
if (object["footer"].is_string()) {
|
||||
detail.footer = object["footer"].get<std::string>();
|
||||
} else {
|
||||
detail.footer = std::nullopt;
|
||||
}
|
||||
|
||||
if (object["events"].is_array()) {
|
||||
for (const auto& event : object["events"]) {
|
||||
if (!event.is_string())
|
||||
continue;
|
||||
detail.events.push_back(event.get<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
games.insert_or_assign(object["name"], std::move(detail));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return StatusResult::Success;
|
||||
} catch (const nlohmann::json::parse_error& e) {
|
||||
return StatusResult::ParseError;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Service::BCAT
|
@ -0,0 +1,58 @@
|
||||
// Copyright 2019 yuzu emulator team
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include "core/hle/service/bcat/backend/backend.h"
|
||||
|
||||
namespace Service::BCAT {
|
||||
|
||||
struct EventStatus {
|
||||
std::optional<std::string> header;
|
||||
std::optional<std::string> footer;
|
||||
std::vector<std::string> events;
|
||||
};
|
||||
|
||||
/// Boxcat is yuzu's custom backend implementation of Nintendo's BCAT service. It is free to use and
|
||||
/// doesn't require a switch or nintendo account. The content is controlled by the yuzu team.
|
||||
class Boxcat final : public Backend {
|
||||
friend void SynchronizeInternal(DirectoryGetter dir_getter, TitleIDVersion title,
|
||||
ProgressServiceBackend& progress,
|
||||
std::optional<std::string> dir_name);
|
||||
|
||||
public:
|
||||
explicit Boxcat(DirectoryGetter getter);
|
||||
~Boxcat() override;
|
||||
|
||||
bool Synchronize(TitleIDVersion title, ProgressServiceBackend& progress) override;
|
||||
bool SynchronizeDirectory(TitleIDVersion title, std::string name,
|
||||
ProgressServiceBackend& progress) override;
|
||||
|
||||
bool Clear(u64 title_id) override;
|
||||
|
||||
void SetPassphrase(u64 title_id, const Passphrase& passphrase) override;
|
||||
|
||||
std::optional<std::vector<u8>> GetLaunchParameter(TitleIDVersion title) override;
|
||||
|
||||
enum class StatusResult {
|
||||
Success,
|
||||
Offline,
|
||||
ParseError,
|
||||
BadClientVersion,
|
||||
};
|
||||
|
||||
static StatusResult GetStatus(std::optional<std::string>& global,
|
||||
std::map<std::string, EventStatus>& games);
|
||||
|
||||
private:
|
||||
std::atomic_bool is_syncing{false};
|
||||
|
||||
class Client;
|
||||
std::unique_ptr<Client> client;
|
||||
};
|
||||
|
||||
} // namespace Service::BCAT
|
@ -0,0 +1,136 @@
|
||||
// Copyright 2019 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#include <QGraphicsItem>
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
#include "core/hle/service/bcat/backend/boxcat.h"
|
||||
#include "core/settings.h"
|
||||
#include "ui_configure_service.h"
|
||||
#include "yuzu/configuration/configure_service.h"
|
||||
|
||||
namespace {
|
||||
QString FormatEventStatusString(const Service::BCAT::EventStatus& status) {
|
||||
QString out;
|
||||
|
||||
if (status.header.has_value()) {
|
||||
out += QStringLiteral("<i>%1</i><br>").arg(QString::fromStdString(*status.header));
|
||||
}
|
||||
|
||||
if (status.events.size() == 1) {
|
||||
out += QStringLiteral("%1<br>").arg(QString::fromStdString(status.events.front()));
|
||||
} else {
|
||||
for (const auto& event : status.events) {
|
||||
out += QStringLiteral("- %1<br>").arg(QString::fromStdString(event));
|
||||
}
|
||||
}
|
||||
|
||||
if (status.footer.has_value()) {
|
||||
out += QStringLiteral("<i>%1</i><br>").arg(QString::fromStdString(*status.footer));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
} // Anonymous namespace
|
||||
|
||||
ConfigureService::ConfigureService(QWidget* parent)
|
||||
: QWidget(parent), ui(std::make_unique<Ui::ConfigureService>()) {
|
||||
ui->setupUi(this);
|
||||
|
||||
ui->bcat_source->addItem(QStringLiteral("None"));
|
||||
ui->bcat_empty_label->setHidden(true);
|
||||
ui->bcat_empty_header->setHidden(true);
|
||||
|
||||
#ifdef YUZU_ENABLE_BOXCAT
|
||||
ui->bcat_source->addItem(QStringLiteral("Boxcat"), QStringLiteral("boxcat"));
|
||||
#endif
|
||||
|
||||
connect(ui->bcat_source, QOverload<int>::of(&QComboBox::currentIndexChanged), this,
|
||||
&ConfigureService::OnBCATImplChanged);
|
||||
|
||||
this->SetConfiguration();
|
||||
}
|
||||
|
||||
ConfigureService::~ConfigureService() = default;
|
||||
|
||||
void ConfigureService::ApplyConfiguration() {
|
||||
Settings::values.bcat_backend = ui->bcat_source->currentText().toLower().toStdString();
|
||||
}
|
||||
|
||||
void ConfigureService::RetranslateUi() {
|
||||
ui->retranslateUi(this);
|
||||
}
|
||||
|
||||
void ConfigureService::SetConfiguration() {
|
||||
const int index =
|
||||
ui->bcat_source->findData(QString::fromStdString(Settings::values.bcat_backend));
|
||||
ui->bcat_source->setCurrentIndex(index == -1 ? 0 : index);
|
||||
}
|
||||
|
||||
std::pair<QString, QString> ConfigureService::BCATDownloadEvents() {
|
||||
std::optional<std::string> global;
|
||||
std::map<std::string, Service::BCAT::EventStatus> map;
|
||||
const auto res = Service::BCAT::Boxcat::GetStatus(global, map);
|
||||
|
||||
switch (res) {
|
||||
case Service::BCAT::Boxcat::StatusResult::Offline:
|
||||
return {QString{},
|
||||
tr("The boxcat service is offline or you are not connected to the internet.")};
|
||||
case Service::BCAT::Boxcat::StatusResult::ParseError:
|
||||
return {QString{},
|
||||
tr("There was an error while processing the boxcat event data. Contact the yuzu "
|
||||
"developers.")};
|
||||
case Service::BCAT::Boxcat::StatusResult::BadClientVersion:
|
||||
return {QString{},
|
||||
tr("The version of yuzu you are using is either too new or too old for the server. "
|
||||
"Try updating to the latest official release of yuzu.")};
|
||||
}
|
||||
|
||||
if (map.empty()) {
|
||||
return {QStringLiteral("Current Boxcat Events"),
|
||||
tr("There are currently no events on boxcat.")};
|
||||
}
|
||||
|
||||
QString out;
|
||||
|
||||
if (global.has_value()) {
|
||||
out += QStringLiteral("%1<br>").arg(QString::fromStdString(*global));
|
||||
}
|
||||
|
||||
for (const auto& [key, value] : map) {
|
||||
out += QStringLiteral("%1<b>%2</b><br>%3")
|
||||
.arg(out.isEmpty() ? QString{} : QStringLiteral("<br>"))
|
||||
.arg(QString::fromStdString(key))
|
||||
.arg(FormatEventStatusString(value));
|
||||
}
|
||||
return {QStringLiteral("Current Boxcat Events"), std::move(out)};
|
||||
}
|
||||
|
||||
void ConfigureService::OnBCATImplChanged() {
|
||||
#ifdef YUZU_ENABLE_BOXCAT
|
||||
const auto boxcat = ui->bcat_source->currentText() == QStringLiteral("Boxcat");
|
||||
ui->bcat_empty_header->setHidden(!boxcat);
|
||||
ui->bcat_empty_label->setHidden(!boxcat);
|
||||
ui->bcat_empty_header->setText(QString{});
|
||||
ui->bcat_empty_label->setText(tr("Yuzu is retrieving the latest boxcat status..."));
|
||||
|
||||
if (!boxcat)
|
||||
return;
|
||||
|
||||
const auto future = QtConcurrent::run([this] { return BCATDownloadEvents(); });
|
||||
|
||||
watcher.setFuture(future);
|
||||
connect(&watcher, &QFutureWatcher<std::pair<QString, QString>>::finished, this,
|
||||
[this] { OnUpdateBCATEmptyLabel(watcher.result()); });
|
||||
#endif
|
||||
}
|
||||
|
||||
void ConfigureService::OnUpdateBCATEmptyLabel(std::pair<QString, QString> string) {
|
||||
#ifdef YUZU_ENABLE_BOXCAT
|
||||
const auto boxcat = ui->bcat_source->currentText() == QStringLiteral("Boxcat");
|
||||
if (boxcat) {
|
||||
ui->bcat_empty_header->setText(string.first);
|
||||
ui->bcat_empty_label->setText(string.second);
|
||||
}
|
||||
#endif
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
// Copyright 2019 yuzu Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <QFutureWatcher>
|
||||
#include <QWidget>
|
||||
|
||||
namespace Ui {
|
||||
class ConfigureService;
|
||||
}
|
||||
|
||||
class ConfigureService : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ConfigureService(QWidget* parent = nullptr);
|
||||
~ConfigureService() override;
|
||||
|
||||
void ApplyConfiguration();
|
||||
void RetranslateUi();
|
||||
|
||||
private:
|
||||
void SetConfiguration();
|
||||
|
||||
std::pair<QString, QString> BCATDownloadEvents();
|
||||
void OnBCATImplChanged();
|
||||
void OnUpdateBCATEmptyLabel(std::pair<QString, QString> string);
|
||||
|
||||
std::unique_ptr<Ui::ConfigureService> ui;
|
||||
QFutureWatcher<std::pair<QString, QString>> watcher{this};
|
||||
};
|
@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ConfigureService</class>
|
||||
<widget class="QWidget" name="ConfigureService">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>433</width>
|
||||
<height>561</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>BCAT</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="1" colspan="2">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>260</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>BCAT is Nintendo's way of sending data to games to engage its community and unlock additional content.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>16777215</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>BCAT Backend</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1" colspan="2">
|
||||
<widget class="QLabel" name="bcat_empty_label">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>260</width>
|
||||
<height>16777215</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1" colspan="2">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string><html><head/><body><p><a href="https://yuzu-emu.org/help/feature/boxcat"><span style=" text-decoration: underline; color:#0000ff;">Learn more about BCAT, Boxcat, and Current Events</span></a></p></body></html></string>
|
||||
</property>
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1" colspan="2">
|
||||
<widget class="QComboBox" name="bcat_source"/>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="bcat_empty_header">
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
Loading…
Reference in New Issue