mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-17 12:39:27 -04:00
Progress modal & persist validation state
This commit is contained in:
@@ -21,6 +21,12 @@ enum class GameLanguage : u8 {
|
||||
Italian = OS_LANGUAGE_ITALIAN,
|
||||
};
|
||||
|
||||
enum class DiscVerificationState : u8 {
|
||||
Unknown = 0,
|
||||
Success,
|
||||
HashMismatch,
|
||||
};
|
||||
|
||||
namespace config {
|
||||
template <>
|
||||
struct ConfigEnumRange<BloomMode> {
|
||||
@@ -33,6 +39,12 @@ struct ConfigEnumRange<GameLanguage> {
|
||||
static constexpr auto min = GameLanguage::English;
|
||||
static constexpr auto max = GameLanguage::Italian;
|
||||
};
|
||||
|
||||
template <>
|
||||
struct ConfigEnumRange<DiscVerificationState> {
|
||||
static constexpr auto min = DiscVerificationState::Unknown;
|
||||
static constexpr auto max = DiscVerificationState::HashMismatch;
|
||||
};
|
||||
}
|
||||
|
||||
// Persistent user settings
|
||||
@@ -150,6 +162,7 @@ struct UserSettings {
|
||||
|
||||
struct {
|
||||
ConfigVar<std::string> isoPath;
|
||||
ConfigVar<DiscVerificationState> isoVerification;
|
||||
ConfigVar<std::string> graphicsBackend;
|
||||
ConfigVar<bool> skipPreLaunchUI;
|
||||
ConfigVar<bool> showPipelineCompilation;
|
||||
|
||||
@@ -224,6 +224,10 @@ body.mirrored version-info {
|
||||
color: #FFD6A7;
|
||||
}
|
||||
|
||||
#disc-status[status=unknown] {
|
||||
color: rgba(224, 219, 200, 65%);
|
||||
}
|
||||
|
||||
#disc-status icon {
|
||||
display: none;
|
||||
width: 24dp;
|
||||
@@ -253,6 +257,10 @@ body.mirrored version-info {
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
#disc-status[status=unknown] icon {
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
#disc-version {
|
||||
font-size: 20dp;
|
||||
}
|
||||
|
||||
@@ -279,6 +279,10 @@ icon.error {
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
icon.verifying {
|
||||
decorator: text("" center center);
|
||||
}
|
||||
|
||||
icon.celebration {
|
||||
decorator: text("" center center);
|
||||
}
|
||||
@@ -399,6 +403,10 @@ button.achievement-clear {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
window.modal.danger {
|
||||
border: 2dp #852221;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -424,6 +432,11 @@ button.achievement-clear {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
window.modal.danger .modal-title,
|
||||
window.modal.danger .modal-header icon {
|
||||
color: #B3261E;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -434,6 +447,30 @@ button.achievement-clear {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.verification-progress {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10dp;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.verification-file {
|
||||
display: block;
|
||||
font-size: 17dp;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
progressbar.verification-progress-bar {
|
||||
height: 8dp;
|
||||
margin: 2dp 0 0 0;
|
||||
}
|
||||
|
||||
.verification-detail {
|
||||
display: block;
|
||||
font-size: 14dp;
|
||||
color: rgba(224, 219, 200, 65%);
|
||||
}
|
||||
|
||||
.modal-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@@ -154,6 +154,7 @@ namespace dusk::config {
|
||||
template class ConfigImpl<f64>;
|
||||
template class ConfigImpl<std::string>;
|
||||
template class ConfigImpl<dusk::BloomMode>;
|
||||
template class ConfigImpl<dusk::DiscVerificationState>;
|
||||
template class ConfigImpl<dusk::GameLanguage>;
|
||||
}
|
||||
|
||||
|
||||
+43
-19
@@ -1,8 +1,13 @@
|
||||
#include "iso_validate.hpp"
|
||||
|
||||
#include <nod.h>
|
||||
#include <SDL3/SDL_iostream.h>
|
||||
#include <nod.h>
|
||||
#include <xxhash.h>
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -62,8 +67,8 @@ struct KnownDisc {
|
||||
|
||||
constexpr KnownDisc(std::string_view id, Platform platform, Region region)
|
||||
: id(id), platform(platform), region(region) {}
|
||||
constexpr KnownDisc(std::string_view id, Platform platform, Region region,
|
||||
const std::string_view hash)
|
||||
constexpr KnownDisc(
|
||||
std::string_view id, Platform platform, Region region, const std::string_view hash)
|
||||
: id(id), platform(platform), region(region), supported(true), hash(parse_xxh128(hash)) {}
|
||||
};
|
||||
|
||||
@@ -144,22 +149,30 @@ void StreamClose(void* user_data) {
|
||||
}
|
||||
|
||||
ValidationError verify_disc(NodHandle* disc, VerificationStatus& status) {
|
||||
const auto hashState = XXH3_createState();
|
||||
XXH3_128bits_reset(hashState);
|
||||
std::unique_ptr<XXH3_state_t, decltype(&XXH3_freeState)> hashState(
|
||||
XXH3_createState(), XXH3_freeState);
|
||||
if (!hashState) {
|
||||
return ValidationError::Unknown;
|
||||
}
|
||||
XXH3_128bits_reset(hashState.get());
|
||||
|
||||
while (true) {
|
||||
if (status.shouldCancel.load(std::memory_order_relaxed)) {
|
||||
return ValidationError::Canceled;
|
||||
}
|
||||
|
||||
size_t bytesAvail;
|
||||
const auto buf = nod_buf_read(disc, &bytesAvail);
|
||||
if (!bytesAvail)
|
||||
break;
|
||||
|
||||
XXH3_128bits_update(hashState, buf, bytesAvail);
|
||||
XXH3_128bits_update(hashState.get(), buf, bytesAvail);
|
||||
|
||||
status.bytesRead += bytesAvail;
|
||||
status.bytesRead.fetch_add(bytesAvail, std::memory_order_relaxed);
|
||||
nod_buf_consume(disc, bytesAvail);
|
||||
}
|
||||
|
||||
const auto hash = XXH3_128bits_digest(hashState);
|
||||
const auto hash = XXH3_128bits_digest(hashState.get());
|
||||
if (!XXH128_isEqual(hash, status.knownDisc->hash)) {
|
||||
return ValidationError::HashMismatch;
|
||||
}
|
||||
@@ -175,7 +188,7 @@ ValidationError validate(const char* path, VerificationStatus& status) {
|
||||
return ValidationError::IOError;
|
||||
}
|
||||
|
||||
const NodDiscStream nod_stream {
|
||||
const NodDiscStream nod_stream{
|
||||
.user_data = sdlStream,
|
||||
.read_at = StreamReadAt,
|
||||
.stream_len = StreamLength,
|
||||
@@ -187,7 +200,7 @@ ValidationError validate(const char* path, VerificationStatus& status) {
|
||||
return convertNodError(result);
|
||||
}
|
||||
|
||||
status.bytesTotal = nod_disc_size(disc.handle);
|
||||
status.bytesTotal.store(nod_disc_size(disc.handle), std::memory_order_relaxed);
|
||||
|
||||
NodDiscHeader header{};
|
||||
result = nod_disc_header(disc.handle, &header);
|
||||
@@ -215,12 +228,12 @@ ValidationError validate(const char* path) {
|
||||
return validate(path, status);
|
||||
}
|
||||
|
||||
bool isPal(const char* path) {
|
||||
ValidationError inspect(const char* path, DiscInfo& info) {
|
||||
NodHandleWrapper disc;
|
||||
|
||||
const auto sdlStream = SDL_IOFromFile(path, "rb");
|
||||
if (sdlStream == nullptr) {
|
||||
return false;
|
||||
return ValidationError::IOError;
|
||||
}
|
||||
|
||||
const NodDiscStream nod_stream{
|
||||
@@ -230,19 +243,30 @@ bool isPal(const char* path) {
|
||||
.close = StreamClose,
|
||||
};
|
||||
|
||||
if (nod_disc_open_stream(&nod_stream, nullptr, &disc.handle) != NOD_RESULT_OK ||
|
||||
disc.handle == nullptr)
|
||||
{
|
||||
return false;
|
||||
auto result = nod_disc_open_stream(&nod_stream, nullptr, &disc.handle);
|
||||
if (disc.handle == nullptr || result != NOD_RESULT_OK) {
|
||||
return convertNodError(result);
|
||||
}
|
||||
|
||||
NodDiscHeader header{};
|
||||
if (nod_disc_header(disc.handle, &header) != NOD_RESULT_OK) {
|
||||
return false;
|
||||
result = nod_disc_header(disc.handle, &header);
|
||||
if (result != NOD_RESULT_OK) {
|
||||
return convertNodError(result);
|
||||
}
|
||||
|
||||
const auto knownDisc = find_disc(std::string_view(header.game_id, 6));
|
||||
if (!knownDisc) {
|
||||
return ValidationError::WrongGame;
|
||||
}
|
||||
info.isPal = knownDisc->region == Region::Europe;
|
||||
if (!knownDisc->supported) {
|
||||
return ValidationError::WrongVersion;
|
||||
}
|
||||
return ValidationError::Success;
|
||||
}
|
||||
|
||||
return knownDisc && knownDisc->region == Region::Europe;
|
||||
bool isPal(const char* path) {
|
||||
DiscInfo info{};
|
||||
return inspect(path, info) == ValidationError::Success && info.isPal;
|
||||
}
|
||||
} // namespace dusk::iso
|
||||
|
||||
+28
-21
@@ -1,31 +1,38 @@
|
||||
#ifndef DUSK_ISO_VALIDATE_HPP
|
||||
#define DUSK_ISO_VALIDATE_HPP
|
||||
|
||||
#include <xxh3.h>
|
||||
#include <atomic>
|
||||
|
||||
namespace dusk::iso {
|
||||
struct KnownDisc;
|
||||
struct KnownDisc;
|
||||
|
||||
enum class ValidationError : u8 {
|
||||
Unknown = 0,
|
||||
IOError,
|
||||
InvalidImage,
|
||||
WrongGame,
|
||||
WrongVersion,
|
||||
HashMismatch,
|
||||
Success
|
||||
};
|
||||
enum class ValidationError : u8 {
|
||||
Unknown = 0,
|
||||
IOError,
|
||||
InvalidImage,
|
||||
WrongGame,
|
||||
WrongVersion,
|
||||
Canceled,
|
||||
HashMismatch,
|
||||
Success
|
||||
};
|
||||
|
||||
struct VerificationStatus {
|
||||
size_t bytesRead = 0;
|
||||
size_t bytesTotal = 0;
|
||||
const KnownDisc* knownDisc = nullptr;
|
||||
bool shouldCancel = false;
|
||||
};
|
||||
struct VerificationStatus {
|
||||
std::atomic_size_t bytesRead = 0;
|
||||
std::atomic_size_t bytesTotal = 0;
|
||||
const KnownDisc* knownDisc = nullptr;
|
||||
std::atomic_bool shouldCancel = false;
|
||||
};
|
||||
|
||||
ValidationError validate(const char* path, VerificationStatus& status);
|
||||
ValidationError validate(const char* path);
|
||||
bool isPal(const char* path);
|
||||
}
|
||||
struct DiscInfo {
|
||||
bool isPal = false;
|
||||
};
|
||||
|
||||
ValidationError inspect(const char* path, DiscInfo& info);
|
||||
ValidationError validate(const char* path, VerificationStatus& status);
|
||||
ValidationError validate(const char* path);
|
||||
bool isPal(const char* path);
|
||||
|
||||
} // namespace dusk::iso
|
||||
|
||||
#endif // DUSK_ISO_VALIDATE_HPP
|
||||
|
||||
@@ -109,6 +109,7 @@ UserSettings g_userSettings = {
|
||||
|
||||
.backend = {
|
||||
.isoPath {"backend.isoPath", ""},
|
||||
.isoVerification {"backend.isoVerification", DiscVerificationState::Unknown},
|
||||
.graphicsBackend {"backend.graphicsBackend", "auto"},
|
||||
.skipPreLaunchUI {"backend.skipPreLaunchUI", false},
|
||||
.showPipelineCompilation {"backend.showPipelineCompilation", false},
|
||||
@@ -206,6 +207,7 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.debugFlyCam);
|
||||
|
||||
Register(g_userSettings.backend.isoPath);
|
||||
Register(g_userSettings.backend.isoVerification);
|
||||
Register(g_userSettings.backend.graphicsBackend);
|
||||
Register(g_userSettings.backend.skipPreLaunchUI);
|
||||
Register(g_userSettings.backend.showPipelineCompilation);
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
Modal::Modal(Props props)
|
||||
: WindowSmall("modal", "modal-dialog"), mProps(std::move(props)) {
|
||||
Modal::Modal(Props props) : WindowSmall("modal", "modal-dialog"), mProps(std::move(props)) {
|
||||
if (!mProps.variant.empty()) {
|
||||
mRoot->SetClass(mProps.variant, true);
|
||||
}
|
||||
|
||||
auto* header = append(mDialog, "div");
|
||||
header->SetClass("modal-header", true);
|
||||
|
||||
@@ -14,7 +17,7 @@ Modal::Modal(Props props)
|
||||
if (mProps.isWarning) {
|
||||
auto* icon = append(header, "icon");
|
||||
icon->SetClass("warning", true);
|
||||
} else if ( mProps.isError ) {
|
||||
} else if (mProps.isError) {
|
||||
auto* icon = append(header, "icon");
|
||||
icon->SetClass("error", true);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public:
|
||||
Rml::String bodyRml;
|
||||
std::vector<ModalAction> actions;
|
||||
std::function<void(Modal&)> onDismiss;
|
||||
Rml::String variant;
|
||||
bool isWarning = false;
|
||||
bool isError = false;
|
||||
};
|
||||
|
||||
@@ -189,7 +189,8 @@ void Overlay::update() {
|
||||
|
||||
const bool showControllerWarning = PADGetIndexForPort(PAD_CHAN0) < 0 &&
|
||||
PADGetKeyButtonBindings(PAD_CHAN0, nullptr) == nullptr &&
|
||||
dynamic_cast<Window*>(top_document()) == nullptr;
|
||||
dynamic_cast<Window*>(top_document()) == nullptr &&
|
||||
dynamic_cast<WindowSmall*>(top_document()) == nullptr;
|
||||
if (showControllerWarning && mControllerWarning == nullptr) {
|
||||
mControllerWarning = create_controller_warning(mDocument);
|
||||
} else if (showControllerWarning && mControllerWarning != nullptr) {
|
||||
|
||||
+373
-46
@@ -11,11 +11,23 @@
|
||||
#include "version.h"
|
||||
|
||||
#include <SDL3/SDL_dialog.h>
|
||||
#include <aurora/lib/logging.hpp>
|
||||
#include <aurora/lib/window.hpp>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
#include "m_Do/m_Do_MemCard.h"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
aurora::Module PrelaunchLog{"dusk::ui::prelaunch"};
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
@@ -54,7 +66,130 @@ constexpr std::array<SDL_DialogFileFilter, 2> kDiscFileFilters{{
|
||||
{"All Files", "*"},
|
||||
}};
|
||||
|
||||
static std::string get_error_msg(iso::ValidationError error) {
|
||||
struct DiscVerificationResult {
|
||||
std::string path;
|
||||
iso::ValidationError validation = iso::ValidationError::Unknown;
|
||||
};
|
||||
|
||||
struct DiscVerificationTask {
|
||||
explicit DiscVerificationTask(std::string discPath) : path(std::move(discPath)) {
|
||||
worker = std::thread([this] {
|
||||
try {
|
||||
validation = iso::validate(path.c_str(), status);
|
||||
} catch (const std::exception& e) {
|
||||
PrelaunchLog.error(
|
||||
"Disc verification failed with exception for '{}': {}", path, e.what());
|
||||
validation = iso::ValidationError::Unknown;
|
||||
} catch (...) {
|
||||
PrelaunchLog.error(
|
||||
"Disc verification failed with unknown exception for '{}'", path);
|
||||
validation = iso::ValidationError::Unknown;
|
||||
}
|
||||
done.store(true, std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
~DiscVerificationTask() {
|
||||
status.shouldCancel.store(true, std::memory_order_relaxed);
|
||||
join();
|
||||
}
|
||||
|
||||
void join() {
|
||||
if (worker.joinable()) {
|
||||
worker.join();
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool finished() const { return done.load(std::memory_order_acquire); }
|
||||
|
||||
std::string path;
|
||||
iso::VerificationStatus status;
|
||||
iso::ValidationError validation = iso::ValidationError::Unknown;
|
||||
std::atomic_bool done = false;
|
||||
std::thread worker;
|
||||
};
|
||||
|
||||
std::unique_ptr<DiscVerificationTask> sDiscVerificationTask;
|
||||
bool sDiscVerificationModalPushed = false;
|
||||
|
||||
bool validation_allows_launch(iso::ValidationError validation) noexcept {
|
||||
return validation == iso::ValidationError::Success ||
|
||||
validation == iso::ValidationError::HashMismatch ||
|
||||
validation == iso::ValidationError::Canceled;
|
||||
}
|
||||
|
||||
bool verification_state_allows_launch(iso::ValidationError validation) noexcept {
|
||||
return validation == iso::ValidationError::Unknown ||
|
||||
validation == iso::ValidationError::Success ||
|
||||
validation == iso::ValidationError::HashMismatch ||
|
||||
validation == iso::ValidationError::Canceled;
|
||||
}
|
||||
|
||||
iso::ValidationError verification_from_config(DiscVerificationState value) noexcept {
|
||||
switch (value) {
|
||||
case DiscVerificationState::Success:
|
||||
return iso::ValidationError::Success;
|
||||
case DiscVerificationState::HashMismatch:
|
||||
return iso::ValidationError::HashMismatch;
|
||||
default:
|
||||
return iso::ValidationError::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
DiscVerificationState verification_to_config(iso::ValidationError validation) {
|
||||
switch (validation) {
|
||||
case iso::ValidationError::Success:
|
||||
return DiscVerificationState::Success;
|
||||
case iso::ValidationError::HashMismatch:
|
||||
return DiscVerificationState::HashMismatch;
|
||||
default:
|
||||
return DiscVerificationState::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
std::string format_bytes(std::size_t bytes) {
|
||||
constexpr double KiB = 1024.0;
|
||||
constexpr double MiB = KiB * 1024.0;
|
||||
constexpr double GiB = MiB * 1024.0;
|
||||
if (bytes >= static_cast<std::size_t>(GiB)) {
|
||||
return fmt::format("{:.2f} GiB", static_cast<double>(bytes) / GiB);
|
||||
}
|
||||
if (bytes >= static_cast<std::size_t>(MiB)) {
|
||||
return fmt::format("{:.0f} MiB", static_cast<double>(bytes) / MiB);
|
||||
}
|
||||
if (bytes >= static_cast<std::size_t>(KiB)) {
|
||||
return fmt::format("{:.0f} KiB", static_cast<double>(bytes) / KiB);
|
||||
}
|
||||
return fmt::format("{} B", bytes);
|
||||
}
|
||||
|
||||
void begin_disc_verification(std::string path) noexcept {
|
||||
if (path.empty()) {
|
||||
return;
|
||||
}
|
||||
if (sDiscVerificationTask != nullptr) {
|
||||
sDiscVerificationTask->status.shouldCancel.store(true, std::memory_order_relaxed);
|
||||
sDiscVerificationTask.reset();
|
||||
}
|
||||
sDiscVerificationTask = std::make_unique<DiscVerificationTask>(std::move(path));
|
||||
sDiscVerificationModalPushed = false;
|
||||
}
|
||||
|
||||
std::optional<DiscVerificationResult> take_finished_disc_verification() {
|
||||
if (sDiscVerificationTask == nullptr || !sDiscVerificationTask->finished()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
DiscVerificationResult result{
|
||||
.path = sDiscVerificationTask->path,
|
||||
.validation = sDiscVerificationTask->validation,
|
||||
};
|
||||
sDiscVerificationTask->join();
|
||||
sDiscVerificationTask.reset();
|
||||
sDiscVerificationModalPushed = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string get_error_msg(iso::ValidationError error) {
|
||||
switch (error) {
|
||||
default:
|
||||
return "The selected disc image could not be validated.";
|
||||
@@ -66,40 +201,215 @@ static std::string get_error_msg(iso::ValidationError error) {
|
||||
return "The selected game is not supported by Dusk.";
|
||||
case iso::ValidationError::WrongVersion:
|
||||
return "Dusk currently supports GameCube USA and PAL disc images only.";
|
||||
case iso::ValidationError::Canceled:
|
||||
return "Disc verification was canceled. Dusk cannot guarantee the selected disc image "
|
||||
"is compatible.";
|
||||
case iso::ValidationError::HashMismatch:
|
||||
return "The selected disc image did not pass hash verification, it may be corrupt or modified.";
|
||||
return "The selected disc image did not pass hash verification. It may be corrupt or "
|
||||
"modified.";
|
||||
case iso::ValidationError::Success:
|
||||
return "The selected disc image is valid.";
|
||||
}
|
||||
}
|
||||
|
||||
void persist_disc_choice(const std::string& path, iso::ValidationError validation) {
|
||||
getSettings().backend.isoPath.setValue(path);
|
||||
getSettings().backend.isoVerification.setValue(verification_to_config(validation));
|
||||
config::Save();
|
||||
}
|
||||
|
||||
void apply_valid_disc_result(const std::string& path, iso::ValidationError validation) {
|
||||
auto& state = prelaunch_state();
|
||||
iso::DiscInfo info{};
|
||||
const bool pal =
|
||||
iso::inspect(path.c_str(), info) == iso::ValidationError::Success && info.isPal;
|
||||
|
||||
state.selectedDiscPath = path;
|
||||
state.selectedDiscIsValid = true;
|
||||
state.selectedDiscIsPal = pal;
|
||||
state.initialDiscValidationRes = validation;
|
||||
state.initialDiscIsPal = pal;
|
||||
persist_disc_choice(path, validation);
|
||||
}
|
||||
|
||||
void apply_disc_verification_result(const DiscVerificationResult& result) {
|
||||
auto& state = prelaunch_state();
|
||||
|
||||
if (result.validation == iso::ValidationError::HashMismatch ||
|
||||
result.validation == iso::ValidationError::Canceled)
|
||||
{
|
||||
state.pendingDiscPath = result.path;
|
||||
state.pendingDiscValidation = result.validation;
|
||||
state.errorString = escape(get_error_msg(result.validation));
|
||||
return;
|
||||
}
|
||||
|
||||
if (validation_allows_launch(result.validation)) {
|
||||
apply_valid_disc_result(result.path, result.validation);
|
||||
|
||||
if (result.validation == iso::ValidationError::Success) {
|
||||
state.errorString.clear();
|
||||
state.pendingDiscPath.clear();
|
||||
state.pendingDiscValidation = iso::ValidationError::Unknown;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
state.pendingDiscPath.clear();
|
||||
state.pendingDiscValidation = iso::ValidationError::Unknown;
|
||||
state.errorString = escape(get_error_msg(result.validation));
|
||||
}
|
||||
|
||||
class DiscVerificationModal : public WindowSmall {
|
||||
public:
|
||||
DiscVerificationModal() : WindowSmall("modal", "modal-dialog") {
|
||||
auto* header = append(mDialog, "div");
|
||||
header->SetClass("modal-header", true);
|
||||
|
||||
auto* title = append(header, "div");
|
||||
title->SetClass("modal-title", true);
|
||||
title->SetInnerRML("Verifying disc image");
|
||||
|
||||
auto* icon = append(header, "icon");
|
||||
icon->SetClass("verifying", true);
|
||||
|
||||
auto* body = append(mDialog, "div");
|
||||
body->SetClass("modal-body", true);
|
||||
|
||||
auto* content = append(body, "div");
|
||||
content->SetClass("verification-progress", true);
|
||||
|
||||
mFileName = append(content, "div");
|
||||
mFileName->SetClass("verification-file", true);
|
||||
|
||||
mProgress = append(content, "progressbar");
|
||||
mProgress->SetClass("progress-ongoing", true);
|
||||
mProgress->SetClass("verification-progress-bar", true);
|
||||
mProgress->SetAttribute("value", 0.f);
|
||||
|
||||
mDetail = append(content, "div");
|
||||
mDetail->SetClass("verification-detail", true);
|
||||
|
||||
auto* actions = append(mDialog, "div");
|
||||
actions->SetClass("modal-actions", true);
|
||||
mCancelButton = std::make_unique<Button>(actions, "Cancel");
|
||||
mCancelButton->root()->SetClass("modal-btn", true);
|
||||
mCancelButton->on_pressed([this] { request_cancel(); });
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
void update() override {
|
||||
if (mFinished) {
|
||||
return;
|
||||
}
|
||||
if (auto result = take_finished_disc_verification()) {
|
||||
mFinished = true;
|
||||
apply_disc_verification_result(*result);
|
||||
pop();
|
||||
return;
|
||||
}
|
||||
if (sDiscVerificationTask == nullptr) {
|
||||
mFinished = true;
|
||||
pop();
|
||||
return;
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
bool focus() override { return mCancelButton != nullptr && mCancelButton->focus(); }
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override {
|
||||
if (cmd == NavCommand::Cancel || cmd == NavCommand::Menu) {
|
||||
request_cancel();
|
||||
event.StopPropagation();
|
||||
return true;
|
||||
}
|
||||
if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
void request_cancel() {
|
||||
if (sDiscVerificationTask == nullptr || mCancelRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
mCancelRequested = true;
|
||||
sDiscVerificationTask->status.shouldCancel.store(true, std::memory_order_relaxed);
|
||||
if (mCancelButton != nullptr) {
|
||||
mCancelButton->set_text("Cancelling...");
|
||||
mCancelButton->set_disabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
if (sDiscVerificationTask == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mCancelRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mFileName != nullptr) {
|
||||
std::string fileName =
|
||||
std::filesystem::path(sDiscVerificationTask->path).filename().string();
|
||||
if (fileName.empty()) {
|
||||
fileName = sDiscVerificationTask->path;
|
||||
}
|
||||
mFileName->SetInnerRML(escape(fileName));
|
||||
}
|
||||
|
||||
const std::size_t bytesRead =
|
||||
sDiscVerificationTask->status.bytesRead.load(std::memory_order_relaxed);
|
||||
const std::size_t bytesTotal =
|
||||
sDiscVerificationTask->status.bytesTotal.load(std::memory_order_relaxed);
|
||||
|
||||
if (bytesTotal == 0) {
|
||||
if (mProgress != nullptr) {
|
||||
mProgress->SetAttribute("value", 0.f);
|
||||
}
|
||||
if (mDetail != nullptr) {
|
||||
mDetail->SetInnerRML("Opening disc image...");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const float fraction =
|
||||
std::clamp(static_cast<float>(bytesRead) / static_cast<float>(bytesTotal), 0.0f, 1.0f);
|
||||
if (mProgress != nullptr) {
|
||||
mProgress->SetAttribute("value", fraction);
|
||||
}
|
||||
if (mDetail != nullptr) {
|
||||
mDetail->SetInnerRML(escape(fmt::format("{} / {} ({:.0f}%)", format_bytes(bytesRead),
|
||||
format_bytes(bytesTotal), fraction * 100.0f)));
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Element* mFileName = nullptr;
|
||||
Rml::Element* mProgress = nullptr;
|
||||
Rml::Element* mDetail = nullptr;
|
||||
std::unique_ptr<Button> mCancelButton;
|
||||
bool mCancelRequested = false;
|
||||
bool mFinished = false;
|
||||
};
|
||||
|
||||
void file_dialog_callback(void*, const char* path, const char* error) {
|
||||
if (path == nullptr || error != nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto& state = prelaunch_state();
|
||||
const auto validation = iso::validate(path);
|
||||
if (validation == iso::ValidationError::Success) {
|
||||
state.selectedDiscPath = path;
|
||||
state.errorString.clear();
|
||||
state.pendingDiscPath.clear();
|
||||
state.userAcceptedDiscPath.clear();
|
||||
getSettings().backend.isoPath.setValue(state.selectedDiscPath);
|
||||
config::Save();
|
||||
refresh_state();
|
||||
return;
|
||||
}
|
||||
if (validation == iso::ValidationError::HashMismatch) {
|
||||
state.pendingDiscPath = path;
|
||||
} else {
|
||||
state.pendingDiscPath.clear();
|
||||
}
|
||||
state.errorString = escape(get_error_msg(validation));
|
||||
begin_disc_verification(path);
|
||||
}
|
||||
|
||||
PrelaunchState sPrelaunchState;
|
||||
|
||||
} // namespace
|
||||
|
||||
PrelaunchState& prelaunch_state() noexcept {
|
||||
return sPrelaunchState;
|
||||
}
|
||||
@@ -108,28 +418,47 @@ void refresh_state() noexcept {
|
||||
auto& state = prelaunch_state();
|
||||
if (state.selectedDiscPath.empty()) {
|
||||
state.selectedDiscIsValid = false;
|
||||
return;
|
||||
}
|
||||
if (!state.userAcceptedDiscPath.empty() &&
|
||||
state.selectedDiscPath == state.userAcceptedDiscPath) {
|
||||
state.selectedDiscIsValid = true;
|
||||
state.selectedDiscIsPal = iso::isPal(state.selectedDiscPath.c_str());
|
||||
state.selectedDiscIsPal = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const auto validation = iso::validate(state.selectedDiscPath.c_str());
|
||||
// Allow HashMismatch, so that the user is not prompted to accept the warning on every boot.
|
||||
if (validation >= iso::ValidationError::HashMismatch) {
|
||||
state.selectedDiscIsValid = true;
|
||||
state.selectedDiscIsPal = iso::isPal(state.selectedDiscPath.c_str());
|
||||
state.userAcceptedDiscPath.clear();
|
||||
iso::DiscInfo info{};
|
||||
const auto metadataValidation = iso::inspect(state.selectedDiscPath.c_str(), info);
|
||||
if (metadataValidation != iso::ValidationError::Success) {
|
||||
state.selectedDiscIsValid = false;
|
||||
state.selectedDiscIsPal = false;
|
||||
state.initialDiscValidationRes = metadataValidation;
|
||||
state.initialDiscIsPal = false;
|
||||
return;
|
||||
}
|
||||
|
||||
auto verification = state.initialDiscValidationRes;
|
||||
if (state.selectedDiscPath == getSettings().backend.isoPath.getValue()) {
|
||||
verification = verification_from_config(getSettings().backend.isoVerification.getValue());
|
||||
}
|
||||
|
||||
if (verification_state_allows_launch(verification)) {
|
||||
state.selectedDiscIsValid = true;
|
||||
state.selectedDiscIsPal = info.isPal;
|
||||
state.initialDiscValidationRes = verification;
|
||||
state.initialDiscIsPal = info.isPal;
|
||||
return;
|
||||
}
|
||||
|
||||
state.selectedDiscIsValid = false;
|
||||
state.selectedDiscIsPal = false;
|
||||
state.initialDiscValidationRes = iso::ValidationError::Unknown;
|
||||
state.initialDiscIsPal = false;
|
||||
}
|
||||
|
||||
void try_push_verification_modal(Document& host) {
|
||||
auto& state = prelaunch_state();
|
||||
if (sDiscVerificationTask != nullptr && !sDiscVerificationModalPushed) {
|
||||
sDiscVerificationModalPushed = true;
|
||||
host.push(std::make_unique<DiscVerificationModal>());
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.errorString.empty()) {
|
||||
return;
|
||||
}
|
||||
@@ -138,20 +467,21 @@ void try_push_verification_modal(Document& host) {
|
||||
auto& state = prelaunch_state();
|
||||
state.errorString.clear();
|
||||
state.pendingDiscPath.clear();
|
||||
state.pendingDiscValidation = iso::ValidationError::Unknown;
|
||||
modal.pop();
|
||||
};
|
||||
|
||||
if (!state.pendingDiscPath.empty()) {
|
||||
const Rml::String bodyRml = state.errorString + "<br/><br/>You may proceed at your own risk.";
|
||||
const Rml::String bodyRml =
|
||||
state.errorString + "<br/><br/>You may proceed at your own risk.";
|
||||
auto acceptHashMismatch = [](Modal& modal) {
|
||||
auto& st = prelaunch_state();
|
||||
std::string path = std::move(st.pendingDiscPath);
|
||||
const auto validation = st.pendingDiscValidation;
|
||||
st.pendingDiscPath.clear();
|
||||
st.pendingDiscValidation = iso::ValidationError::Unknown;
|
||||
st.errorString.clear();
|
||||
st.selectedDiscPath = path;
|
||||
st.userAcceptedDiscPath = path;
|
||||
getSettings().backend.isoPath.setValue(path);
|
||||
config::Save();
|
||||
apply_valid_disc_result(path, validation);
|
||||
refresh_state();
|
||||
modal.pop();
|
||||
};
|
||||
@@ -170,6 +500,7 @@ void try_push_verification_modal(Document& host) {
|
||||
},
|
||||
},
|
||||
.onDismiss = dismiss,
|
||||
.variant = "danger",
|
||||
.isWarning = true,
|
||||
}));
|
||||
return;
|
||||
@@ -198,11 +529,8 @@ void ensure_initialized() noexcept {
|
||||
|
||||
state.selectedDiscPath = getSettings().backend.isoPath;
|
||||
state.initialDiscPath = state.selectedDiscPath;
|
||||
const auto& res = iso::validate(state.initialDiscPath.c_str());
|
||||
if (res >= iso::ValidationError::HashMismatch) {
|
||||
state.initialDiscValidationRes = res;
|
||||
state.initialDiscIsPal = iso::isPal(state.initialDiscPath.c_str());
|
||||
}
|
||||
state.initialDiscValidationRes =
|
||||
verification_from_config(getSettings().backend.isoVerification.getValue());
|
||||
state.initialLanguage = getSettings().game.language;
|
||||
state.initialGraphicsBackend = getSettings().backend.graphicsBackend;
|
||||
state.initialCardFileType = getSettings().backend.cardFileType;
|
||||
@@ -397,10 +725,6 @@ void Prelaunch::update() {
|
||||
|
||||
const auto discStatusLabel = mDiscStatus->GetElementById("disc-status-label");
|
||||
|
||||
const bool discHashMismatchAccepted =
|
||||
hasValidPath && !state.userAcceptedDiscPath.empty() &&
|
||||
state.selectedDiscPath == state.userAcceptedDiscPath;
|
||||
|
||||
if (mDiscStatus != nullptr && discStatusLabel != nullptr) {
|
||||
if (state.initialDiscValidationRes == iso::ValidationError::Success) {
|
||||
mDiscStatus->SetAttribute("status", "good");
|
||||
@@ -408,6 +732,9 @@ void Prelaunch::update() {
|
||||
} else if (state.initialDiscValidationRes == iso::ValidationError::HashMismatch) {
|
||||
mDiscStatus->SetAttribute("status", "mismatch");
|
||||
discStatusLabel->SetInnerRML("Disc hash mismatch.");
|
||||
} else if (hasValidPath) {
|
||||
mDiscStatus->SetAttribute("status", "unknown");
|
||||
discStatusLabel->SetInnerRML("Disc not verified.");
|
||||
} else {
|
||||
mDiscStatus->RemoveAttribute("status");
|
||||
discStatusLabel->SetInnerRML("No disc image found.");
|
||||
|
||||
@@ -48,7 +48,7 @@ struct PrelaunchState {
|
||||
int initialCardFileType = 0;
|
||||
std::string errorString;
|
||||
std::string pendingDiscPath;
|
||||
std::string userAcceptedDiscPath;
|
||||
iso::ValidationError pendingDiscValidation = iso::ValidationError::Unknown;
|
||||
};
|
||||
|
||||
PrelaunchState& prelaunch_state() noexcept;
|
||||
|
||||
+12
-7
@@ -172,7 +172,7 @@ Document& push_document(std::unique_ptr<Document> doc, bool show, bool passive)
|
||||
if (passive) {
|
||||
sPassiveDocuments.push_back(std::move(doc));
|
||||
} else {
|
||||
sDocumentStack.push_back({std::move(doc)});
|
||||
sDocumentStack.push_back(std::move(doc));
|
||||
}
|
||||
if (show) {
|
||||
ret.show();
|
||||
@@ -215,12 +215,17 @@ void update() noexcept {
|
||||
}
|
||||
|
||||
input::update_input();
|
||||
for (const auto& doc : sDocumentStack) {
|
||||
doc->update();
|
||||
}
|
||||
for (const auto& doc : sPassiveDocuments) {
|
||||
doc->update();
|
||||
}
|
||||
const auto update_documents = [](auto& documents) {
|
||||
const std::size_t count = documents.size();
|
||||
for (std::size_t i = 0; i < count && i < documents.size(); ++i) {
|
||||
Document* doc = documents[i].get();
|
||||
if (doc != nullptr && !doc->closed()) {
|
||||
doc->update();
|
||||
}
|
||||
}
|
||||
};
|
||||
update_documents(sDocumentStack);
|
||||
update_documents(sPassiveDocuments);
|
||||
|
||||
// Remove closed documents
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user