Display the real title screen behind the prelaunch menu (#638)

* Start game execution as soon as a disk image is available

* Do not update dDemo_c if prelaunch document is visible

* Prevent intro music until prelaunch has popped

* Replace "Start Game" references with "Play"

* Make prelaunch layout respect mirror mode

* Add drop shadow to prelaunch disk-status and version-info

* Remove ImGui prelaunch

* Add "Change Disk Image" button to prelaunch options

* Actually validate discs and make prelaunch very betterer :)

* Check your build before pushing dumbass, and go to sleep

* "Disc" consistency, adjust restart notice logic

* Better LanguageSelect logic

* Add restart notice to SaveTypeSelect

* Added wind sounds to the pre-launch menu

* Add Modal document, use it for disc verification

* Consolidate Modal and PresetWindow

* Squash various bugs, rearrange document flow

* Allow Window inheritors to opt-out of being toggleable

* Tweak focus behavior/syntax

* Implement "Restart Now" option

* Tweaks

* Remove a bunch of dynamic_cast

* Update README.md

---------

Co-authored-by: Luke Street <luke@street.dev>
This commit is contained in:
Irastris
2026-05-05 14:18:25 -04:00
committed by GitHub
parent 7300c0e0f5
commit 08321699cd
27 changed files with 911 additions and 485 deletions
+120 -30
View File
@@ -1,10 +1,13 @@
#include "prelaunch.hpp"
#include "popup.hpp"
#include "dusk/config.hpp"
#include "dusk/file_select.hpp"
#include "dusk/iso_validate.hpp"
#include "dusk/main.h"
#include "dusk/settings.h"
#include "dusk/ui/prelaunch_options.hpp"
#include "dusk/ui/preset.hpp"
#include "version.h"
#include <SDL3/SDL_dialog.h>
@@ -12,7 +15,6 @@
#include <aurora/lib/window.hpp>
namespace dusk::ui {
namespace {
const Rml::String kDocumentSource = R"RML(
<rml>
@@ -20,6 +22,8 @@ const Rml::String kDocumentSource = R"RML(
<link type="text/rcss" href="res/rml/prelaunch.rcss" />
</head>
<body>
<div class="gradient" />
<div class="background" />
<content id="root" open>
<menu>
<hero class="intro-item delay-0">
@@ -49,6 +53,23 @@ constexpr std::array<SDL_DialogFileFilter, 2> kDiscFileFilters{{
{"All Files", "*"},
}};
static std::string get_error_msg(iso::ValidationError error) {
switch (error) {
case iso::ValidationError::IOError:
return "Unable to read the selected file.";
case iso::ValidationError::InvalidImage:
return "The selected file is not a valid disc image.";
case iso::ValidationError::WrongGame:
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::Success:
return "The selected disc image is valid.";
default:
return "The selected disc image could not be validated.";
}
}
void file_dialog_callback(void*, const char* path, const char* error) {
auto& state = prelaunch_state();
if (error != nullptr) {
@@ -58,14 +79,18 @@ void file_dialog_callback(void*, const char* path, const char* error) {
return;
}
state.selectedIsoPath = path;
state.errorString.clear();
refresh_path_state();
getSettings().backend.isoPath.setValue(state.selectedIsoPath);
config::Save();
}
const auto validation = iso::validate(path);
if (validation != iso::ValidationError::Success) {
state.errorString = escape(get_error_msg(validation));
return;
}
} // namespace
state.selectedDiscPath = path;
state.errorString.clear();
getSettings().backend.isoPath.setValue(state.selectedDiscPath);
config::Save();
refresh_state();
}
PrelaunchState sPrelaunchState;
@@ -73,9 +98,15 @@ PrelaunchState& prelaunch_state() noexcept {
return sPrelaunchState;
}
void refresh_path_state() noexcept {
void refresh_state() noexcept {
auto& state = prelaunch_state();
state.isPal = !state.selectedIsoPath.empty() && iso::isPal(state.selectedIsoPath.c_str());
const auto validation = iso::validate(state.selectedDiscPath.c_str());
if (state.selectedDiscPath.empty() || validation != iso::ValidationError::Success) {
state.selectedDiscIsValid = false;
return;
}
state.selectedDiscIsValid = true;
state.selectedDiscIsPal = iso::isPal(state.selectedDiscPath.c_str());
}
void ensure_initialized() noexcept {
@@ -84,16 +115,17 @@ void ensure_initialized() noexcept {
return;
}
state.selectedIsoPath = getSettings().backend.isoPath;
state.selectedDiscPath = getSettings().backend.isoPath;
state.initialDiscPath = state.selectedDiscPath;
if (iso::validate(state.initialDiscPath.c_str()) == iso::ValidationError::Success) {
state.initialDiscIsPal = iso::isPal(state.initialDiscPath.c_str());
}
state.initialLanguage = getSettings().game.language;
state.initialGraphicsBackend = getSettings().backend.graphicsBackend;
state.initialCardFileType = getSettings().backend.cardFileType;
state.errorString.clear();
state.initialized = true;
refresh_path_state();
}
bool is_selected_path_valid() noexcept {
return !prelaunch_state().selectedIsoPath.empty() &&
SDL_GetPathInfo(prelaunch_state().selectedIsoPath.c_str(), nullptr);
refresh_state();
}
void open_iso_picker() noexcept {
@@ -102,6 +134,23 @@ void open_iso_picker() noexcept {
kDiscFileFilters.data(), kDiscFileFilters.size(), nullptr, false);
}
bool is_restart_pending() noexcept {
const auto& state = prelaunch_state();
if (!state.initialDiscPath.empty() && state.selectedDiscPath != state.initialDiscPath) {
return true;
}
if (getSettings().backend.graphicsBackend.getValue() != state.initialGraphicsBackend) {
return true;
}
if (getSettings().game.language.getValue() != state.initialLanguage) {
return true;
}
if (getSettings().backend.cardFileType.getValue() != state.initialCardFileType) {
return true;
}
return false;
}
void apply_intro_animation(Rml::Element* element, const char* delay_class) {
if (element == nullptr || delay_class == nullptr) {
return;
@@ -110,19 +159,38 @@ void apply_intro_animation(Rml::Element* element, const char* delay_class) {
element->SetClass(delay_class, true);
}
void try_apply_mirrored_layout(Rml::Element* body) {
if (body == nullptr) {
return;
}
body->SetClass("mirrored", getSettings().game.enableMirrorMode.getValue());
}
Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementById("root")) {
ensure_initialized();
if (auto* menuList = mDocument->GetElementById("menu-list")) {
const bool hasValidPath = is_selected_path_valid();
auto& state = prelaunch_state();
mMenuButtons.push_back(
std::make_unique<Button>(menuList, hasValidPath ? "Start Game" : "Select Disc Image"));
std::make_unique<Button>(menuList, state.selectedDiscIsValid ? "Play" : "Select Disc Image"));
mMenuButtons.back()->on_pressed([this] {
if (!is_selected_path_valid()) {
if (!prelaunch_state().selectedDiscIsValid) {
open_iso_picker();
return;
}
if (getSettings().audio.menuSounds) {
JAISoundHandle* handle = g_mEnvSeMgr.field_0x144.getHandle();
if (*handle) {
(*handle)->stop(60);
(*handle)->releaseHandle();
}
}
IsGameLaunched = true;
if (!getSettings().backend.wasPresetChosen) {
push_document(std::make_unique<dusk::ui::PresetWindow>());
}
hide(true);
});
apply_intro_animation(mMenuButtons.back()->root(), "delay-1");
@@ -140,6 +208,8 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
mDiscDetail = mDocument->GetElementById("disc-version");
mVersion = mDocument->GetElementById("version-text");
try_apply_mirrored_layout(mDocument);
listen(mDocument, Rml::EventId::Transitionend, [this](Rml::Event& event) {
auto* target = event.GetTargetElement();
if (target == nullptr) {
@@ -164,6 +234,8 @@ void Prelaunch::hide(bool close) {
if (!mEntranceAnimationStarted) {
// Close document immediately
Document::hide(true);
} else {
mPendingClose = true;
}
mDocument->RemoveAttribute("open");
} else {
@@ -173,12 +245,33 @@ void Prelaunch::hide(bool close) {
void Prelaunch::update() {
ensure_initialized();
refresh_path_state();
try_apply_mirrored_layout(mDocument);
auto& state = prelaunch_state();
const bool hasValidPath = is_selected_path_valid();
if (hasValidPath && getSettings().backend.skipPreLaunchUI) {
hide(true);
if (!state.errorString.empty() && top_document() == this) {
auto dismissInvalidDisc = [](Modal& modal) {
prelaunch_state().errorString.clear();
modal.pop();
};
push_document(std::make_unique<Modal>(Modal::Props{
.title = "Invalid disc image",
.bodyRml = state.errorString,
.actions = {
ModalAction{
.label = "OK",
.onPressed = dismissInvalidDisc,
},
},
.onDismiss = dismissInvalidDisc,
}));
}
const bool hasValidPath = prelaunch_state().selectedDiscIsValid;
mDocument->SetClass("disc-ready", hasValidPath);
if (hasValidPath) {
if (getSettings().backend.skipPreLaunchUI) {
hide(true);
}
IsGameLaunched = true;
}
@@ -188,7 +281,7 @@ void Prelaunch::update() {
}
if (!mMenuButtons.empty()) {
mMenuButtons[0]->set_text(hasValidPath ? "Start Game" : "Select Disc Image");
mMenuButtons[0]->set_text(hasValidPath ? "Play" : "Select Disc Image");
}
const auto discStatusLabel = mDiscStatus->GetElementById("disc-status-label");
@@ -197,15 +290,12 @@ void Prelaunch::update() {
if (hasValidPath) {
mDiscStatus->SetAttribute("status", "good");
discStatusLabel->SetInnerRML("Disc ready.");
} else {
mDiscStatus->SetAttribute("status", "bad");
discStatusLabel->SetInnerRML("Disc not found.");
}
}
if (mDiscDetail != nullptr) {
if (hasValidPath) {
mDiscDetail->SetProperty(Rml::PropertyId::Display, Rml::Style::Display::Block);
mDiscDetail->SetInnerRML(state.isPal ? "GameCube • EUR" : "GameCube • USA");
mDiscDetail->SetInnerRML(prelaunch_state().initialDiscIsPal ? "GameCube • EUR" : "GameCube • USA");
} else {
mDiscDetail->SetProperty(Rml::PropertyId::Display, Rml::Style::Display::None);
}
@@ -225,7 +315,7 @@ bool Prelaunch::focus() {
if (mMenuButtons.empty()) {
return false;
}
return mMenuButtons[0]->focus();
return mMenuButtons.front()->focus();
}
bool Prelaunch::visible() const {