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
+2 -2
View File
@@ -34,8 +34,8 @@ First, make sure your dump of the game is clean and supported by Dusk. You can d
- Extract the .zip file
- Launch Dusk
- Press **Select Disc Image**, navigate to your game dump, and select the file
- Press **Start Game** to play!
- Press **Select Disc Image** and provide the path to your supported game dump.
- Press **Play**!
# Building
+6 -6
View File
@@ -1447,8 +1447,6 @@ set(DUSK_FILES
src/dusk/imgui/ImGuiBloomWindow.hpp
src/dusk/imgui/ImGuiMenuTools.cpp
src/dusk/imgui/ImGuiMenuTools.hpp
src/dusk/imgui/ImGuiPreLaunchWindow.cpp
src/dusk/imgui/ImGuiPreLaunchWindow.hpp
src/dusk/imgui/ImGuiProcessOverlay.cpp
src/dusk/imgui/ImGuiCameraOverlay.cpp
src/dusk/imgui/ImGuiHeapOverlay.cpp
@@ -1459,6 +1457,8 @@ set(DUSK_FILES
src/dusk/imgui/ImGuiSaveEditor.cpp
src/dusk/imgui/ImGuiStateShare.hpp
src/dusk/imgui/ImGuiStateShare.cpp
src/dusk/ui/achievements.cpp
src/dusk/ui/achievements.hpp
src/dusk/ui/bool_button.cpp
src/dusk/ui/bool_button.hpp
src/dusk/ui/button.cpp
@@ -1469,16 +1469,14 @@ set(DUSK_FILES
src/dusk/ui/controller_config.hpp
src/dusk/ui/document.cpp
src/dusk/ui/document.hpp
src/dusk/ui/achievements.cpp
src/dusk/ui/achievements.hpp
src/dusk/ui/preset.cpp
src/dusk/ui/preset.hpp
src/dusk/ui/editor.cpp
src/dusk/ui/editor.hpp
src/dusk/ui/event.cpp
src/dusk/ui/event.hpp
src/dusk/ui/input.cpp
src/dusk/ui/input.hpp
src/dusk/ui/modal.cpp
src/dusk/ui/modal.hpp
src/dusk/ui/nav_types.hpp
src/dusk/ui/number_button.cpp
src/dusk/ui/number_button.hpp
@@ -1492,6 +1490,8 @@ set(DUSK_FILES
src/dusk/ui/prelaunch.hpp
src/dusk/ui/prelaunch_options.cpp
src/dusk/ui/prelaunch_options.hpp
src/dusk/ui/preset.cpp
src/dusk/ui/preset.hpp
src/dusk/ui/select_button.cpp
src/dusk/ui/select_button.hpp
src/dusk/ui/settings.cpp
+14
View File
@@ -1,6 +1,10 @@
#ifndef DUSK_MAIN_H
#define DUSK_MAIN_H
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#include <filesystem>
namespace dusk {
@@ -8,7 +12,17 @@ namespace dusk {
extern bool IsShuttingDown;
extern bool IsGameLaunched;
extern bool IsFocusPaused;
extern bool RestartRequested;
extern std::filesystem::path ConfigPath;
#if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \
(defined(TARGET_OS_TV) && TARGET_OS_TV)
inline constexpr bool SupportsProcessRestart = false;
#else
inline constexpr bool SupportsProcessRestart = true;
#endif
void RequestRestart() noexcept;
}
#endif // DUSK_MAIN_H
+72 -2
View File
@@ -9,17 +9,44 @@ body {
font-weight: normal;
font-size: 20dp;
color: #FFFFFF;
background-color: #000000;
decorator: image(../prelaunch-bg.png cover left center);
filter: opacity(0);
transition: filter 1s 0.2s linear-in-out;
z-index: -1;
}
.gradient {
position: absolute;
width: 100%;
height: 100%;
/* The color gradient from the Figma bands really badly. A fully black gradient does as well, but not as badly. */
decorator: horizontal-gradient(#000000FF #00000000);
}
body.mirrored .gradient {
decorator: horizontal-gradient(#00000000 #000000FF);
}
.background {
position: absolute;
width: 100%;
height: 100%;
decorator: image(../prelaunch-bg.png cover left center);
opacity: 0;
transition: opacity 1s linear-in-out;
}
body[open] {
filter: opacity(1);
}
body[open] .background {
opacity: 1;
}
body.disc-ready .background {
opacity: 0;
}
content {
display: block;
width: 100%;
@@ -35,6 +62,7 @@ content[open] {
menu {
position: absolute;
left: 96dp;
right: auto;
top: 50%;
transform: translateY(-50%);
/* Scale based on a reference screen width, 428/1216 */
@@ -47,6 +75,11 @@ menu {
gap: 48dp;
}
body.mirrored menu {
left: auto;
right: 96dp;
}
hero {
display: flex;
flex-direction: column;
@@ -55,6 +88,10 @@ hero {
gap: 8dp;
}
body.mirrored hero {
align-items: flex-end;
}
hero img {
width: 100%;
}
@@ -79,6 +116,7 @@ hero img {
display: flex;
flex-direction: column;
gap: 12dp;
align-items: flex-start;
}
#menu-list button {
@@ -86,6 +124,7 @@ hero img {
height: 54dp;
padding: 8dp 16dp;
border-radius: 8dp;
text-align: left;
text-transform: uppercase;
font-family: "Fira Sans Condensed";
font-size: 32dp;
@@ -105,25 +144,56 @@ hero img {
decorator: horizontal-gradient(#FEE685FF #FEE68500);
}
body.mirrored #menu-list {
align-items: flex-end;
}
body.mirrored #menu-list button {
text-align: right;
}
body.mirrored #menu-list button:hover,
body.mirrored #menu-list button:focus-visible {
decorator: horizontal-gradient(#FEE68500 #FEE685FF);
}
disc-info {
position: absolute;
left: 96dp;
right: auto;
bottom: 72dp;
display: flex;
flex-direction: column;
gap: 12dp;
font-size: 24dp;
font-effect: glow(0dp 4dp 0dp 4dp black);
text-align: left;
}
body.mirrored disc-info {
left: auto;
right: 96dp;
text-align: right;
}
version-info {
position: absolute;
right: 96dp;
left: auto;
bottom: 72dp;
display: flex;
flex-direction: column;
gap: 12dp;
text-align: right;
font-size: 24dp;
font-effect: glow(0dp 4dp 0dp 4dp black);
text-align: right;
}
body.mirrored version-info {
right: auto;
left: 96dp;
text-align: left;
}
#disc-status {
+39
View File
@@ -43,11 +43,19 @@ window.preset {
min-width: 650dp;
}
window.modal {
max-width: 816dp;
}
window[open] {
filter: opacity(1);
transform: scale(1);
}
window[open].blurred {
filter: blur(2dp);
}
@media (max-height: 640dp) {
body {
padding: 16dp;
@@ -108,6 +116,12 @@ window content pane > spacer {
pointer-events: none;
}
window modal {
padding: 32dp;
gap: 20dp;
flex: 0 1 auto;
}
scrollbarvertical {
width: 8dp;
margin: 4dp 4dp 4dp 0;
@@ -194,6 +208,12 @@ button:not(:disabled):active {
box-shadow: #C2A42D 0 0 0 2dp;
}
button.modal-btn {
font-size: 20dp;
padding: 16dp 10dp;
flex: 1 1 0;
}
select-button {
display: flex;
align-items: center;
@@ -399,3 +419,22 @@ button.preset-btn {
color: rgba(224, 219, 200, 65%);
text-align: center;
}
.modal-dialog {
display: flex;
flex-flow: column;
padding: 16dp;
gap: 20dp;
flex: 0 1 auto;
min-width: 0;
}
.modal-actions {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: stretch;
gap: 12dp;
padding-top: 12dp;
width: 100%;
}
+73
View File
@@ -11,6 +11,62 @@
#include "JSystem/JGadget/define.h"
#include <cstring>
#include "dusk/logging.h"
#if TARGET_PC
#include "dusk/ui/ui.hpp"
namespace {
static int sJaiSkip = -1;
static JSUList<JAIStream>* get_stream_list() {
return Z2GetSoundMgr()->getStreamMgr()->getStreamList();
}
static int get_stream_count(JSUList<JAIStream>* list) {
int i = 0;
for (JSULink<JAIStream>* l = list != nullptr ? list->getFirst() : nullptr; l != nullptr;
l = l->getNext()) {
i++;
}
return i;
}
static void pause_stream(int skip_first, bool paused) {
int i = 0;
JSUList<JAIStream>* list = get_stream_list();
for (JSULink<JAIStream>* l = list->getFirst(); l != nullptr; l = l->getNext(), ++i) {
if (i >= skip_first) {
l->getObject()->pause(paused);
}
}
}
static void pause_streams(int skip_first) {
if (!dusk::ui::is_prelaunch_open()) {
return;
}
JSUList<JAIStream>* list = get_stream_list();
if (list == nullptr || get_stream_count(list) <= skip_first) {
return;
}
pause_stream(skip_first, true);
sJaiSkip = skip_first;
}
static void unpause_streams(bool require_prelaunch_hidden) {
if (sJaiSkip < 0) {
return;
}
if (require_prelaunch_hidden && dusk::ui::is_prelaunch_open()) {
return;
}
pause_stream(sJaiSkip, false);
sJaiSkip = -1;
}
} // namespace
#endif
s16 dDemo_c::m_branchId = -1;
namespace {
@@ -1006,7 +1062,16 @@ int dDemo_c::start(u8 const* p_data, cXyz* p_translation, f32 rotationY) {
m_control->setSuspend(0);
}
#if TARGET_PC
const int existing_streams = get_stream_count(get_stream_list());
#endif
m_control->forward(0);
#if TARGET_PC
pause_streams(existing_streams);
#endif
m_translation = p_translation;
if (m_translation != NULL) {
@@ -1034,6 +1099,10 @@ static void dummyString2() {
void dDemo_c::end() {
JUT_ASSERT(1956, m_system != NULL);
#if TARGET_PC
unpause_streams(false);
#endif
m_control->destroyObject_all();
m_object->remove();
m_data = NULL;
@@ -1054,6 +1123,10 @@ void dDemo_c::branch() {
int dDemo_c::update() {
JUT_ASSERT(2064, m_system != NULL);
#if TARGET_PC
unpause_streams(true);
#endif
if (m_data == NULL) {
if (m_branchData == NULL) {
return 0;
+12 -1
View File
@@ -40,8 +40,9 @@
#include "JSystem/JKernel/JKRAramArchive.h"
#if TARGET_PC
#include "dusk/autosave.h"
#include "dusk/memory.h"
#include <dusk/autosave.h>
#include "dusk/ui/ui.hpp"
#endif
#if DEBUG
@@ -794,7 +795,17 @@ static int dScnPly_Execute(dScnPly_c* i_this) {
dJprev_c::get()->update();
#endif
#if TARGET_PC
if (!dusk::ui::is_prelaunch_open()) {
dDemo_c::update();
} else if (dusk::getSettings().audio.menuSounds) {
s8 reverb = dComIfGp_getReverb(dComIfGp_roomControl_getStayNo());
f32 fxMix = reverb / 127.0f;
g_mEnvSeMgr.field_0x144.startEnvSeDirLevel(JA_SE_ATM_WIND_1, fxMix, 1.0f);
}
#else
dDemo_c::update();
#endif
#if DEBUG
dJcame_c::get()->update();
-4
View File
@@ -274,10 +274,6 @@ namespace dusk {
ImGuiMenuGame::ToggleFullscreen();
}
// if (!dusk::IsGameLaunched) {
// m_preLaunchWindow.draw();
// }
if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_F1)) {
m_isHidden = !m_isHidden;
}
-2
View File
@@ -9,7 +9,6 @@
#include "ImGuiMenuGame.hpp"
#include "ImGuiMenuTools.hpp"
#include "ImGuiPreLaunchWindow.hpp"
#include "imgui.h"
union SDL_Event;
@@ -45,7 +44,6 @@ private:
std::deque<Toast> m_toasts;
ImGuiMenuGame m_menuGame;
ImGuiPreLaunchWindow m_preLaunchWindow;
// Keep always last
ImGuiMenuTools m_menuTools;
-282
View File
@@ -1,282 +0,0 @@
#include "imgui.h"
#include "ImGuiConfig.hpp"
#include "ImGuiEngine.hpp"
#include "ImGuiPreLaunchWindow.hpp"
#include "../file_select.hpp"
#include "../iso_validate.hpp"
#include "ImGuiConsole.hpp"
#include "dusk/main.h"
#include "dusk/settings.h"
#include <SDL3/SDL_dialog.h>
#include <SDL3/SDL_filesystem.h>
#include "aurora/lib/internal.hpp"
#include "aurora/lib/window.hpp"
namespace dusk {
typedef void (ImGuiPreLaunchWindow::*drawFunc)();
drawFunc drawTable[2] = {&ImGuiPreLaunchWindow::drawMainMenu, &ImGuiPreLaunchWindow::drawOptions};
static constexpr std::array<const char*, 5> skLanguageNames = {
"English", "German", "French", "Spanish", "Italian"
};
static constexpr std::array<SDL_DialogFileFilter, 2> skGameDiscFileFilters{{
{"Game Disc Images", "iso;gcm;ciso;gcz;nfs;rvz;wbfs;wia;tgc"},
{"All Files", "*"},
}};
static std::string ShowIsoInvalidError(const iso::ValidationError code) {
using namespace std::literals::string_literals;
switch (code) {
case iso::ValidationError::IOError:
return "Unknown IO error occurred"s;
case iso::ValidationError::InvalidImage:
return "Unable to interpret selected file as a disc image"s;
case iso::ValidationError::WrongGame:
return "Selected disc image is for a different game"s;
case iso::ValidationError::WrongVersion:
return "Selected disc image is for an unsupported version of the game. Only North American GameCube (NTSC/GZ2E01) is supported at this time."s;
case iso::ValidationError::ExecutableMismatch:
return "Selected disc image contains modified executable files."s;
default:
return "Unknown error"s;
}
}
static std::string_view card_type_name(CARDFileType type) {
switch (type) {
case CARD_GCIFOLDER:
return "GCI Folder"sv;
case CARD_RAWIMAGE:
return "Card Image"sv;
default:
return ""sv;
}
}
void fileDialogCallback(void* userdata, const char* path, const char* error) {
auto* self = static_cast<ImGuiPreLaunchWindow*>(userdata);
if (error != nullptr) {
self->m_selectedIsoPath.clear();
self->m_errorString = fmt::format("File dialog error: {}", error);
return;
}
if (path == nullptr) {
self->m_selectedIsoPath.clear();
return;
}
self->m_selectedIsoPath = path;
self->m_isPal = iso::isPal(path);
getSettings().backend.isoPath.setValue(self->m_selectedIsoPath);
config::Save();
}
ImGuiPreLaunchWindow::ImGuiPreLaunchWindow() = default;
bool ImGuiPreLaunchWindow::isSelectedPathValid() const {
#if TARGET_ANDROID
return !m_selectedIsoPath.empty(); // unsure why SDL_GetPathInfo doesnt work here
#else
return !m_selectedIsoPath.empty() && SDL_GetPathInfo(m_selectedIsoPath.c_str(), nullptr);
#endif
}
void ImGuiPreLaunchWindow::draw() {
if (m_IsFirstDraw) {
m_selectedIsoPath = getSettings().backend.isoPath;
m_isPal = !m_selectedIsoPath.empty() && iso::isPal(m_selectedIsoPath.c_str());
m_initialGraphicsBackend = getSettings().backend.graphicsBackend;
m_IsFirstDraw = false;
}
if (isSelectedPathValid() && getSettings().backend.skipPreLaunchUI) {
dusk::IsGameLaunched = true;
return;
}
auto& io = ImGui::GetIO();
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y));
ImGui::SetNextWindowPos(ImVec2(0, 0));
ImGui::SetNextWindowBgAlpha(0.65f);
ImGui::Begin("Pre Launch Window", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing |
ImGuiWindowFlags_NoBringToFrontOnFocus);
const auto& windowSize = ImGui::GetWindowSize();
for (int i = 0; i < 5; i++)
ImGui::NewLine();
float iconSize = 150.f;
ImGui::SameLine(windowSize.x / 2 - iconSize + (iconSize / 2));
if (ImGuiEngine::orgIcon != 0) {
ImGui::Image(ImGuiEngine::orgIcon, ImVec2{iconSize, iconSize});
}
ImGuiTextCenter("Twilit Realm presents");
if (ImGuiEngine::duskLogo) {
ImGui::NewLine();
float width = iconSize * 2.5f;
ImGui::SameLine(windowSize.x / 2 - width + (width / 2));
ImGui::Image(ImGuiEngine::duskLogo, ImVec2{width, iconSize});
} else {
ImGui::PushFont(ImGuiEngine::fontExtraLarge);
ImGuiTextCenter("Dusk");
ImGui::PopFont();
}
(this->*drawTable[m_CurMenu])();
ImGui::End();
}
void ImGuiPreLaunchWindow::drawMainMenu() {
const auto& windowSize = ImGui::GetWindowSize();
ImGui::SetCursorPosY(windowSize.y - 200);
ImGui::PushFont(ImGuiEngine::fontLarge);
if (!isSelectedPathValid()) {
if (!m_errorString.empty()) {
ImGuiTextCenter(m_errorString);
}
if (ImGuiButtonCenter("Select disc image...")) {
ShowFileSelect(&fileDialogCallback, this, aurora::window::get_sdl_window(),
skGameDiscFileFilters.data(), int(skGameDiscFileFilters.size()), nullptr,
false);
}
} else {
if (ImGuiButtonCenter("Start game")) {
dusk::IsGameLaunched = true;
}
}
if (ImGuiButtonCenter("Options")) {
m_CurMenu = 1;
}
ImGui::PopFont();
}
void ImGuiPreLaunchWindow::drawOptions() {
const auto& windowSize = ImGui::GetWindowSize();
ImGui::NewLine();
ImGui::PushFont(ImGuiEngine::fontLarge);
ImGuiTextCenter("Options");
ImGui::Separator();
ImGui::PopFont();
auto cursorY = ImGui::GetCursorPosY();
float endCursorY = windowSize.y - 100;
float childWidth = windowSize.x - 400;
ImGui::SetCursorPosX(windowSize.x / 2 - (childWidth / 2));
if (ImGui::BeginChild("OptionsChild", ImVec2(childWidth, endCursorY - cursorY),
ImGuiChildFlags_None, ImGuiWindowFlags_NoBackground))
{
if (!m_errorString.empty()) {
ImGuiTextCenter(m_errorString);
}
ImGui::InputText("Game ISO Path", &m_selectedIsoPath, ImGuiInputTextFlags_ReadOnly);
ImGui::SameLine();
if (ImGui::Button(m_selectedIsoPath == "" ? "Set" : "Change")) {
ShowFileSelect(&fileDialogCallback, this, aurora::window::get_sdl_window(),
skGameDiscFileFilters.data(), int(skGameDiscFileFilters.size()), nullptr,
false);
}
if (m_isPal) {
auto selectedLanguage = getSettings().game.language.getValue();
if (ImGui::BeginCombo("Language", skLanguageNames[static_cast<u8>(selectedLanguage)])) {
for (u8 i = 0; i < skLanguageNames.size(); ++i) {
if (ImGui::Selectable(skLanguageNames[i])) {
getSettings().game.language.setValue(static_cast<GameLanguage>(i));
config::Save();
}
}
ImGui::EndCombo();
}
}
AuroraBackend configuredBackend = BACKEND_AUTO;
const std::string& configuredBackendId = getSettings().backend.graphicsBackend;
if (!try_parse_backend(configuredBackendId, configuredBackend)) {
configuredBackend = BACKEND_AUTO;
}
if (ImGui::BeginCombo("Graphics Backend", backend_name(configuredBackend).data())) {
if (ImGui::Selectable("Auto", configuredBackend == BACKEND_AUTO)) {
getSettings().backend.graphicsBackend.setValue("auto");
config::Save();
}
size_t backendCount = 0;
const AuroraBackend* availableBackends = aurora_get_available_backends(&backendCount);
for (size_t i = 0; i < backendCount; ++i) {
const AuroraBackend backend = availableBackends[i];
const bool isSelected = configuredBackend == backend;
if (ImGui::Selectable(backend_name(backend).data(), isSelected)) {
getSettings().backend.graphicsBackend.setValue(
std::string(backend_id(backend)));
config::Save();
}
if (isSelected) {
ImGui::SetItemDefaultFocus();
}
}
ImGui::EndCombo();
}
if (configuredBackendId != m_initialGraphicsBackend) {
ImGui::TextDisabled("Restart Required");
}
auto curFileType = (CARDFileType)getSettings().backend.cardFileType.getValue();
if (ImGui::BeginCombo("Save File Type", card_type_name(curFileType).data())) {
if (ImGui::Selectable("GCI Folder", curFileType == CARD_GCIFOLDER)) {
getSettings().backend.cardFileType.setValue(CARD_GCIFOLDER);
config::Save();
}
if (ImGui::Selectable("Card Image", curFileType == CARD_RAWIMAGE)) {
getSettings().backend.cardFileType.setValue(CARD_RAWIMAGE);
config::Save();
}
ImGui::EndCombo();
}
ImGui::EndChild();
}
ImGui::SetCursorPosY(endCursorY);
ImGui::NewLine();
ImGui::Separator();
ImGui::PushFont(ImGuiEngine::fontLarge);
if (ImGuiButtonCenter("Back")) {
m_CurMenu = 0;
}
ImGui::PopFont();
}
} // namespace dusk
-23
View File
@@ -1,23 +0,0 @@
#pragma once
namespace dusk {
class ImGuiPreLaunchWindow {
private:
int m_CurMenu = 0;
bool m_IsFirstDraw = true;
std::string m_initialGraphicsBackend;
bool isSelectedPathValid() const;
public:
ImGuiPreLaunchWindow();
void draw();
void drawMainMenu();
void drawOptions();
std::string m_selectedIsoPath;
std::string m_errorString;
bool m_isPal = false;
};
} // namespace dusk
+110 -7
View File
@@ -5,17 +5,110 @@
#endif
#include <aurora/main.h>
#include "dusk/main.h"
#include <algorithm>
#include <array>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <string>
#include <string_view>
#include <vector>
#if !defined(_WIN32)
#include <unistd.h>
#if defined(__APPLE__)
#include <mach-o/dyld.h>
#endif
#endif
int game_main(int argc, char* argv[]);
namespace {
bool RestartProcess(int argc, char* argv[]) {
#if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \
(defined(TARGET_OS_TV) && TARGET_OS_TV)
(void)argc;
(void)argv;
return false;
#elif _WIN32
std::wstring commandLine = GetCommandLineW();
STARTUPINFOW startupInfo{};
startupInfo.cb = sizeof(startupInfo);
PROCESS_INFORMATION processInfo{};
if (!CreateProcessW(nullptr, commandLine.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr,
&startupInfo, &processInfo))
{
fprintf(stderr, "Failed to restart Dusk: CreateProcessW error %lu\n", GetLastError());
return false;
}
CloseHandle(processInfo.hThread);
CloseHandle(processInfo.hProcess);
return true;
#else
std::filesystem::path executablePath;
#if defined(__APPLE__)
uint32_t pathSize = 0;
_NSGetExecutablePath(nullptr, &pathSize);
if (pathSize > 0) {
std::string path(pathSize, '\0');
if (_NSGetExecutablePath(path.data(), &pathSize) == 0) {
path.resize(std::strlen(path.c_str()));
std::error_code ec;
executablePath = std::filesystem::weakly_canonical(path, ec);
if (ec) {
executablePath = path;
}
}
}
#elif defined(__linux__)
std::array<char, 4096> path{};
const ssize_t len = readlink("/proc/self/exe", path.data(), path.size() - 1);
if (len > 0) {
path[static_cast<size_t>(len)] = '\0';
executablePath = path.data();
}
#endif
if (executablePath.empty() && argc > 0 && argv[0] != nullptr && argv[0][0] != '\0') {
std::error_code ec;
executablePath = std::filesystem::absolute(argv[0], ec);
if (ec) {
executablePath = argv[0];
}
}
if (executablePath.empty()) {
fprintf(stderr, "Failed to restart Dusk: unable to resolve executable path\n");
return false;
}
std::vector<std::string> args;
args.reserve(static_cast<size_t>(std::max(argc, 1)));
args.push_back(executablePath.string());
for (int i = 1; i < argc; ++i) {
args.emplace_back(argv[i] != nullptr ? argv[i] : "");
}
std::vector<char*> execArgv;
execArgv.reserve(args.size() + 1);
for (auto& arg : args) {
execArgv.push_back(arg.data());
}
execArgv.push_back(nullptr);
execv(executablePath.c_str(), execArgv.data());
fprintf(stderr, "Failed to restart Dusk: execv failed: %s\n", std::strerror(errno));
return false;
#endif
}
#if _WIN32
bool ShouldShowWindowsConsole(int argc, char* argv[]) {
if (const auto* env = std::getenv("DUSK_CONSOLE")) {
@@ -53,19 +146,25 @@ void WindowsSetupConsole(bool showConsole) {
SetConsoleOutputCP(CP_UTF8);
if (const HANDLE stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE);
stdoutHandle != INVALID_HANDLE_VALUE && stdoutHandle != nullptr) {
stdoutHandle != INVALID_HANDLE_VALUE && stdoutHandle != nullptr)
{
DWORD consoleMode = 0;
if (GetConsoleMode(stdoutHandle, &consoleMode)) {
SetConsoleMode(stdoutHandle,
consoleMode | ENABLE_PROCESSED_OUTPUT
| ENABLE_VIRTUAL_TERMINAL_PROCESSING);
consoleMode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
}
}
}
int DuskMain(int argc, char* argv[]) {
WindowsSetupConsole(ShouldShowWindowsConsole(argc, argv));
return game_main(argc, argv);
const int result = game_main(argc, argv);
if constexpr (dusk::SupportsProcessRestart) {
if (dusk::RestartRequested) {
return RestartProcess(argc, argv) ? 0 : result;
}
}
return result;
}
std::vector<std::string> WideArgsToUtf8(int argc, wchar_t** argv) {
@@ -81,8 +180,8 @@ std::vector<std::string> WideArgsToUtf8(int argc, wchar_t** argv) {
}
std::vector<char> utf8Buffer(static_cast<size_t>(requiredSize));
WideCharToMultiByte(CP_UTF8, 0, argv[i], -1, utf8Buffer.data(), requiredSize, nullptr,
nullptr);
WideCharToMultiByte(
CP_UTF8, 0, argv[i], -1, utf8Buffer.data(), requiredSize, nullptr, nullptr);
utf8Args.emplace_back(utf8Buffer.data());
}
@@ -109,7 +208,11 @@ int RunWindowsGuiEntryPoint() {
}
#else
int DuskMain(int argc, char* argv[]) {
return game_main(argc, argv);
const int result = game_main(argc, argv);
if (dusk::RestartRequested && RestartProcess(argc, argv)) {
return 0;
}
return result;
}
#endif
-10
View File
@@ -59,16 +59,6 @@ void Component::set_disabled(bool value) {
}
}
Rml::Element* Component::append(Rml::Element* parent, const Rml::String& tag) {
if (parent == nullptr) {
return nullptr;
}
auto* doc = parent->GetOwnerDocument();
if (doc == nullptr) {
return nullptr;
}
return parent->AppendChild(doc->CreateElement(tag));
}
void Component::listen(Rml::Element* element, Rml::EventId event,
ScopedEventListener::Callback callback, bool capture) {
if (element == nullptr) {
-1
View File
@@ -47,7 +47,6 @@ public:
Rml::Element* root() const { return mRoot; }
protected:
static Rml::Element* append(Rml::Element* parent, const Rml::String& tag);
void clear_children();
Rml::Element* mRoot = nullptr;
+75
View File
@@ -0,0 +1,75 @@
#include "modal.hpp"
namespace dusk::ui {
Modal::Modal(Props props)
: WindowSmall("modal", "modal-dialog"), mProps(std::move(props)) {
auto* title = append(mDialog, "div");
title->SetClass("preset-title", true);
title->SetInnerRML(mProps.title);
auto* body = append(mDialog, "div");
body->SetClass("preset-intro", true);
body->SetInnerRML(mProps.bodyRml);
auto* actions = append(mDialog, "div");
actions->SetClass("modal-actions", true);
for (auto& action : mProps.actions) {
auto btn = std::make_unique<Button>(actions, action.label);
btn->root()->SetClass("modal-btn", true);
btn->on_pressed([this, callback = std::move(action.onPressed)] {
if (callback) {
callback(*this);
}
});
mButtons.push_back(std::move(btn));
}
}
bool Modal::focus() {
if (!mButtons.empty()) {
return mButtons.front()->focus();
}
return false;
}
void Modal::dismiss() {
if (mProps.onDismiss) {
mProps.onDismiss(*this);
return;
}
pop();
}
bool Modal::handle_nav_command(Rml::Event& event, NavCommand cmd) {
if (cmd == NavCommand::Cancel || cmd == NavCommand::Menu) {
mDoAud_seStartMenu(Z2SE_SY_CURSOR_CANCEL);
dismiss();
return true;
}
int direction = 0;
if (cmd == NavCommand::Left) {
direction = -1;
} else if (cmd == NavCommand::Right) {
direction = 1;
} else {
return false;
}
auto* target = event.GetTargetElement();
for (int i = 0; i < static_cast<int>(mButtons.size()); ++i) {
if (mButtons[i]->contains(target)) {
const int next = i + direction;
if (next >= 0 && next < static_cast<int>(mButtons.size()) && mButtons[next]->focus()) {
mDoAud_seStartMenu(Z2SE_SY_NAME_CURSOR);
return true;
}
return false;
}
}
return false;
}
} // namespace dusk::ui
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include "button.hpp"
#include "window.hpp"
namespace dusk::ui {
class Modal;
struct ModalAction {
Rml::String label;
std::function<void(Modal&)> onPressed;
};
class Modal : public WindowSmall {
public:
struct Props {
Rml::String title;
Rml::String bodyRml;
std::vector<ModalAction> actions;
std::function<void(Modal&)> onDismiss;
bool doBlur = false;
};
explicit Modal(Props props);
bool focus() override;
protected:
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
private:
void dismiss();
Props mProps;
std::vector<std::unique_ptr<Button> > mButtons;
};
} // namespace dusk::ui
+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 {
+11 -6
View File
@@ -34,17 +34,22 @@ private:
class PrelaunchOptions;
struct PrelaunchState {
std::string selectedIsoPath;
std::string errorString;
std::string initialGraphicsBackend;
bool isPal = false;
bool initialized = false;
std::string selectedDiscPath;
bool selectedDiscIsValid = false;
bool selectedDiscIsPal = false;
std::string errorString;
bool initialDiscIsPal = false;
std::string initialDiscPath;
GameLanguage initialLanguage = GameLanguage::English;
std::string initialGraphicsBackend;
int initialCardFileType = 0;
};
PrelaunchState& prelaunch_state() noexcept;
void ensure_initialized() noexcept;
void refresh_path_state() noexcept;
bool is_selected_path_valid() noexcept;
void refresh_state() noexcept;
void open_iso_picker() noexcept;
bool is_restart_pending() noexcept;
} // namespace dusk::ui
+181 -24
View File
@@ -1,6 +1,7 @@
#include "prelaunch_options.hpp"
#include "dusk/config.hpp"
#include "dusk/main.h"
#include "dusk/settings.h"
#include "pane.hpp"
#include "prelaunch.hpp"
@@ -9,7 +10,11 @@ namespace dusk::ui {
namespace {
static constexpr std::array<const char*, 5> kLanguageNames = {
"English", "German", "French", "Spanish", "Italian",
"English",
"German",
"French",
"Spanish",
"Italian",
};
// TODO: Copied from ImGui prelaunch. Needs a refactor?
@@ -114,35 +119,68 @@ std::vector<AuroraBackend> available_backends() {
return backends;
}
class LanguageSelect final : public SelectButton {
class DiscSelect final : public SelectButton {
public:
explicit LanguageSelect(Rml::Element* parent) : SelectButton(parent, Props{.key = "Language"}) {}
explicit DiscSelect(Rml::Element* parent)
: SelectButton(parent, Props{.key = "Change Disc Image"}) {}
void update() override {
ensure_initialized();
refresh_path_state();
const bool validPath = is_selected_path_valid();
const bool ntscDiscLocked = validPath && !prelaunch_state().isPal;
if (ntscDiscLocked) {
if (getSettings().game.language.getValue() != GameLanguage::English) {
getSettings().game.language.setValue(GameLanguage::English);
config::Save();
const auto& path = prelaunch_state().selectedDiscPath;
std::string display;
if (path.empty()) {
display = "(none)";
} else {
display = std::filesystem::path(path).filename().string();
if (display.empty()) {
display = path;
}
}
const auto& initial = prelaunch_state().initialDiscPath;
if (!initial.empty() && path != initial) {
display += " (restart required)";
}
set_value_label(Rml::String(display));
SelectButton::update();
}
protected:
bool handle_nav_command(NavCommand cmd) override {
if (cmd != NavCommand::Confirm) {
return false;
}
open_iso_picker();
return true;
}
};
class LanguageSelect final : public SelectButton {
public:
explicit LanguageSelect(Rml::Element* parent)
: SelectButton(parent, Props{.key = "Language"}) {}
void update() override {
ensure_initialized();
// LanguageInit already forces English for USA discs, so we can just change the button's
// value instead of actually updating the config. This allows the old language setting to
// be remembered when switching back to a PAL disc.
const auto& state = prelaunch_state();
std::string value;
if (state.selectedDiscIsValid && !state.selectedDiscIsPal) {
value = kLanguageNames[0];
set_disabled(true);
} else {
const u8 idx = static_cast<u8>(getSettings().game.language.getValue());
value = kLanguageNames[idx];
set_disabled(false);
}
const auto lang = getSettings().game.language.getValue();
auto value = static_cast<u8>(lang);
if (value >= kLanguageNames.size()) {
getSettings().game.language.setValue(GameLanguage::English);
config::Save();
value = static_cast<u8>(getSettings().game.language.getValue());
if (getSettings().game.language.getValue() != state.initialLanguage) {
value += " (restart required)";
}
set_value_label(kLanguageNames[value]);
set_value_label(Rml::String(value));
SelectButton::update();
}
@@ -167,7 +205,8 @@ protected:
class BackendSelect final : public SelectButton {
public:
explicit BackendSelect(Rml::Element* parent) : SelectButton(parent, Props{.key = "Graphics Backend"}) {}
explicit BackendSelect(Rml::Element* parent)
: SelectButton(parent, Props{.key = "Graphics Backend"}) {}
void update() override {
AuroraBackend configuredBackend = BACKEND_AUTO;
@@ -219,7 +258,8 @@ protected:
const int dir = (cmd == NavCommand::Left) ? -1 : 1;
idx = ((idx + dir) % n + n) % n;
getSettings().backend.graphicsBackend.setValue(std::string(backend_id(backends[static_cast<size_t>(idx)])));
getSettings().backend.graphicsBackend.setValue(
std::string(backend_id(backends[static_cast<size_t>(idx)])));
config::Save();
return true;
}
@@ -227,11 +267,20 @@ protected:
class SaveTypeSelect final : public SelectButton {
public:
explicit SaveTypeSelect(Rml::Element* parent) : SelectButton(parent, Props{.key = "Save File Type"}) {}
explicit SaveTypeSelect(Rml::Element* parent)
: SelectButton(parent, Props{.key = "Save File Type"}) {}
void update() override {
const CARDFileType cft = static_cast<CARDFileType>(getSettings().backend.cardFileType.getValue());
set_value_label(cft == CARD_GCIFOLDER ? "GCI Folder" : "Card Image");
ensure_initialized();
const CARDFileType cft =
static_cast<CARDFileType>(getSettings().backend.cardFileType.getValue());
std::string label = cft == CARD_GCIFOLDER ? "GCI Folder" : "Card Image";
if (getSettings().backend.cardFileType.getValue() != prelaunch_state().initialCardFileType)
{
label += " (restart required)";
}
set_value_label(Rml::String(label));
SelectButton::update();
}
@@ -252,12 +301,120 @@ protected:
} // namespace
PrelaunchOptions::PrelaunchOptions() {
mSuppressNavFallback = true;
add_tab("Options", [this](Rml::Element* content) {
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
leftPane.add_child<DiscSelect>();
leftPane.add_child<LanguageSelect>();
leftPane.add_child<BackendSelect>();
leftPane.add_child<SaveTypeSelect>();
});
}
void PrelaunchOptions::push_modal(Modal::Props props) {
for (auto& action : props.actions) {
auto originalOnPressed = std::move(action.onPressed);
action.onPressed = [this, props, callback = std::move(originalOnPressed)](Modal& modal) {
if (props.doBlur) {
mRoot->SetClass("blurred", false);
}
if (callback) {
callback(modal);
}
};
}
auto originalOnDismiss = std::move(props.onDismiss);
props.onDismiss = [this, props, callback = std::move(originalOnDismiss)](Modal& modal) {
if (props.doBlur) {
mRoot->SetClass("blurred", false);
}
if (callback) {
callback(modal);
}
};
if (props.doBlur) {
mRoot->SetClass("blurred", true);
}
push_document(std::make_unique<Modal>(std::move(props)));
}
void PrelaunchOptions::hide(bool close) {
mRoot->SetClass("blurred", false);
Window::hide(close);
}
void PrelaunchOptions::update() {
Window::update();
auto& state = prelaunch_state();
if (state.errorString.empty() || top_document() != this) {
return;
}
auto dismissInvalidDisc = [](Modal& modal) {
prelaunch_state().errorString.clear();
modal.pop();
};
push_modal(Modal::Props{
.title = "Invalid disc image",
.bodyRml = state.errorString,
.actions =
{
ModalAction{
.label = "OK",
.onPressed = dismissInvalidDisc,
},
},
.onDismiss = dismissInvalidDisc,
.doBlur = true,
});
}
bool PrelaunchOptions::consume_close_request() {
if (!is_restart_pending()) {
return false;
}
if (top_document() != this) {
return true;
}
std::vector<ModalAction> actions;
if constexpr (dusk::SupportsProcessRestart) {
actions.push_back(ModalAction{
.label = "Restart later",
.onPressed =
[this](Modal& modal) {
modal.pop();
pop();
},
});
actions.push_back(ModalAction{
.label = "Restart now",
.onPressed = [](Modal&) { dusk::RequestRestart(); },
});
} else {
actions.push_back(ModalAction{
.label = "OK",
.onPressed =
[this](Modal& modal) {
modal.pop();
pop();
},
});
}
push_modal(Modal::Props{
.title = "Apply Options",
.bodyRml = dusk::SupportsProcessRestart ?
"A restart is required to apply selected options.<br/><br/>Restart now to "
"apply them immediately?" :
"A restart is required to apply selected options.<br/><br/>Close and reopen "
"Dusk to apply them.",
.actions = std::move(actions),
.onDismiss = [](Modal& modal) { modal.pop(); },
.doBlur = true,
});
return true;
}
} // namespace dusk::ui
+9
View File
@@ -1,5 +1,6 @@
#pragma once
#include "modal.hpp"
#include "window.hpp"
namespace dusk::ui {
@@ -7,6 +8,14 @@ namespace dusk::ui {
class PrelaunchOptions : public Window {
public:
PrelaunchOptions();
void update() override;
void hide(bool close) override;
protected:
bool consume_close_request() override;
private:
void push_modal(Modal::Props props);
};
} // namespace dusk::ui
+6 -51
View File
@@ -1,10 +1,8 @@
#include "preset.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "button.hpp"
#include "dusk/config.hpp"
#include "dusk/settings.h"
#include "m_Do/m_Do_audio.h"
#include "ui.hpp"
#include <dolphin/gx/GXAurora.h>
@@ -49,49 +47,20 @@ void applyPresetDusk() {
s.game.enableGyroAim.setValue(true);
}
Rml::Element* createElement(Rml::Element* parent, const Rml::String& tag) {
auto* doc = parent->GetOwnerDocument();
auto elem = doc->CreateElement(tag);
return parent->AppendChild(std::move(elem));
}
const Rml::String kDocumentSource = R"RML(
<rml>
<head>
<link type="text/rcss" href="res/rml/window.rcss" />
</head>
<body>
<window id="window" class="small preset">
<div id="preset-dialog" class="preset-dialog"></div>
</window>
</body>
</rml>
)RML";
} // namespace
PresetWindow::PresetWindow()
: Document(kDocumentSource), mRoot(mDocument->GetElementById("window")) {
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") &&
Document::visible()) {
Document::hide(mPendingClose);
}
});
auto* dialog = mDocument->GetElementById("preset-dialog");
auto* title = createElement(dialog, "div");
PresetWindow::PresetWindow() : WindowSmall("preset", "preset-dialog") {
auto* title = append(mDialog, "div");
title->SetClass("preset-title", true);
title->SetInnerRML("Welcome to Dusk!");
auto* intro = createElement(dialog, "div");
auto* intro = append(mDialog, "div");
intro->SetClass("preset-intro", true);
intro->SetInnerRML(
"Choose a preset to get started.<br/>"
"You can change any setting later from the Settings menu.");
auto* grid = createElement(dialog, "div");
auto* grid = append(mDialog, "div");
grid->SetClass("preset-grid", true);
struct PresetInfo {
@@ -112,7 +81,7 @@ PresetWindow::PresetWindow()
};
for (const auto& preset : kPresets) {
auto* col = createElement(grid, "div");
auto* col = append(grid, "div");
col->SetClass("preset-col", true);
auto btn = std::make_unique<Button>(col, Rml::String(preset.name));
@@ -129,26 +98,12 @@ PresetWindow::PresetWindow()
});
mButtons.push_back(std::move(btn));
auto* desc = createElement(col, "div");
auto* desc = append(col, "div");
desc->SetClass("preset-desc", true);
desc->SetInnerRML(preset.desc);
}
}
void PresetWindow::show() {
Document::show();
mRoot->SetAttribute("open", "");
}
void PresetWindow::hide(bool close) {
mRoot->RemoveAttribute("open");
mPendingClose = close;
}
bool PresetWindow::visible() const {
return mRoot->HasAttribute("open");
}
bool PresetWindow::focus() {
if (!mButtons.empty()) {
return mButtons.back()->focus();
+3 -6
View File
@@ -1,25 +1,22 @@
#pragma once
#include "component.hpp"
#include "document.hpp"
#include "window.hpp"
#include <memory>
#include <vector>
namespace dusk::ui {
class PresetWindow : public Document {
class PresetWindow : public WindowSmall {
public:
PresetWindow();
void show() override;
void hide(bool close) override;
bool visible() const override;
bool focus() override;
protected:
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
private:
Rml::Element* mRoot = nullptr;
std::vector<std::unique_ptr<Component>> mButtons;
};
+19
View File
@@ -10,6 +10,7 @@
#include "aurora/lib/window.hpp"
#include "input.hpp"
#include "prelaunch.hpp"
#include "window.hpp"
namespace dusk::ui {
@@ -73,6 +74,13 @@ bool any_document_visible() noexcept {
[](const auto& doc) { return doc && doc->visible(); });
}
bool is_prelaunch_open() noexcept {
return std::any_of(sDocuments.begin(), sDocuments.end(), [](const auto& doc) {
const auto* prelaunch = dynamic_cast<const Prelaunch*>(doc.get());
return prelaunch != nullptr && !prelaunch->pending_close() && !prelaunch->closed();
});
}
Document* top_document() noexcept {
for (auto& doc : std::views::reverse(sDocuments)) {
if (!doc->closed() && !doc->pending_close()) {
@@ -140,6 +148,17 @@ std::string escape(std::string_view str) noexcept {
return result;
}
Rml::Element* append(Rml::Element* parent, const Rml::String& tag) noexcept {
if (parent == nullptr) {
return nullptr;
}
auto* doc = parent->GetOwnerDocument();
if (doc == nullptr) {
return nullptr;
}
return parent->AppendChild(doc->CreateElement(tag));
}
NavCommand map_nav_event(const Rml::Event& event) noexcept {
const auto key = static_cast<Rml::Input::KeyIdentifier>(
event.GetParameter<int>("key_identifier", Rml::Input::KI_UNKNOWN));
+2 -2
View File
@@ -35,12 +35,12 @@ void update() noexcept;
Document& push_document(std::unique_ptr<Document> doc, bool show = true) noexcept;
void show_top_document() noexcept;
bool any_document_visible() noexcept;
bool is_prelaunch_open() noexcept;
Document* top_document() noexcept;
Popup& add_popup(std::unique_ptr<Popup> popup) noexcept;
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept;
std::string escape(std::string_view str) noexcept;
Rml::Element* append(Rml::Element* parent, const Rml::String& tag) noexcept;
NavCommand map_nav_event(const Rml::Event& event) noexcept;
Insets safe_area_insets(Rml::Context* context) noexcept;
+59 -5
View File
@@ -39,11 +39,24 @@ const Rml::String kDocumentSource = R"RML(
</rml>
)RML";
const Rml::String kDocumentSourceSmall = R"RML(
<rml>
<head>
<link type="text/rcss" href="res/rml/window.rcss" />
</head>
<body>
<window id="window" class="small">
<div id="dialog"/>
</window>
</body>
</rml>
)RML";
} // namespace
Window::Window() : Document(kDocumentSource), mRoot(mDocument->GetElementById("window")) {
mTabBar = std::make_unique<TabBar>(mRoot, TabBar::Props{
.onClose = [this] { pop(); },
.onClose = [this] { request_close(); },
.selectedTabIndex = 0,
.autoSelect = true,
});
@@ -68,7 +81,9 @@ Window::Window() : Document(kDocumentSource), mRoot(mDocument->GetElementById("w
// Hide document after transition completion
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") && Document::visible()) {
if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") &&
Document::visible())
{
Document::hide(mPendingClose);
}
});
@@ -139,6 +154,16 @@ bool Window::set_active_tab(int index) {
return mTabBar->set_active_tab(index);
}
void Window::request_close() {
if (!consume_close_request()) {
pop();
}
}
bool Window::consume_close_request() {
return false;
}
void Window::refresh_active_tab() {
mTabBar->refresh_active_tab();
}
@@ -182,13 +207,13 @@ bool Window::handle_nav_command(Rml::Event& event, NavCommand cmd) {
}
if (cmd == NavCommand::Cancel) {
mDoAud_seStartMenu(Z2SE_SY_CURSOR_CANCEL);
pop();
request_close();
return true;
}
if (mTabBar->handle_nav_command(event, cmd)) {
return true;
}
return Document::handle_nav_command(event, cmd);
return mSuppressNavFallback ? false : Document::handle_nav_command(event, cmd);
}
bool Window::handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept {
@@ -250,4 +275,33 @@ bool Window::handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept {
return false;
}
} // namespace dusk::ui
WindowSmall::WindowSmall(const Rml::String& windowClass, const Rml::String& dialogClass)
: Document(kDocumentSourceSmall), mRoot(mDocument->GetElementById("window")),
mDialog(mDocument->GetElementById("dialog")) {
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") &&
Document::visible())
{
Document::hide(mPendingClose);
}
});
mRoot->SetClass(windowClass, true);
mDialog->SetClass(dialogClass, true);
}
void WindowSmall::show() {
Document::show();
mRoot->SetAttribute("open", "");
}
void WindowSmall::hide(bool close) {
mRoot->RemoveAttribute("open");
mPendingClose = close;
}
bool WindowSmall::visible() const {
return mRoot->HasAttribute("open");
}
} // namespace dusk::ui
+17
View File
@@ -31,12 +31,15 @@ public:
bool set_active_tab(int index);
protected:
void request_close();
virtual bool consume_close_request();
void add_tab(const Rml::String& title, TabBuilder builder);
void refresh_active_tab();
void update_safe_area() noexcept;
void clear_content() noexcept;
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
bool handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept;
bool mSuppressNavFallback = false;
template <typename T, typename... Args>
requires std::is_base_of_v<Component, T> T& add_child(Args&&... args) {
@@ -53,4 +56,18 @@ protected:
Insets mBodyPadding;
};
// Shared shell for small-style windows such as Modal and PresetWindow
class WindowSmall : public Document {
public:
WindowSmall(const Rml::String& windowClass, const Rml::String& dialogClass);
void show() override;
void hide(bool close) override;
bool visible() const override;
protected:
Rml::Element* mRoot = nullptr;
Rml::Element* mDialog = nullptr;
};
} // namespace dusk::ui
+43 -21
View File
@@ -54,11 +54,11 @@
#include "dusk/gyro.h"
#include "dusk/imgui/ImGuiConsole.hpp"
#include "dusk/imgui/ImGuiEngine.hpp"
#include "dusk/iso_validate.hpp"
#include "dusk/logging.h"
#include "dusk/main.h"
#include "dusk/ui/popup.hpp"
#include "dusk/ui/prelaunch.hpp"
#include "dusk/ui/preset.hpp"
#include "dusk/ui/ui.hpp"
#include "version.h"
@@ -104,9 +104,15 @@ bool dusk::IsRunning = true;
bool dusk::IsShuttingDown = false;
bool dusk::IsGameLaunched = false;
bool dusk::IsFocusPaused = false;
bool dusk::RestartRequested = false;
std::filesystem::path dusk::ConfigPath;
#endif
void dusk::RequestRestart() noexcept {
RestartRequested = SupportsProcessRestart;
IsRunning = false;
}
s32 LOAD_COPYDATE(void*) {
char buffer[32];
memset(buffer, 0, sizeof(buffer));
@@ -484,7 +490,7 @@ u8 OSGetLanguage() {
}
static void LanguageInit() {
// Keep language at 0 (English) if not on a PAL disk.
// Keep language at 0 (English) if not on a PAL disc.
// Doubt this matters, but avoid funky shit.
if (!dusk::version::isRegionPal()) {
return;
@@ -591,34 +597,50 @@ int game_main(int argc, char* argv[]) {
dusk::audio::EnableHrtf = dusk::getSettings().audio.enableHrtf;
dusk::ui::initialize();
dusk::ui::push_document(std::make_unique<dusk::ui::Popup>(), false);
// 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) {
dusk::getSettings().backend.isoPath.setValue("");
}
std::string dvd_path;
bool dvd_opened = false;
if (parsed_arg_options.count("dvd")) {
dvd_path = parsed_arg_options["dvd"].as<std::string>();
DuskLog.info("Loading DVD image from command line: {}", dvd_path);
dvd_opened = aurora_dvd_open(dvd_path.c_str());
if (!dvd_opened) {
DuskLog.warn("Failed to open DVD image from command line: {}, opening prelaunch UI", dvd_path);
if (dusk::iso::validate(dvd_path.c_str()) == dusk::iso::ValidationError::Success) {
DuskLog.info("Loading DVD image from command line: {}", dvd_path);
dvd_opened = aurora_dvd_open(dvd_path.c_str());
if (!dvd_opened) {
DuskLog.warn("Failed to open DVD image from command line: {}, opening prelaunch UI", dvd_path);
} else {
dusk::getSettings().backend.isoPath.setValue(dvd_path);
dusk::config::Save();
dusk::IsGameLaunched = true;
}
} else {
dusk::getSettings().backend.isoPath.setValue(dvd_path);
dusk::config::Save();
dusk::IsGameLaunched = true;
DuskLog.warn("DVD image from command line failed verification: {}, opening prelaunch UI", dvd_path);
}
}
if (!dvd_opened) {
dusk::ui::push_document(std::make_unique<dusk::ui::Prelaunch>(), true);
if (!dusk::getSettings().backend.skipPreLaunchUI) {
dusk::ui::push_document(std::make_unique<dusk::ui::Prelaunch>(), true);
// pre game launch ui main loop
if (!launchUILoop()) {
dusk::ShutdownCrashReporting();
// pre game launch ui main loop
if (!launchUILoop()) {
dusk::ShutdownCrashReporting();
dusk::ShutdownFileLogging();
fflush(stdout);
fflush(stderr);
#ifdef DUSK_DISCORD
dusk::discord::shutdown();
dusk::discord::shutdown();
#endif
dusk::ui::shutdown();
aurora_shutdown();
return 0;
dusk::ui::shutdown();
aurora_shutdown();
return 0;
}
}
dvd_path = dusk::getSettings().backend.isoPath;
@@ -626,15 +648,15 @@ 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) {
DuskLog.fatal("DVD image failed verification: {}", dvd_path);
}
DuskLog.info("Loading DVD image: {}", dvd_path);
if (!aurora_dvd_open(dvd_path.c_str())) {
DuskLog.fatal("Failed to open DVD image: {}", dvd_path);
}
}
dusk::ui::push_document(std::make_unique<dusk::ui::Popup>(), false);
if (!dusk::getSettings().backend.wasPresetChosen) {
dusk::ui::push_document(std::make_unique<dusk::ui::PresetWindow>());
dusk::IsGameLaunched = true;
}
dusk::version::init();