mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-06-23 15:25:19 -04:00
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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user