From 47593d0eb43a342bf1131156b50231b1b895316c Mon Sep 17 00:00:00 2001 From: Irastris Date: Wed, 6 May 2026 16:17:18 -0400 Subject: [PATCH] Allow hash-mismatched discs to be loaded with user confirmation --- src/dusk/ui/prelaunch.cpp | 150 +++++++++++++++++++++++++++++--------- src/dusk/ui/prelaunch.hpp | 9 ++- src/dusk/ui/settings.cpp | 23 +----- src/m_Do/m_Do_main.cpp | 7 +- 4 files changed, 126 insertions(+), 63 deletions(-) diff --git a/src/dusk/ui/prelaunch.cpp b/src/dusk/ui/prelaunch.cpp index b091239bc2..02fabde70f 100644 --- a/src/dusk/ui/prelaunch.cpp +++ b/src/dusk/ui/prelaunch.cpp @@ -80,16 +80,22 @@ void file_dialog_callback(void*, const char* path, const char* error) { auto& state = prelaunch_state(); const auto validation = iso::validate(path); - if (validation != iso::ValidationError::Success) { - state.errorString = escape(get_error_msg(validation)); + 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; } - - state.selectedDiscPath = path; - state.errorString.clear(); - getSettings().backend.isoPath.setValue(state.selectedDiscPath); - config::Save(); - refresh_state(); + if (validation == iso::ValidationError::HashMismatch) { + state.pendingDiscPath = path; + } else { + state.pendingDiscPath.clear(); + } + state.errorString = escape(get_error_msg(validation)); } PrelaunchState sPrelaunchState; @@ -100,13 +106,86 @@ PrelaunchState& prelaunch_state() noexcept { void refresh_state() noexcept { auto& state = prelaunch_state(); - const auto validation = iso::validate(state.selectedDiscPath.c_str()); - if (state.selectedDiscPath.empty() || validation != iso::ValidationError::Success) { + if (state.selectedDiscPath.empty()) { state.selectedDiscIsValid = false; return; } - state.selectedDiscIsValid = true; - state.selectedDiscIsPal = iso::isPal(state.selectedDiscPath.c_str()); + if (!state.userAcceptedDiscPath.empty() && + state.selectedDiscPath == state.userAcceptedDiscPath) { + state.selectedDiscIsValid = true; + state.selectedDiscIsPal = iso::isPal(state.selectedDiscPath.c_str()); + 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(); + return; + } + state.selectedDiscIsValid = false; +} + +void try_push_verification_modal(Document& host) { + auto& state = prelaunch_state(); + if (state.errorString.empty()) { + return; + } + + auto dismiss = [](Modal& modal) { + auto& state = prelaunch_state(); + state.errorString.clear(); + state.pendingDiscPath.clear(); + modal.pop(); + }; + + if (!state.pendingDiscPath.empty()) { + const Rml::String bodyRml = state.errorString + "

You may proceed at your own risk."; + auto acceptHashMismatch = [](Modal& modal) { + auto& st = prelaunch_state(); + std::string path = std::move(st.pendingDiscPath); + st.pendingDiscPath.clear(); + st.errorString.clear(); + st.selectedDiscPath = path; + st.userAcceptedDiscPath = path; + getSettings().backend.isoPath.setValue(path); + config::Save(); + refresh_state(); + modal.pop(); + }; + host.push(std::make_unique(Modal::Props{ + .title = "Disc verification", + .bodyRml = bodyRml, + .actions = + { + ModalAction{ + .label = "Cancel", + .onPressed = dismiss, + }, + ModalAction{ + .label = "Continue anyway", + .onPressed = acceptHashMismatch, + }, + }, + .onDismiss = dismiss, + })); + return; + } + + host.push(std::make_unique(Modal::Props{ + .title = "Disc verification", + .bodyRml = state.errorString, + .actions = + { + ModalAction{ + .label = "OK", + .onPressed = dismiss, + }, + }, + .onDismiss = dismiss, + })); } void ensure_initialized() noexcept { @@ -117,7 +196,9 @@ void ensure_initialized() noexcept { state.selectedDiscPath = getSettings().backend.isoPath; state.initialDiscPath = state.selectedDiscPath; - if (iso::validate(state.initialDiscPath.c_str()) == iso::ValidationError::Success) { + 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.initialLanguage = getSettings().game.language; @@ -287,28 +368,14 @@ void Prelaunch::update() { ensure_initialized(); try_apply_mirrored_layout(mDocument); - auto& state = prelaunch_state(); - if (!state.errorString.empty() && top_document() == this) { - auto dismiss = [](Modal& modal) { - prelaunch_state().errorString.clear(); - modal.pop(); - }; - push(std::make_unique(Modal::Props{ - .title = "Invalid disc image", - .bodyRml = state.errorString, - .actions = - { - ModalAction{ - .label = "OK", - .onPressed = dismiss, - }, - }, - .onDismiss = dismiss, - })); + if (top_document() == this) { + try_push_verification_modal(*this); } - const bool hasValidPath = prelaunch_state().selectedDiscIsValid; - mDocument->SetClass("disc-ready", hasValidPath); + const auto& state = prelaunch_state(); + + const bool hasValidPath = state.selectedDiscIsValid; + mDocument->SetClass("disc-ready", IsGameLaunched); if (hasValidPath) { if (getSettings().backend.skipPreLaunchUI) { hide(true); @@ -327,17 +394,28 @@ 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 (hasValidPath) { + if (state.initialDiscValidationRes == iso::ValidationError::Success) { mDiscStatus->SetAttribute("status", "good"); discStatusLabel->SetInnerRML("Disc ready."); + } else if (state.initialDiscValidationRes == iso::ValidationError::HashMismatch) { + mDiscStatus->SetAttribute("status", "mismatch"); + discStatusLabel->SetInnerRML("Disc hash mismatch."); + } else { + mDiscStatus->RemoveAttribute("status"); + discStatusLabel->SetInnerRML("No disc image found."); } } if (mDiscDetail != nullptr) { if (hasValidPath) { mDiscDetail->SetProperty(Rml::PropertyId::Display, Rml::Style::Display::Block); - mDiscDetail->SetInnerRML( - prelaunch_state().initialDiscIsPal ? "GameCube • EUR" : "GameCube • USA"); + Rml::String innerRML = "GameCube • "; + innerRML += state.initialDiscIsPal ? "EUR" : "USA"; + mDiscDetail->SetInnerRML(innerRML); } else { mDiscDetail->SetProperty(Rml::PropertyId::Display, Rml::Style::Display::None); } diff --git a/src/dusk/ui/prelaunch.hpp b/src/dusk/ui/prelaunch.hpp index e4d542cc2d..73033f9816 100644 --- a/src/dusk/ui/prelaunch.hpp +++ b/src/dusk/ui/prelaunch.hpp @@ -1,5 +1,6 @@ #pragma once +#include "dusk/iso_validate.hpp" #include "button.hpp" #include "document.hpp" @@ -39,12 +40,15 @@ struct PrelaunchState { std::string selectedDiscPath; bool selectedDiscIsValid = false; bool selectedDiscIsPal = false; - std::string errorString; - bool initialDiscIsPal = false; std::string initialDiscPath; + iso::ValidationError initialDiscValidationRes = iso::ValidationError::Unknown; + bool initialDiscIsPal = false; GameLanguage initialLanguage = GameLanguage::English; std::string initialGraphicsBackend; int initialCardFileType = 0; + std::string errorString; + std::string pendingDiscPath; + std::string userAcceptedDiscPath; }; PrelaunchState& prelaunch_state() noexcept; @@ -52,5 +56,6 @@ void ensure_initialized() noexcept; void refresh_state() noexcept; void open_iso_picker() noexcept; bool is_restart_pending() noexcept; +void try_push_verification_modal(Document& host); } // namespace dusk::ui diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 7979b22116..ddb6a4cd31 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -17,8 +17,6 @@ #include -#include "modal.hpp" - namespace dusk::ui { namespace { @@ -869,27 +867,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { } void SettingsWindow::update() { - // Show disc validation error message if present if (mPrelaunch && top_document() == this) { - auto& state = prelaunch_state(); - if (!state.errorString.empty()) { - auto dismissInvalidDisc = [](Modal& modal) { - prelaunch_state().errorString.clear(); - modal.pop(); - }; - push_document(std::make_unique(Modal::Props{ - .title = "Invalid disc image", - .bodyRml = state.errorString, - .actions = - { - ModalAction{ - .label = "OK", - .onPressed = dismissInvalidDisc, - }, - }, - .onDismiss = dismissInvalidDisc, - })); - } + try_push_verification_modal(*this); } Window::update(); diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index f7b6cd8865..e996589b59 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -621,7 +621,7 @@ int game_main(int argc, char* argv[]) { // Invalidate a bad saved isoPath so that Dusk can't get blocked from starting up const std::string p = dusk::getSettings().backend.isoPath; - if (!p.empty() && dusk::iso::validate(p.c_str()) != dusk::iso::ValidationError::Success) { + if (!p.empty() && dusk::iso::validate(p.c_str()) < dusk::iso::ValidationError::HashMismatch) { dusk::getSettings().backend.isoPath.setValue(""); } @@ -629,7 +629,7 @@ int game_main(int argc, char* argv[]) { bool dvd_opened = false; if (parsed_arg_options.count("dvd")) { dvd_path = parsed_arg_options["dvd"].as(); - if (dusk::iso::validate(dvd_path.c_str()) == dusk::iso::ValidationError::Success) { + if (dusk::iso::validate(dvd_path.c_str()) >= dusk::iso::ValidationError::HashMismatch) { DuskLog.info("Loading DVD image from command line: {}", dvd_path); dvd_opened = aurora_dvd_open(dvd_path.c_str()); if (!dvd_opened) { @@ -668,7 +668,8 @@ int game_main(int argc, char* argv[]) { if (dvd_path.empty()) { DuskLog.fatal("No DVD image specified, unable to boot!"); } - if (dusk::iso::validate(dvd_path.c_str()) != dusk::iso::ValidationError::Success) { + if (!dusk::IsGameLaunched && + dusk::iso::validate(dvd_path.c_str()) < dusk::iso::ValidationError::HashMismatch) { DuskLog.fatal("DVD image failed verification: {}", dvd_path); } DuskLog.info("Loading DVD image: {}", dvd_path);