diff --git a/README.md b/README.md
index 02b1ded875..5e7412e6aa 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -46,3 +46,10 @@ Pull requests are welcomed! Note that we do not accept contributions that are pr
# Credits
Special thanks to the [TP decompilation](https://github.com/zeldaret/tp) team, the GC/Wii decompilation community, the [Aurora](https://github.com/encounter/aurora) developers, the [TP speedrunning community](https://zsrtp.link), and all [contributors](https://github.com/TwilitRealm/dusk/graphs/contributors).
+
+
+
diff --git a/assets/aurora-powered.png b/assets/aurora-powered.png
new file mode 100644
index 0000000000..35ff017776
Binary files /dev/null and b/assets/aurora-powered.png differ
diff --git a/extern/aurora b/extern/aurora
index 518747aa86..f845a5a58c 160000
--- a/extern/aurora
+++ b/extern/aurora
@@ -1 +1 @@
-Subproject commit 518747aa86a50be62ecf92aeb309309b0d58a54a
+Subproject commit f845a5a58cfb576049e9ba8392c5c5ca61d550b6
diff --git a/files.cmake b/files.cmake
index e899318cb8..56ea825685 100644
--- a/files.cmake
+++ b/files.cmake
@@ -1461,6 +1461,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
@@ -1471,16 +1473,16 @@ 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/graphics_tuner.cpp
+ src/dusk/ui/graphics_tuner.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
@@ -1488,12 +1490,12 @@ set(DUSK_FILES
src/dusk/ui/overlay.hpp
src/dusk/ui/pane.cpp
src/dusk/ui/pane.hpp
- src/dusk/ui/popup.cpp
- src/dusk/ui/popup.hpp
+ src/dusk/ui/menu_bar.cpp
+ src/dusk/ui/menu_bar.hpp
src/dusk/ui/prelaunch.cpp
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
diff --git a/include/d/d_menu_fmap2D.h b/include/d/d_menu_fmap2D.h
index d0152f0229..7df78bb6d0 100644
--- a/include/d/d_menu_fmap2D.h
+++ b/include/d/d_menu_fmap2D.h
@@ -169,6 +169,12 @@ public:
void mapBlink() {}
+ #if PLATFORM_WII || TARGET_PC
+ f32 getMirrorPosX(f32 param_0, f32 param_1) {
+ return (field_0x11dc * 2.0f - (param_0 + param_1)) - param_1;
+ }
+ #endif
+
// Unknown name
struct RegionTexData {
/* 0x00 */ float mMinX;
diff --git a/include/d/d_menu_map_common.h b/include/d/d_menu_map_common.h
index 989aee7d8b..de50d775ca 100644
--- a/include/d/d_menu_map_common.h
+++ b/include/d/d_menu_map_common.h
@@ -66,6 +66,16 @@ public:
_c90 = param_2;
}
+#if PLATFORM_WII || TARGET_PC
+ f32 getMirrorCenterPosX(f32 param_0, f32 param_1) {
+ if (_c90) {
+ return (mCenterPosX * 2.0f - (param_0 + param_1)) - param_1;
+ }
+
+ return param_0;
+ }
+#endif
+
struct Stage_c {
// Incomplete class
diff --git a/include/dusk/achievements.h b/include/dusk/achievements.h
index ca4676ab2d..bf0021c9d7 100644
--- a/include/dusk/achievements.h
+++ b/include/dusk/achievements.h
@@ -50,8 +50,6 @@ public:
bool hasSignal(const char* key) const;
std::vector getAchievements() const;
- bool hasPendingUnlock() const { return !m_pendingUnlocks.empty(); }
- std::string consumePendingUnlock();
private:
struct Entry {
@@ -68,7 +66,6 @@ private:
std::unordered_set m_signals;
bool m_loaded = false;
bool m_dirty = false;
- std::queue m_pendingUnlocks;
};
} // namespace dusk
diff --git a/include/dusk/main.h b/include/dusk/main.h
index 065f507d36..d6b9c9927f 100644
--- a/include/dusk/main.h
+++ b/include/dusk/main.h
@@ -1,6 +1,10 @@
#ifndef DUSK_MAIN_H
#define DUSK_MAIN_H
+#if defined(__APPLE__)
+#include
+#endif
+
#include
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
diff --git a/include/dusk/settings.h b/include/dusk/settings.h
index 7f7aa8cd69..766fa506c1 100644
--- a/include/dusk/settings.h
+++ b/include/dusk/settings.h
@@ -67,7 +67,6 @@ struct UserSettings {
// QoL
ConfigVar enableQuickTransform;
ConfigVar hideTvSettingsScreen;
- ConfigVar skipWarningScreen;
ConfigVar biggerWallets;
ConfigVar noReturnRupees;
ConfigVar disableRupeeCutscenes;
diff --git a/res/rml/overlay.rcss b/res/rml/overlay.rcss
index e18bb436e5..416252a66b 100644
--- a/res/rml/overlay.rcss
+++ b/res/rml/overlay.rcss
@@ -8,140 +8,107 @@ body {
height: 100%;
margin: 0;
padding: 0;
- font-family: "Fira Sans Condensed";
- font-size: 24dp;
- color: #FFFFFF;
- display: flex;
- flex-direction: column;
- justify-content: flex-end;
- align-items: stretch;
-}
-
-.overlay-root {
- width: 100%;
- min-height: 45%;
- display: flex;
- flex-direction: column;
- justify-content: flex-end;
- align-items: stretch;
- decorator: vertical-gradient(#00000000 #151610F2);
- filter: opacity(0);
- transition: filter 0.2s linear-in-out;
-}
-
-.overlay-root[open] {
- filter: opacity(1);
-}
-
-.overlay {
- width: 100%;
- max-width: 1216dp;
- margin-left: auto;
- margin-right: auto;
- display: flex;
- flex-direction: column;
- gap: 24dp;
- padding: 48dp 64dp;
-}
-
-@media (max-height: 800dp) {
- .overlay-root {
- min-height: 38%;
- }
-
- .overlay {
- gap: 16dp;
- padding: 32dp 48dp;
- }
-}
-
-.header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 24dp;
-}
-
-.carousel-container {
- flex: 1 1 auto;
- display: flex;
- justify-content: flex-end;
- min-width: 0;
-}
-
-.description {
- font-size: 18dp;
- line-height: 22dp;
- color: rgba(255, 255, 255, 50%);
-}
-
-.divider {
- margin: 1dp 0;
- border-top: 1dp rgba(217, 217, 217, 50%);
-}
-
-.footer {
- display: flex;
- justify-content: space-between;
- align-items: center;
- gap: 24dp;
-}
-
-footer-button {
- display: block;
- width: 100%;
- max-width: 220dp;
- border: 0;
- padding: 0;
- background-color: transparent;
- font-family: "Fira Sans Condensed";
- font-weight: bold;
+ font-family: "Fira Sans";
+ font-weight: normal;
font-size: 20dp;
- line-height: 24dp;
- text-transform: uppercase;
- color: #FFFFFF;
- opacity: 1;
+ color: #E0DBC8;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: stretch;
+ z-index: 1;
+ pointer-events: none;
+}
+
+toast {
+ position: absolute;
+ top: 40dp;
+ right: 40dp;
+ display: flex;
+ flex-flow: column;
+ border-radius: 14dp;
+ overflow: hidden;
+ border: 1dp #92875B;
+ backdrop-filter: blur(5dp);
+ box-shadow: 0 0 15dp 3dp;
+ background-color: rgba(21, 22, 16, 80%);
+ filter: opacity(0);
+ transform: scale(0.9);
+ transform-origin: center;
+ transition: filter transform 0.2s cubic-in-out;
+ padding: 18dp 24dp;
+ gap: 8dp;
+}
+
+toast[open] {
+ filter: opacity(1);
+ transform: scale(1);
+}
+
+/*toast:hover {
cursor: pointer;
+ background-color: rgba(61, 59, 36, 80%);
}
-footer-button.return {
- text-align: left;
+toast:active {
+ background-color: rgba(45, 43, 26, 80%);
+}*/
+
+toast heading {
+ display: flex;
+ gap: 18dp;
+ align-items: center;
+ font-family: "Fira Sans Condensed";
+ font-size: 18dp;
+ font-weight: bold;
+ text-transform: uppercase;
+ color: #92875B;
}
-footer-button.reset {
- text-align: right;
-}
-
-.stepped-carousel {
+toast message {
display: flex;
align-items: center;
- justify-content: center;
- gap: 16dp;
- width: auto;
- min-width: 246dp;
- padding: 0;
- background-color: transparent;
- font-family: "Fira Sans Condensed";
- font-weight: bold;
+ justify-content: start;
+ gap: 8dp;
}
-.stepped-carousel-value {
- line-height: 29dp;
- min-width: 166dp;
- text-align: center;
- white-space: nowrap;
- opacity: 0.9;
+toast progress {
+ height: 4dp;
+ position: absolute;
+ left: 0;
+ bottom: 0;
+ width: 100%;
}
-.stepped-carousel-arrow {
- width: 24dp;
- height: 24dp;
- min-width: 24dp;
- padding: 0;
- border: 0;
- background-color: transparent;
- opacity: 1;
- cursor: pointer;
+toast progress fill {
+ background-color: rgba(194, 164, 45, 80%);
+}
+
+toast.achievement {
+ border: 1dp #C2A42D;
+}
+
+toast.achievement heading {
+ color: #C2A42D;
+}
+
+icon {
font-family: "Material Symbols Rounded";
font-weight: normal;
+ display: inline-block;
+ vertical-align: middle;
}
+
+icon.arrow-forward {
+ width: 24dp;
+ height: 24dp;
+ font-size: 24dp;
+ decorator: text("" center center);
+}
+
+icon.trophy {
+ width: 24dp;
+ height: 24dp;
+ font-size: 24dp;
+ decorator: text("" center center);
+}
\ No newline at end of file
diff --git a/res/rml/prelaunch.rcss b/res/rml/prelaunch.rcss
index 9556e74ef2..1c217e466c 100644
--- a/res/rml/prelaunch.rcss
+++ b/res/rml/prelaunch.rcss
@@ -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 {
diff --git a/res/rml/tuner.rcss b/res/rml/tuner.rcss
new file mode 100644
index 0000000000..86dd2043f6
--- /dev/null
+++ b/res/rml/tuner.rcss
@@ -0,0 +1,147 @@
+*, *:before, *:after {
+ box-sizing: border-box;
+}
+
+body {
+ overflow: visible;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ font-family: "Fira Sans Condensed";
+ font-size: 24dp;
+ color: #FFFFFF;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: stretch;
+}
+
+.tuner-root {
+ width: 100%;
+ min-height: 45%;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ align-items: stretch;
+ decorator: vertical-gradient(#00000000 #151610F2);
+ filter: opacity(0);
+ transition: filter 0.2s linear-in-out;
+}
+
+.tuner-root[open] {
+ filter: opacity(1);
+}
+
+.tuner {
+ width: 100%;
+ max-width: 1216dp;
+ margin-left: auto;
+ margin-right: auto;
+ display: flex;
+ flex-direction: column;
+ gap: 24dp;
+ padding: 48dp 64dp;
+}
+
+@media (max-height: 800dp) {
+ .tuner-root {
+ min-height: 38%;
+ }
+
+ .tuner {
+ gap: 16dp;
+ padding: 32dp 48dp;
+ }
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 24dp;
+}
+
+.carousel-container {
+ flex: 1 1 auto;
+ display: flex;
+ justify-content: flex-end;
+ min-width: 0;
+}
+
+.description {
+ font-size: 18dp;
+ line-height: 22dp;
+ color: rgba(255, 255, 255, 50%);
+}
+
+.divider {
+ margin: 1dp 0;
+ border-top: 1dp rgba(217, 217, 217, 50%);
+}
+
+.footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 24dp;
+}
+
+footer-button {
+ display: block;
+ width: 100%;
+ max-width: 220dp;
+ border: 0;
+ padding: 0;
+ background-color: transparent;
+ font-family: "Fira Sans Condensed";
+ font-weight: bold;
+ font-size: 20dp;
+ line-height: 24dp;
+ text-transform: uppercase;
+ color: #FFFFFF;
+ opacity: 1;
+ cursor: pointer;
+}
+
+footer-button.return {
+ text-align: left;
+}
+
+footer-button.reset {
+ text-align: right;
+}
+
+.stepped-carousel {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 16dp;
+ width: auto;
+ min-width: 246dp;
+ padding: 0;
+ background-color: transparent;
+ font-family: "Fira Sans Condensed";
+ font-weight: bold;
+}
+
+.stepped-carousel-value {
+ line-height: 29dp;
+ min-width: 166dp;
+ text-align: center;
+ white-space: nowrap;
+ opacity: 0.9;
+}
+
+.stepped-carousel-arrow {
+ width: 24dp;
+ height: 24dp;
+ min-width: 24dp;
+ padding: 0;
+ border: 0;
+ background-color: transparent;
+ opacity: 1;
+ cursor: pointer;
+ font-family: "Material Symbols Rounded";
+ font-weight: normal;
+}
diff --git a/res/rml/window.rcss b/res/rml/window.rcss
index db3558778a..c35670904e 100644
--- a/res/rml/window.rcss
+++ b/res/rml/window.rcss
@@ -43,6 +43,10 @@ window.preset {
min-width: 650dp;
}
+window.modal {
+ max-width: 816dp;
+}
+
window[open] {
filter: opacity(1);
transform: scale(1);
@@ -108,6 +112,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 +204,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 +415,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%;
+}
diff --git a/src/d/actor/d_a_alink_horse.inc b/src/d/actor/d_a_alink_horse.inc
index ac7ce73e50..f0c585f1b8 100644
--- a/src/d/actor/d_a_alink_horse.inc
+++ b/src/d/actor/d_a_alink_horse.inc
@@ -2721,7 +2721,7 @@ int daAlink_c::procHorseRun() {
}
if (mProcVar2.field_0x300c == 0) {
- set3DStatus(BUTTON_STATUS_HOLD_ON, 4);
+ set3DStatus(BUTTON_STATUS_HOLD_ON, IF_DUSK(dusk::getSettings().game.enableMirrorMode ? 1 :) 4);
}
} else {
if (mProcVar3.field_0x300e != 0) {
@@ -2731,7 +2731,7 @@ int daAlink_c::procHorseRun() {
}
if (mProcVar2.field_0x300c == 0) {
- set3DStatus(BUTTON_STATUS_HOLD_ON, 1);
+ set3DStatus(BUTTON_STATUS_HOLD_ON, IF_DUSK(dusk::getSettings().game.enableMirrorMode ? 4 :) 1);
}
}
diff --git a/src/d/d_demo.cpp b/src/d/d_demo.cpp
index d6425848d3..2f099f2c2a 100644
--- a/src/d/d_demo.cpp
+++ b/src/d/d_demo.cpp
@@ -11,6 +11,62 @@
#include "JSystem/JGadget/define.h"
#include
+#include "dusk/logging.h"
+
+#if TARGET_PC
+#include "dusk/ui/ui.hpp"
+
+namespace {
+static int sJaiSkip = -1;
+
+static JSUList* get_stream_list() {
+ return Z2GetSoundMgr()->getStreamMgr()->getStreamList();
+}
+
+static int get_stream_count(JSUList* list) {
+ int i = 0;
+ for (JSULink* 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* list = get_stream_list();
+ for (JSULink* 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* 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;
diff --git a/src/d/d_menu_fmap.cpp b/src/d/d_menu_fmap.cpp
index 149e03349d..d2b06e962c 100644
--- a/src/d/d_menu_fmap.cpp
+++ b/src/d/d_menu_fmap.cpp
@@ -919,9 +919,20 @@ void dMenu_Fmap_c::region_map_proc() {
}
mpDraw2DBack->regionMapMove(mpStick);
int stage_no, room_no;
+
+#if TARGET_PC
+ f32 arrow_pos_x = mpDraw2DBack->getArrowPos2DX();
+ if (dusk::getSettings().game.enableMirrorMode) {
+ arrow_pos_x = mpDraw2DBack->getMirrorPosX(arrow_pos_x, 0.0f);
+ }
+
+ f32 pos_x = arrow_pos_x - mDoGph_gInf_c::getMinXF() - mDoGph_gInf_c::getWidthF() * 0.5f;
+#else
f32 pos_x = mpDraw2DBack->getArrowPos2DX() - mDoGph_gInf_c::getMinXF()
- mDoGph_gInf_c::getWidthF() * 0.5f;
+#endif
f32 pos_y = mpDraw2DBack->getArrowPos2DY() - mDoGph_gInf_c::getHeightF() * 0.5f;
+
mpMenuFmapMap->getPointStagePathInnerNo(getNowFmapRegionData(), pos_x, pos_y,
mStayStageNo, &stage_no, &room_no);
if (mStageCursor != stage_no || mRoomCursor != room_no || mResetAreaName) {
@@ -2464,6 +2475,13 @@ void dMenu_Fmap_c::portalWarpMapMove(STControl* i_stick) {
f32 arrow_y = mpDraw2DBack->getArrowPos2DY();
u8 uVar6 = 0xff;
+#if TARGET_PC
+ if (dusk::getSettings().game.enableMirrorMode) {
+ arrow_x = mpDraw2DBack->getMirrorPosX(arrow_x, 0.0f);
+ }
+#endif
+
+
for (int i = 0; i < portal_dat->mCount; i++) {
if (portals[i].mRegionNo == mpDraw2DBack->getRegionCursor() + 1
&& checkDrawPortalIcon(portals[i].mStageNo, portals[i].mSwitchNo))
diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp
index ea9912998b..ffec184d4e 100644
--- a/src/d/d_menu_fmap2D.cpp
+++ b/src/d/d_menu_fmap2D.cpp
@@ -1043,6 +1043,12 @@ void dMenu_Fmap2DBack_c::allmap_move2(STControl* param_0) {
calcAllMapPos2D((mArrowPos3DX + control_xpos) - mStageTransX,
(mArrowPos3DZ + control_ypos) - mStageTransZ, &sp14, &sp10);
+#if TARGET_PC
+ if (dusk::getSettings().game.enableMirrorMode) {
+ sp14 = getMirrorPosX(sp14, 0.0f);
+ }
+#endif
+
mSelectRegion = 0xff;
for (int i = 7; i >= 0; i--) {
int val = field_0x1230[i];
@@ -1397,6 +1403,15 @@ void dMenu_Fmap2DBack_c::regionTextureDraw() {
if (uVar10 != uVar9) {
bool b = 0;
f32 v = mTransX + (dVar14 + (mRegionMinMapX[uVar10] + field_0xf0c[uVar10]));
+
+ #if TARGET_PC
+ if (dusk::getSettings().game.enableMirrorMode) {
+ b = true;
+ v = getMirrorPosX(mTransX + (dVar14 + (mRegionMinMapX[uVar10] + field_0xf0c[uVar10])),
+ mRegionMapSizeX[uVar10] * mZoom * 0.5f);
+ }
+ #endif
+
mpAreaTex[uVar10]->draw(
v, mTransZ + (dVar13 + (mRegionMinMapY[uVar10] + field_0xf2c[uVar10])),
mRegionMapSizeX[uVar10] * mZoom, mRegionMapSizeY[uVar10] * mZoom, b, false,
@@ -1404,6 +1419,15 @@ void dMenu_Fmap2DBack_c::regionTextureDraw() {
} else {
bool b = 0;
f32 v = mTransX + (dVar14 + (mRegionMinMapX[uVar9] + field_0xf0c[uVar9]));
+
+ #if TARGET_PC
+ if (dusk::getSettings().game.enableMirrorMode) {
+ b = true;
+ v = getMirrorPosX(mTransX + (dVar14 + (mRegionMinMapX[uVar9] + field_0xf0c[uVar9])),
+ mRegionMapSizeX[uVar9] * mZoom * 0.5f);
+ }
+ #endif
+
mpAreaTex[uVar9]->draw(
v, mTransZ + (dVar13 + (mRegionMinMapY[uVar9] + field_0xf2c[uVar9])),
mRegionMapSizeX[uVar9] * mZoom, mRegionMapSizeY[uVar9] * mZoom, b, false,
diff --git a/src/d/d_menu_map_common.cpp b/src/d/d_menu_map_common.cpp
index e7d0fef7ad..0017975c4e 100644
--- a/src/d/d_menu_map_common.cpp
+++ b/src/d/d_menu_map_common.cpp
@@ -343,6 +343,11 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4
}
f32 pos_x = icon_pos_x + i_posX;
+ #if TARGET_PC
+ if (dusk::getSettings().game.enableMirrorMode) {
+ pos_x = getMirrorCenterPosX(pos_x, 0.0f);
+ }
+ #endif
mpDrawCursor->setPos(pos_x, icon_pos_y + i_posY);
mpDrawCursor->setScale(mIconInfo[info_idx].scale * g_fmapHIO.mMapIconHIO.mPortalCursorScale);
mpDrawCursor->draw();
@@ -364,6 +369,12 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4
}
f32 pos_x = (icon_pos_x + i_posX);
+ #if TARGET_PC
+ if (dusk::getSettings().game.enableMirrorMode) {
+ pos_x = getMirrorCenterPosX(pos_x, 0.0f);
+ }
+ #endif
+
mpPortalIcon->setPos(pos_x, icon_pos_y + i_posY);
mpPortalIcon->setScale(mIconInfo[info_idx].scale * g_fmapHIO.mMapIconHIO.mPortalIconScale);
mpPortalIcon->draw();
@@ -399,6 +410,12 @@ void dMenuMapCommon_c::drawIcon(f32 i_posX, f32 i_posY, f32 param_3, f32 param_4
}
f32 pos_x = i_posX + (icon_pos_x - (icon_size_x / 2));
+ #if TARGET_PC
+ if (dusk::getSettings().game.enableMirrorMode) {
+ pos_x = getMirrorCenterPosX(i_posX + (icon_pos_x - (icon_size_x / 2)), icon_size_x / 2);
+ }
+ #endif
+
mPictures[mIconInfo[info_idx].icon_no]->draw(pos_x, (i_posY + (icon_pos_y - icon_size_y / 2)),
icon_size_x, icon_size_y, false, false, false);
diff --git a/src/d/d_msg_object.cpp b/src/d/d_msg_object.cpp
index 5e520753c9..3313534de3 100644
--- a/src/d/d_msg_object.cpp
+++ b/src/d/d_msg_object.cpp
@@ -431,6 +431,16 @@ static void dummyStrings() {
dMsgObject_HIO_c g_MsgObject_HIO_c;
int dMsgObject_c::_execute() {
+#if TARGET_PC
+ if (dusk::getSettings().game.enableMirrorMode) {
+ // enable wii message index override
+ g_MsgObject_HIO_c.mMessageDisplay = 1;
+ } else if (!dusk::getSettings().game.enableMirrorMode && g_MsgObject_HIO_c.mMessageDisplay == 1) {
+ g_MsgObject_HIO_c.mMessageDisplay = 0;
+ }
+#endif
+
+
field_0x4c7 = 0;
if (mpTalkHeap != NULL) {
field_0x148 = mDoExt_setCurrentHeap(mpTalkHeap);
diff --git a/src/d/d_s_logo.cpp b/src/d/d_s_logo.cpp
index cc5d0114a4..db0cfa1387 100644
--- a/src/d/d_s_logo.cpp
+++ b/src/d/d_s_logo.cpp
@@ -1120,26 +1120,12 @@ int dScnLogo_c::create() {
checkProgSelect();
if (field_0x20a != 0) {
mExecCommand = EXEC_PROG_IN;
- #if TARGET_PC
- mTimer = dusk::getSettings().game.skipWarningScreen ? 1 : 30;
- #else
mTimer = 30;
- #endif
field_0x218 = getProgressiveMode();
} else {
#if TARGET_PC
- if (dusk::getSettings().game.skipWarningScreen) {
- mTimer = 0; // Possibly unnecessary but just in case
- mExecCommand = EXEC_DVD_WAIT;
- } else {
- if (mDoRst::getWarningDispFlag()) {
- mTimer = 90;
- mExecCommand = EXEC_NINTENDO_IN;
- } else {
- mTimer = 120;
- mExecCommand = EXEC_WARNING_IN;
- }
- }
+ mTimer = 0; // Possibly unnecessary but just in case
+ mExecCommand = EXEC_DVD_WAIT;
#else
if (mDoRst::getWarningDispFlag()) {
mTimer = 90;
diff --git a/src/d/d_s_play.cpp b/src/d/d_s_play.cpp
index 68d788d1f4..3b38b6c8a5 100644
--- a/src/d/d_s_play.cpp
+++ b/src/d/d_s_play.cpp
@@ -40,9 +40,11 @@
#include "JSystem/JKernel/JKRAramArchive.h"
#if TARGET_PC
+#include "dusk/autosave.h"
#include "dusk/memory.h"
#include "dusk/randomizer/game/tools.h"
#include
+#include "dusk/ui/ui.hpp"
#endif
#if DEBUG
@@ -795,7 +797,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();
diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp
index 26a302678e..a090a40969 100644
--- a/src/dusk/achievements.cpp
+++ b/src/dusk/achievements.cpp
@@ -1,14 +1,15 @@
#include "dusk/achievements.h"
-#include "dusk/io.hpp"
-#include "dusk/main.h"
-#include "d/d_com_inf_game.h"
-#include "d/d_meter2_info.h"
#include "d/actor/d_a_alink.h"
#include "d/actor/d_a_npc4.h"
#include "d/actor/d_a_player.h"
+#include "d/d_com_inf_game.h"
#include "d/d_demo.h"
-#include "f_pc/f_pc_name.h"
+#include "d/d_meter2_info.h"
+#include "dusk/io.hpp"
+#include "dusk/main.h"
+#include "dusk/ui/ui.hpp"
#include "f_op/f_op_actor_mng.h"
+#include "f_pc/f_pc_name.h"
#include
#include
@@ -454,12 +455,6 @@ AchievementSystem& AchievementSystem::get() {
return instance;
}
-std::string AchievementSystem::consumePendingUnlock() {
- std::string msg = std::move(m_pendingUnlocks.front());
- m_pendingUnlocks.pop();
- return msg;
-}
-
std::vector AchievementSystem::getAchievements() const {
std::vector result;
result.reserve(m_entries.size());
@@ -559,7 +554,14 @@ void AchievementSystem::processEntry(Entry& e) {
if (nowUnlocked) {
e.achievement.progress = e.achievement.isCounter ? e.achievement.goal : 1;
e.achievement.unlocked = true;
- m_pendingUnlocks.push(e.achievement.name);
+ if (getSettings().game.enableAchievementNotifications) {
+ ui::push_toast({
+ .type = "achievement",
+ .title = "Achievement Unlocked!",
+ .content = e.achievement.name,
+ .duration = std::chrono::seconds(5),
+ });
+ }
m_dirty = true;
} else if (progressChanged) {
m_dirty = true;
diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp
index 68c11efb10..166461e19f 100644
--- a/src/dusk/imgui/ImGuiConsole.cpp
+++ b/src/dusk/imgui/ImGuiConsole.cpp
@@ -10,11 +10,9 @@
#include "fmt/format.h"
#include "ImGuiConsole.hpp"
-#include "dusk/ui/preset.hpp"
#include "dusk/ui/ui.hpp"
#include "JSystem/JUtility/JUTGamePad.h"
#include "SDL3/SDL_mouse.h"
-#include "dusk/achievements.h"
#include "dusk/audio/DuskAudioSystem.h"
#include "dusk/config.hpp"
#include "dusk/dusk.h"
@@ -254,15 +252,6 @@ namespace dusk {
UpdateSettings();
- AchievementSystem::get().tick();
- while (AchievementSystem::get().hasPendingUnlock()) {
- if (getSettings().game.enableAchievementNotifications) {
- m_menuTools.notifyAchievement(AchievementSystem::get().consumePendingUnlock());
- } else {
- AchievementSystem::get().consumePendingUnlock();
- }
- }
-
if (!fpcM_SearchByName(fpcNm_LOGO_SCENE_e) &&
(ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) &&
ImGui::IsKeyPressed(ImGuiKey_R))
@@ -274,10 +263,6 @@ namespace dusk {
ImGuiMenuGame::ToggleFullscreen();
}
- // if (!dusk::IsGameLaunched) {
- // m_preLaunchWindow.draw();
- // }
-
if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_F1)) {
m_isHidden = !m_isHidden;
}
@@ -345,7 +330,6 @@ namespace dusk {
}
m_menuRandomizer.windowRandoStats();
m_menuRandomizer.windowRandoGeneration();
- m_menuTools.showAchievementNotification();
DuskDebugPad(); // temporary, remove later
// Hide mouse cursor if the F1 menu is not open and the cursor is idle for 3 seconds.
diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp
index 73b52f04c1..e4992b2544 100644
--- a/src/dusk/imgui/ImGuiConsole.hpp
+++ b/src/dusk/imgui/ImGuiConsole.hpp
@@ -10,7 +10,6 @@
#include "ImGuiMenuGame.hpp"
#include "ImGuiMenuTools.hpp"
#include "ImGuiMenuRandomizer.hpp"
-#include "ImGuiPreLaunchWindow.hpp"
#include "imgui.h"
union SDL_Event;
@@ -47,7 +46,6 @@ private:
ImGuiMenuGame m_menuGame;
ImGuiMenuRandomizer m_menuRandomizer;
- ImGuiPreLaunchWindow m_preLaunchWindow;
// Keep always last
ImGuiMenuTools m_menuTools;
diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp
index 714add87c4..d3c062e0f0 100644
--- a/src/dusk/imgui/ImGuiMenuTools.cpp
+++ b/src/dusk/imgui/ImGuiMenuTools.cpp
@@ -267,66 +267,4 @@ namespace dusk {
ImGui::End();
ImGui::PopFont();
}
-
- void ImGuiMenuTools::notifyAchievement(std::string name) {
- if (m_notifyTimer <= 0.f) {
- m_notifyName = std::move(name);
- m_notifyTimer = NOTIFY_DURATION;
- } else {
- m_notifyQueue.push(std::move(name));
- }
- }
-
- void ImGuiMenuTools::showAchievementNotification() {
- if (!getSettings().game.enableAchievementNotifications.getValue()) {
- return;
- }
- if (m_notifyTimer <= 0.f) {
- if (m_notifyQueue.empty()) {
- return;
- }
- m_notifyName = std::move(m_notifyQueue.front());
- m_notifyQueue.pop();
- m_notifyTimer = NOTIFY_DURATION;
- }
-
- m_notifyTimer -= ImGui::GetIO().DeltaTime;
-
- const float alpha = std::min({
- m_notifyTimer / NOTIFY_FADE_TIME,
- (NOTIFY_DURATION - m_notifyTimer) / NOTIFY_FADE_TIME,
- 1.0f
- });
-
- const ImGuiViewport* viewport = ImGui::GetMainViewport();
- const float padding = 12.0f;
- ImGui::SetNextWindowPos(
- ImVec2(viewport->WorkPos.x + viewport->WorkSize.x - padding, viewport->WorkPos.y + padding),
- ImGuiCond_Always, ImVec2(1.0f, 0.0f)
- );
-
- ImGui::SetNextWindowBgAlpha(alpha * 0.92f);
- ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(0.08f, 0.06f, 0.01f, alpha * 0.92f));
- ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(1.0f, 0.8f, 0.1f, alpha));
- ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, alpha));
- ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 2.0f);
- ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(14.0f, 10.0f));
-
- constexpr ImGuiWindowFlags flags =
- ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize |
- ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing |
- ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoInputs;
-
- if (ImGui::Begin("##achievement_notify", nullptr, flags)) {
- ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.82f, 0.1f, alpha));
- ImGui::TextUnformatted("Achievement Unlocked!");
- ImGui::PopStyleColor();
- ImGui::Spacing();
- ImGui::TextUnformatted(m_notifyName.c_str());
- }
- ImGui::End();
-
- ImGui::PopStyleVar(2);
- ImGui::PopStyleColor(3);
- }
}
diff --git a/src/dusk/imgui/ImGuiMenuTools.hpp b/src/dusk/imgui/ImGuiMenuTools.hpp
index 7ae0e5bec6..f2c68494f7 100644
--- a/src/dusk/imgui/ImGuiMenuTools.hpp
+++ b/src/dusk/imgui/ImGuiMenuTools.hpp
@@ -27,8 +27,6 @@ namespace dusk {
void ShowAudioDebug();
void ShowSaveEditor();
void ShowStateShare();
- void notifyAchievement(std::string name);
- void showAchievementNotification();
private:
bool m_showDebugOverlay = false;
@@ -68,12 +66,6 @@ namespace dusk {
bool m_showStateShare = false;
ImGuiStateShare m_stateShare;
-
- std::string m_notifyName;
- float m_notifyTimer = 0.f;
- std::queue m_notifyQueue;
- static constexpr float NOTIFY_DURATION = 4.0f;
- static constexpr float NOTIFY_FADE_TIME = 0.5f;
};
}
diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp b/src/dusk/imgui/ImGuiPreLaunchWindow.cpp
deleted file mode 100644
index a0006f0ad3..0000000000
--- a/src/dusk/imgui/ImGuiPreLaunchWindow.cpp
+++ /dev/null
@@ -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
-#include
-
-#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 skLanguageNames = {
- "English", "German", "French", "Spanish", "Italian"
-};
-
-static constexpr std::array 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(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(selectedLanguage)])) {
- for (u8 i = 0; i < skLanguageNames.size(); ++i) {
- if (ImGui::Selectable(skLanguageNames[i])) {
- getSettings().game.language.setValue(static_cast(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
diff --git a/src/dusk/imgui/ImGuiPreLaunchWindow.hpp b/src/dusk/imgui/ImGuiPreLaunchWindow.hpp
deleted file mode 100644
index 6cb078a228..0000000000
--- a/src/dusk/imgui/ImGuiPreLaunchWindow.hpp
+++ /dev/null
@@ -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
diff --git a/src/dusk/main.cpp b/src/dusk/main.cpp
index 22cd5a9fc6..e1b2fd0b6e 100644
--- a/src/dusk/main.cpp
+++ b/src/dusk/main.cpp
@@ -5,17 +5,110 @@
#endif
#include
+#include "dusk/main.h"
+#include
+#include
+#include
#include
#include
+#include
+#include
#include
#include
#include
+#if !defined(_WIN32)
+#include
+#if defined(__APPLE__)
+#include
+#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 path{};
+ const ssize_t len = readlink("/proc/self/exe", path.data(), path.size() - 1);
+ if (len > 0) {
+ path[static_cast(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 args;
+ args.reserve(static_cast(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 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 WideArgsToUtf8(int argc, wchar_t** argv) {
@@ -81,8 +180,8 @@ std::vector WideArgsToUtf8(int argc, wchar_t** argv) {
}
std::vector utf8Buffer(static_cast(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
diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp
index 9114bfbee3..2b84c8bb47 100644
--- a/src/dusk/settings.cpp
+++ b/src/dusk/settings.cpp
@@ -27,7 +27,6 @@ UserSettings g_userSettings = {
// Quality of Life
.enableQuickTransform {"game.enableQuickTransform", false},
.hideTvSettingsScreen {"game.hideTvSettingsScreen", true},
- .skipWarningScreen {"game.skipWarningScreen", true},
.biggerWallets {"game.biggerWallets", false},
.noReturnRupees {"game.noReturnRupees", false},
.disableRupeeCutscenes {"game.disableRupeeCutscenes", false},
@@ -143,7 +142,6 @@ void registerSettings() {
Register(g_userSettings.game.language);
Register(g_userSettings.game.enableQuickTransform);
Register(g_userSettings.game.hideTvSettingsScreen);
- Register(g_userSettings.game.skipWarningScreen);
Register(g_userSettings.game.biggerWallets);
Register(g_userSettings.game.noReturnRupees);
Register(g_userSettings.game.disableRupeeCutscenes);
diff --git a/src/dusk/ui/achievements.cpp b/src/dusk/ui/achievements.cpp
index 93993bb380..67e36eb4f4 100644
--- a/src/dusk/ui/achievements.cpp
+++ b/src/dusk/ui/achievements.cpp
@@ -65,7 +65,7 @@ public:
btn.on_nav_command([this, key = std::string(a.key)](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm) {
if (mConfirming) {
- mDoAud_seStartMenu(Z2SE_SY_CURSOR_OK);
+ mDoAud_seStartMenu(kSoundClick);
AchievementSystem::get().clearOne(key.c_str());
resetConfirm();
} else {
@@ -158,7 +158,7 @@ AchievementsWindow::AchievementsWindow() {
clearAllBtn.on_nav_command([clearAllPtr, confirmingAll](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm) {
if (*confirmingAll) {
- mDoAud_seStartMenu(Z2SE_SY_CURSOR_OK);
+ mDoAud_seStartMenu(kSoundClick);
AchievementSystem::get().clearAll();
*confirmingAll = false;
clearAllPtr->set_text("Clear All Achievements");
diff --git a/src/dusk/ui/bool_button.cpp b/src/dusk/ui/bool_button.cpp
index ca4d3f96d8..2ed088542a 100644
--- a/src/dusk/ui/bool_button.cpp
+++ b/src/dusk/ui/bool_button.cpp
@@ -36,7 +36,7 @@ bool BoolButton::handle_nav_command(NavCommand cmd) {
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || cmd == NavCommand::Right) {
const bool newValue = !mGetValue();
mSetValue(newValue);
- mDoAud_seStartMenu(newValue ? Z2SE_SY_CURSOR_OK : Z2SE_SY_CURSOR_CANCEL);
+ mDoAud_seStartMenu(newValue ? kSoundItemEnable : kSoundItemDisable);
return true;
}
return false;
diff --git a/src/dusk/ui/button.cpp b/src/dusk/ui/button.cpp
index ac27b03d5a..fb11571af4 100644
--- a/src/dusk/ui/button.cpp
+++ b/src/dusk/ui/button.cpp
@@ -37,7 +37,6 @@ Button& Button::on_pressed(ButtonCallback callback) {
// TODO: convert this to a FluentComponent method?
on_nav_command([callback = std::move(callback)](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm) {
- mDoAud_seStartMenu(Z2SE_SY_CURSOR_OK);
callback();
return true;
}
diff --git a/src/dusk/ui/component.cpp b/src/dusk/ui/component.cpp
index 466420eb1e..748df848be 100644
--- a/src/dusk/ui/component.cpp
+++ b/src/dusk/ui/component.cpp
@@ -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) {
diff --git a/src/dusk/ui/component.hpp b/src/dusk/ui/component.hpp
index 0c49ce3254..e0a602e7fd 100644
--- a/src/dusk/ui/component.hpp
+++ b/src/dusk/ui/component.hpp
@@ -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;
diff --git a/src/dusk/ui/controller_config.cpp b/src/dusk/ui/controller_config.cpp
index 9a3238725e..038248d7d0 100644
--- a/src/dusk/ui/controller_config.cpp
+++ b/src/dusk/ui/controller_config.cpp
@@ -322,6 +322,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
.isSelected = [port] { return PADGetIndexForPort(port) < 0; },
})
.on_pressed([this, port] {
+ mDoAud_seStartMenu(kSoundItemChange);
cancel_pending_binding();
PADClearPort(port);
PADSerializeMappings();
@@ -335,6 +336,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
[port, i] { return PADGetIndexForPort(port) == static_cast(i); },
})
.on_pressed([this, port, i] {
+ mDoAud_seStartMenu(kSoundItemChange);
cancel_pending_binding();
PADSetPortForIndex(i, port);
PADSerializeMappings();
diff --git a/src/dusk/ui/document.cpp b/src/dusk/ui/document.cpp
index 9d581d21ca..a7bcc3f9ed 100644
--- a/src/dusk/ui/document.cpp
+++ b/src/dusk/ui/document.cpp
@@ -44,15 +44,8 @@ Document::Document(const Rml::String& source) : mDocument(load_document(source))
if (cmd == NavCommand::None) {
return;
}
- auto* prevFocused = mDocument->GetFocusLeafNode();
if (handle_nav_command(event, cmd)) {
event.StopPropagation();
- return;
- }
- if ((cmd == NavCommand::Up || cmd == NavCommand::Down) &&
- mDocument->GetFocusLeafNode() != prevFocused)
- {
- mDoAud_seStartMenu(Z2SE_SY_NAME_CURSOR);
}
});
}
@@ -113,7 +106,7 @@ bool Document::visible() const {
bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) {
if (cmd == NavCommand::Menu) {
- mDoAud_seStartMenu(visible() ? Z2SE_SY_MENU_OUT : Z2SE_SY_MENU_IN);
+ mDoAud_seStartMenu(visible() ? kSoundMenuClose : kSoundMenuOpen);
toggle();
return true;
}
diff --git a/src/dusk/ui/editor.cpp b/src/dusk/ui/editor.cpp
index 1ba44f0a27..fd42409ed2 100644
--- a/src/dusk/ui/editor.cpp
+++ b/src/dusk/ui/editor.cpp
@@ -185,7 +185,10 @@ void populate_stage_picker(Pane& pane, std::function getStageFile
return getStageFile() == stageFile;
},
})
- .on_pressed([setStageFile, stageFile = map.mapFile] { setStageFile(stageFile); });
+ .on_pressed([setStageFile, stageFile = map.mapFile] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ setStageFile(stageFile);
+ });
}
}
}
@@ -741,11 +744,13 @@ void populate_toggle_group(Pane& pane, const std::vector& entries)
pane.clear();
pane.add_section("Actions");
pane.add_button("Select All").on_pressed([entries] {
+ mDoAud_seStartMenu(kSoundItemChange);
for (const auto& entry : entries) {
entry.setSelected(true);
}
});
pane.add_button("Select None").on_pressed([entries] {
+ mDoAud_seStartMenu(kSoundItemChange);
for (const auto& entry : entries) {
entry.setSelected(false);
}
@@ -758,6 +763,7 @@ void populate_toggle_group(Pane& pane, const std::vector& entries)
.isSelected = entry.isSelected,
})
.on_pressed([isSelected = entry.isSelected, setSelected = entry.setSelected] {
+ mDoAud_seStartMenu(kSoundItemChange);
setSelected(!isSelected());
});
}
@@ -878,7 +884,10 @@ void populate_item_slot_picker(Pane& pane, int slot) {
pane.clear();
pane.add_section("Actions");
pane.add_button(fmt::format("Default ({})", get_item_name(get_slot_default(slot))))
- .on_pressed([slot] { dComIfGs_setItem(slot, get_slot_default(slot)); });
+ .on_pressed([slot] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ dComIfGs_setItem(slot, get_slot_default(slot));
+ });
pane.add_section("Items");
pane.add_button(
@@ -886,7 +895,10 @@ void populate_item_slot_picker(Pane& pane, int slot) {
.text = "None",
.isSelected = [slot] { return get_player_item()->mItems[slot] == dItemNo_NONE_e; },
})
- .on_pressed([slot] { dComIfGs_setItem(slot, dItemNo_NONE_e); });
+ .on_pressed([slot] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ dComIfGs_setItem(slot, dItemNo_NONE_e);
+ });
for (const auto& [itemId, item] : itemMap) {
if (item.m_type != ITEMTYPE_EQUIP_e) {
continue;
@@ -896,15 +908,24 @@ void populate_item_slot_picker(Pane& pane, int slot) {
.text = item.m_name,
.isSelected = [slot, itemId] { return get_player_item()->mItems[slot] == itemId; },
})
- .on_pressed([slot, itemId] { dComIfGs_setItem(slot, static_cast(itemId)); });
+ .on_pressed([slot, itemId] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ dComIfGs_setItem(slot, static_cast(itemId));
+ });
}
}
void populate_item_flag_picker(Pane& pane) {
pane.clear();
pane.add_section("Actions");
- pane.add_button("Select All").on_pressed([] { set_all_item_first_bits(true); });
- pane.add_button("Clear None").on_pressed([] { set_all_item_first_bits(false); });
+ pane.add_button("Select All").on_pressed([] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ set_all_item_first_bits(true);
+ });
+ pane.add_button("Clear None").on_pressed([] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ set_all_item_first_bits(false);
+ });
pane.add_section("Items");
for (const auto& [itemId, item] : itemMap) {
@@ -916,7 +937,10 @@ void populate_item_flag_picker(Pane& pane) {
.text = item.m_name,
.isSelected = [itemId] { return dComIfGs_isItemFirstBit(static_cast(itemId)); },
})
- .on_pressed([itemId] { toggle_item_first_bit(static_cast(itemId)); });
+ .on_pressed([itemId] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ toggle_item_first_bit(static_cast(itemId));
+ });
}
}
@@ -927,13 +951,19 @@ void populate_select_item_picker(Pane& pane, u8& selectItemData) {
.text = "None",
.isSelected = [&selectItemData] { return selectItemData == dItemNo_NONE_e; },
})
- .on_pressed([&selectItemData] { selectItemData = dItemNo_NONE_e; });
+ .on_pressed([&selectItemData] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ selectItemData = dItemNo_NONE_e;
+ });
for (int i = 0; i < 24; i++) {
pane.add_button({
.text = item_label_for_slot(i),
.isSelected = [i, &selectItemData] { return selectItemData == i; },
})
- .on_pressed([i, &selectItemData] { selectItemData = i; });
+ .on_pressed([i, &selectItemData] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ selectItemData = i;
+ });
}
}
@@ -946,6 +976,7 @@ void populate_select_clothes_picker(Pane& pane) {
.isSelected = [id] { return get_player_status()->mSelectEquip[0] == id; },
})
.on_pressed([id] {
+ mDoAud_seStartMenu(kSoundItemChange);
dMeter2Info_setCloth(id, false);
daPy_getPlayerActorClass()->setClothesChange(0);
});
@@ -964,7 +995,10 @@ void populate_select_equip_picker(Pane& pane, u8& equip, const std::arraygetWalletSize() == i; },
})
- .on_pressed([i] { get_player_status()->setWalletSize(i); });
+ .on_pressed([i] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ get_player_status()->setWalletSize(i);
+ });
}
}
@@ -1002,7 +1039,10 @@ void populate_form_picker(Pane& pane) {
.text = formNames[i],
.isSelected = [i] { return get_player_status()->getTransformStatus() == i; },
})
- .on_pressed([i] { get_player_status()->setTransformStatus(i); });
+ .on_pressed([i] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ get_player_status()->setTransformStatus(i);
+ });
}
}
@@ -1013,7 +1053,10 @@ void add_toggle_button(Pane& pane, ToggleEntry entry) {
.text = entry.text,
.isSelected = isSelected,
})
- .on_pressed([isSelected, setSelected] { setSelected(!isSelected()); });
+ .on_pressed([isSelected, setSelected] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ setSelected(!isSelected());
+ });
}
template
@@ -1126,8 +1169,14 @@ void populate_collect_clothes_picker(Pane& pane) {
void populate_poe_souls_picker(Pane& pane) {
pane.clear();
pane.add_section("Actions");
- pane.add_button("All 60").on_pressed([] { dComIfGs_setPohSpiritNum(60); });
- pane.add_button("Clear").on_pressed([] { dComIfGs_setPohSpiritNum(0); });
+ pane.add_button("All 60").on_pressed([] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ dComIfGs_setPohSpiritNum(60);
+ });
+ pane.add_button("Clear").on_pressed([] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ dComIfGs_setPohSpiritNum(0);
+ });
pane.add_section("Value");
pane.add_child(NumberButton::Props{
@@ -1143,10 +1192,12 @@ void populate_max_life_picker(Pane& pane) {
pane.clear();
pane.add_section("Actions");
pane.add_button("3 Hearts").on_pressed([] {
+ mDoAud_seStartMenu(kSoundItemChange);
dComIfGs_setMaxLife(15);
dComIfGs_setLife(12);
});
pane.add_button("20 Hearts").on_pressed([] {
+ mDoAud_seStartMenu(kSoundItemChange);
dComIfGs_setMaxLife(100);
dComIfGs_setLife(80);
});
@@ -1257,7 +1308,10 @@ void populate_target_type_picker(Pane& pane) {
.text = targetTypeNames[type],
.isSelected = [type] { return get_player_config()->getAttentionType() == type; },
})
- .on_pressed([type] { get_player_config()->setAttentionType(type); });
+ .on_pressed([type] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ get_player_config()->setAttentionType(type);
+ });
}
}
@@ -1269,7 +1323,10 @@ void populate_sound_mode_picker(Pane& pane) {
.text = soundModeNames[mode],
.isSelected = [mode] { return get_player_config()->getSound() == mode; },
})
- .on_pressed([mode] { get_player_config()->setSound(mode); });
+ .on_pressed([mode] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ get_player_config()->setSound(mode);
+ });
}
}
@@ -1594,6 +1651,7 @@ EditorWindow::EditorWindow() {
leftPane.add_section("Item Wheel");
leftPane.register_control(leftPane.add_button("Default All").on_pressed([&rightPane] {
+ mDoAud_seStartMenu(kSoundItemChange);
for (int slot = 0; slot < 24; ++slot) {
dComIfGs_setItem(slot, get_slot_default(slot));
}
@@ -1601,6 +1659,7 @@ EditorWindow::EditorWindow() {
}),
rightPane, {});
leftPane.register_control(leftPane.add_button("Clear All").on_pressed([&rightPane] {
+ mDoAud_seStartMenu(kSoundItemChange);
for (int slot = 0; slot < 24; ++slot) {
dComIfGs_setItem(slot, dItemNo_NONE_e);
}
diff --git a/src/dusk/ui/graphics_tuner.cpp b/src/dusk/ui/graphics_tuner.cpp
new file mode 100644
index 0000000000..390d4f9971
--- /dev/null
+++ b/src/dusk/ui/graphics_tuner.cpp
@@ -0,0 +1,282 @@
+#include "graphics_tuner.hpp"
+
+#include "Z2AudioLib/Z2SeMgr.h"
+#include "m_Do/m_Do_audio.h"
+
+#include
+#include
+#include
+
+#include "dusk/config.hpp"
+#include "dusk/settings.h"
+
+#include
+#include
+
+namespace dusk::ui {
+namespace {
+
+const Rml::String kDocumentSource = R"RML(
+
+
+
+
+
+
+
+
+)RML";
+
+int get_value(GraphicsOption option) {
+ switch (option) {
+ case GraphicsOption::InternalResolution:
+ return getSettings().game.internalResolutionScale.getValue();
+ case GraphicsOption::ShadowResolution:
+ return getSettings().game.shadowResolutionMultiplier.getValue();
+ case GraphicsOption::BloomMode:
+ return static_cast(getSettings().game.bloomMode.getValue());
+ case GraphicsOption::BloomMultiplier:
+ return std::clamp(
+ static_cast(getSettings().game.bloomMultiplier.getValue() * 100.0f + 0.5f), 0,
+ 100);
+ }
+ return 0;
+}
+
+void set_value(GraphicsOption option, int value) {
+ switch (option) {
+ case GraphicsOption::InternalResolution:
+ getSettings().game.internalResolutionScale.setValue(value);
+ VISetFrameBufferScale(static_cast(value));
+ break;
+ case GraphicsOption::ShadowResolution:
+ getSettings().game.shadowResolutionMultiplier.setValue(value);
+ break;
+ case GraphicsOption::BloomMode:
+ getSettings().game.bloomMode.setValue(static_cast(std::clamp(
+ value, static_cast(BloomMode::Off), static_cast(BloomMode::Dusk))));
+ break;
+ case GraphicsOption::BloomMultiplier:
+ getSettings().game.bloomMultiplier.setValue(std::clamp(value, 0, 100) / 100.0f);
+ break;
+ }
+ config::Save();
+}
+
+Rml::Element* create_stepped_carousel_root(Rml::Element* parent) {
+ auto* doc = parent->GetOwnerDocument();
+ auto root = doc->CreateElement("div");
+ root->SetClass("stepped-carousel", true);
+ root->SetAttribute("tabindex", "0");
+ return parent->AppendChild(std::move(root));
+}
+
+Rml::Element* create_stepped_carousel_arrow(
+ Rml::Element* parent, const Rml::String& className, const Rml::String& label) {
+ auto* doc = parent->GetOwnerDocument();
+ auto button = doc->CreateElement("button");
+ button->SetClass("stepped-carousel-arrow", true);
+ button->SetClass(className, true);
+ button->SetInnerRML(label);
+ return parent->AppendChild(std::move(button));
+}
+
+} // namespace
+
+SteppedCarousel::SteppedCarousel(Rml::Element* parent, Props props)
+ : Component(create_stepped_carousel_root(parent)), mProps(std::move(props)) {
+ Rml::Element* prevElem = create_stepped_carousel_arrow(mRoot, "prev", "");
+ mValueElem = append(mRoot, "div");
+ mValueElem->SetClass("stepped-carousel-value", true);
+ Rml::Element* nextElem = create_stepped_carousel_arrow(mRoot, "next", "");
+
+ listen(prevElem, Rml::EventId::Click,
+ [this](Rml::Event&) { handle_nav_command(NavCommand::Left); });
+ listen(nextElem, Rml::EventId::Click,
+ [this](Rml::Event&) { handle_nav_command(NavCommand::Right); });
+ listen(mRoot, Rml::EventId::Keydown, [this](Rml::Event& event) {
+ const auto cmd = map_nav_event(event);
+ if (cmd != NavCommand::None && handle_nav_command(cmd)) {
+ event.StopPropagation();
+ }
+ });
+}
+
+bool SteppedCarousel::focus() {
+ return Component::focus();
+}
+
+void SteppedCarousel::update() {
+ if (mValueElem == nullptr) {
+ return;
+ }
+ const int value = std::clamp(mProps.getValue ? mProps.getValue() : 0, mProps.min, mProps.max);
+ if (mProps.formatValue) {
+ mValueElem->SetInnerRML(mProps.formatValue(value));
+ } else {
+ mValueElem->SetInnerRML(std::to_string(value));
+ }
+}
+
+bool SteppedCarousel::handle_nav_command(NavCommand cmd) {
+ if (cmd == NavCommand::Left) {
+ const int value = mProps.getValue ? mProps.getValue() : 0;
+ apply(std::clamp(value - mProps.step, mProps.min, mProps.max));
+ return true;
+ }
+ if (cmd == NavCommand::Right) {
+ const int value = mProps.getValue ? mProps.getValue() : 0;
+ apply(std::clamp(value + mProps.step, mProps.min, mProps.max));
+ return true;
+ }
+ return false;
+}
+
+void SteppedCarousel::apply(int value) {
+ const int nextValue = std::clamp(value, mProps.min, mProps.max);
+ const int currentValue =
+ std::clamp(mProps.getValue ? mProps.getValue() : 0, mProps.min, mProps.max);
+ if (nextValue == currentValue) {
+ return;
+ }
+ mDoAud_seStartMenu(kSoundItemChange);
+ if (mProps.onChange) {
+ mProps.onChange(nextValue);
+ }
+}
+
+Rml::String format_graphics_setting_value(GraphicsOption option, int value) {
+ switch (option) {
+ case GraphicsOption::InternalResolution:
+ if (value <= 0) {
+ return "Auto";
+ } else {
+ u32 width = 0;
+ u32 height = 0;
+ AuroraGetRenderSize(&width, &height);
+ return fmt::format("{}× ({}×{})", value, width, height);
+ }
+ case GraphicsOption::ShadowResolution:
+ return fmt::format("{}×", value);
+ case GraphicsOption::BloomMode:
+ switch (static_cast(value)) {
+ case BloomMode::Off:
+ return "Off";
+ case BloomMode::Classic:
+ return "Classic";
+ case BloomMode::Dusk:
+ return "Dusk";
+ }
+ break;
+ case GraphicsOption::BloomMultiplier:
+ return fmt::format("{}%", value);
+ }
+ return "";
+}
+
+GraphicsTuner::GraphicsTuner(GraphicsTunerProps props)
+ : Document(kDocumentSource), mOption(props.option), mValueMin(props.valueMin),
+ mValueMax(props.valueMax), mDefaultValue(props.defaultValue) {
+ if (mDocument == nullptr) {
+ return;
+ }
+
+ if (auto* title = mDocument->GetElementById("title")) {
+ title->SetInnerRML(escape(props.title));
+ }
+ if (auto* description = mDocument->GetElementById("description")) {
+ description->SetInnerRML(escape(props.helpText));
+ }
+ if (auto* carouselParent = mDocument->GetElementById("carousel-container")) {
+ add_component(carouselParent,
+ SteppedCarousel::Props{
+ .min = mValueMin,
+ .max = mValueMax,
+ .step = 1,
+ .getValue = [this] { return get_value(mOption); },
+ .onChange = [this](int value) { set_value(mOption, value); },
+ .formatValue =
+ [this](int value) { return format_graphics_setting_value(mOption, value); },
+ });
+ }
+
+ if (auto* footer = mDocument->GetElementById("footer")) {
+ auto& returnButton = add_component(footer, "\xE2\x86\x90 Return", "footer-button")
+ .on_pressed([this] { pop(); });
+ returnButton.root()->SetClass("return", true);
+ auto& resetButton =
+ add_component(footer, "Reset to default", "footer-button").on_pressed([this] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ reset_default();
+ });
+ resetButton.root()->SetClass("reset", true);
+ }
+
+ // Hide document after transition completion
+ mRoot = mDocument->GetElementById("root");
+ listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
+ if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") &&
+ Document::visible())
+ {
+ Document::hide(mPendingClose);
+ }
+ });
+}
+
+void GraphicsTuner::show() {
+ Document::show();
+ mRoot->SetAttribute("open", "");
+ mDoAud_seStartMenu(kSoundWindowOpen);
+}
+
+void GraphicsTuner::hide(bool close) {
+ mRoot->RemoveAttribute("open");
+ if (close) {
+ mPendingClose = true;
+ mDoAud_seStartMenu(kSoundWindowClose);
+ }
+}
+
+void GraphicsTuner::update() {
+ for (const auto& component : mComponents) {
+ component->update();
+ }
+ Document::update();
+}
+
+bool GraphicsTuner::focus() {
+ for (const auto& component : mComponents) {
+ if (component->focus()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool GraphicsTuner::visible() const {
+ return mRoot->HasAttribute("open");
+}
+
+bool GraphicsTuner::handle_nav_command(Rml::Event& event, NavCommand cmd) {
+ if (cmd == NavCommand::Cancel) {
+ pop();
+ return true;
+ }
+ return Document::handle_nav_command(event, cmd);
+}
+
+void GraphicsTuner::reset_default() {
+ set_value(mOption, mDefaultValue);
+}
+
+} // namespace dusk::ui
\ No newline at end of file
diff --git a/src/dusk/ui/graphics_tuner.hpp b/src/dusk/ui/graphics_tuner.hpp
new file mode 100644
index 0000000000..ecbd3ccac4
--- /dev/null
+++ b/src/dusk/ui/graphics_tuner.hpp
@@ -0,0 +1,90 @@
+#pragma once
+
+#include "button.hpp"
+#include "component.hpp"
+#include "document.hpp"
+#include "ui.hpp"
+
+#include
+#include
+#include
+#include
+#include
+
+namespace dusk::ui {
+
+class SteppedCarousel : public Component {
+public:
+ struct Props {
+ int min = 0;
+ int max = 0;
+ int step = 1;
+ std::function getValue;
+ std::function onChange;
+ std::function formatValue;
+ };
+
+ SteppedCarousel(Rml::Element* parent, Props props);
+
+ bool focus() override;
+ void update() override;
+
+private:
+ bool handle_nav_command(NavCommand cmd);
+ void apply(int value);
+
+ Props mProps;
+ Rml::Element* mValueElem = nullptr;
+};
+
+enum class GraphicsOption {
+ InternalResolution,
+ ShadowResolution,
+ BloomMode,
+ BloomMultiplier,
+};
+
+Rml::String format_graphics_setting_value(GraphicsOption option, int value);
+
+struct GraphicsTunerProps {
+ GraphicsOption option;
+ Rml::String title;
+ Rml::String helpText;
+ int valueMin = 0;
+ int valueMax = 0;
+ int defaultValue = 0;
+};
+
+class GraphicsTuner : public Document {
+public:
+ explicit GraphicsTuner(GraphicsTunerProps props);
+
+ void show() override;
+ void hide(bool close) override;
+ void update() override;
+ bool focus() override;
+ bool visible() const override;
+
+protected:
+ bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
+
+private:
+ template
+ requires std::is_base_of_v T& add_component(Args&&... args) {
+ auto child = std::make_unique(std::forward(args)...);
+ T& ref = *child;
+ mComponents.emplace_back(std::move(child));
+ return ref;
+ }
+
+ void reset_default();
+
+ GraphicsOption mOption;
+ int mValueMin = 0;
+ int mValueMax = 0;
+ int mDefaultValue = 0;
+ std::vector > mComponents;
+ Rml::Element* mRoot;
+};
+
+} // namespace dusk::ui
diff --git a/src/dusk/ui/popup.cpp b/src/dusk/ui/menu_bar.cpp
similarity index 82%
rename from src/dusk/ui/popup.cpp
rename to src/dusk/ui/menu_bar.cpp
index 41e026eef4..d6f6274798 100644
--- a/src/dusk/ui/popup.cpp
+++ b/src/dusk/ui/menu_bar.cpp
@@ -1,4 +1,4 @@
-#include "popup.hpp"
+#include "menu_bar.hpp"
#include
@@ -30,16 +30,20 @@ const Rml::String kDocumentSource = R"RML(
-
+
)RML";
}
-Popup::Popup() : Document(kDocumentSource), mRoot(mDocument->GetElementById("popup")) {
+MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById("popup")) {
mTabBar = std::make_unique(mRoot, TabBar::Props{
- .onClose = [this] { hide(false); },
+ .onClose =
+ [this] {
+ mDoAud_seStartMenu(kSoundMenuClose);
+ hide(false);
+ },
.autoSelect = false,
});
mTabBar->add_tab("Settings", [this] { push(std::make_unique()); });
@@ -68,7 +72,7 @@ Popup::Popup() : Document(kDocumentSource), mRoot(mDocument->GetElementById("pop
});
}
-void Popup::show() {
+void MenuBar::show() {
Document::show();
mRoot->SetAttribute("open", "");
mTabBar->set_active_tab(-1);
@@ -77,7 +81,7 @@ void Popup::show() {
}
}
-void Popup::hide(bool close) {
+void MenuBar::hide(bool close) {
mFocusedTabIndex = mTabBar->focused_tab_index();
mRoot->RemoveAttribute("open");
if (close) {
@@ -85,12 +89,12 @@ void Popup::hide(bool close) {
}
}
-void Popup::update() {
+void MenuBar::update() {
update_safe_area();
Document::update();
}
-void Popup::update_safe_area() noexcept {
+void MenuBar::update_safe_area() noexcept {
if (mDocument == nullptr || mTabBar == nullptr) {
return;
}
@@ -128,23 +132,23 @@ void Popup::update_safe_area() noexcept {
}
}
-bool Popup::visible() const {
+bool MenuBar::visible() const {
return mRoot->HasAttribute("open");
}
-bool Popup::handle_nav_command(Rml::Event& event, NavCommand cmd) {
+bool MenuBar::handle_nav_command(Rml::Event& event, NavCommand cmd) {
if (!getSettings().backend.wasPresetChosen) {
return true;
}
if (cmd == NavCommand::Cancel && visible()) {
- mDoAud_seStartMenu(Z2SE_SY_MENU_OUT);
+ mDoAud_seStartMenu(kSoundMenuClose);
hide(false);
return true;
}
return Document::handle_nav_command(event, cmd);
}
-bool Popup::focus() {
+bool MenuBar::focus() {
return mTabBar->focus();
}
diff --git a/src/dusk/ui/popup.hpp b/src/dusk/ui/menu_bar.hpp
similarity index 82%
rename from src/dusk/ui/popup.hpp
rename to src/dusk/ui/menu_bar.hpp
index 9865d0b752..3c8b716ee2 100644
--- a/src/dusk/ui/popup.hpp
+++ b/src/dusk/ui/menu_bar.hpp
@@ -8,12 +8,12 @@
namespace dusk::ui {
-class Popup : public Document {
+class MenuBar : public Document {
public:
- Popup();
+ MenuBar();
- Popup(const Popup&) = delete;
- Popup& operator=(const Popup&) = delete;
+ MenuBar(const MenuBar&) = delete;
+ MenuBar& operator=(const MenuBar&) = delete;
void show() override;
void hide(bool close) override;
diff --git a/src/dusk/ui/modal.cpp b/src/dusk/ui/modal.cpp
new file mode 100644
index 0000000000..cd46f68528
--- /dev/null
+++ b/src/dusk/ui/modal.cpp
@@ -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(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(kSoundWindowClose);
+ 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(mButtons.size()); ++i) {
+ if (mButtons[i]->contains(target)) {
+ const int next = i + direction;
+ if (next >= 0 && next < static_cast(mButtons.size()) && mButtons[next]->focus()) {
+ mDoAud_seStartMenu(kSoundItemFocus);
+ return true;
+ }
+ return false;
+ }
+ }
+ return false;
+}
+
+} // namespace dusk::ui
diff --git a/src/dusk/ui/modal.hpp b/src/dusk/ui/modal.hpp
new file mode 100644
index 0000000000..585966b98d
--- /dev/null
+++ b/src/dusk/ui/modal.hpp
@@ -0,0 +1,37 @@
+#pragma once
+
+#include "button.hpp"
+#include "window.hpp"
+
+namespace dusk::ui {
+class Modal;
+
+struct ModalAction {
+ Rml::String label;
+ std::function onPressed;
+};
+
+class Modal : public WindowSmall {
+public:
+ struct Props {
+ Rml::String title;
+ Rml::String bodyRml;
+ std::vector actions;
+ std::function onDismiss;
+ };
+
+ 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 > mButtons;
+};
+
+} // namespace dusk::ui
diff --git a/src/dusk/ui/number_button.cpp b/src/dusk/ui/number_button.cpp
index f891b4275b..ab5095cf3f 100644
--- a/src/dusk/ui/number_button.cpp
+++ b/src/dusk/ui/number_button.cpp
@@ -59,7 +59,7 @@ bool NumberButton::handle_nav_command(NavCommand cmd) {
mGetValue() + (cmd == NavCommand::Right ? mStep : -mStep), mMin, mMax);
if (newValue != mGetValue()) {
mSetValue(newValue);
- mDoAud_seStartMenu(Z2SE_SY_NAME_CURSOR);
+ mDoAud_seStartMenu(kSoundItemChange);
}
return true;
}
diff --git a/src/dusk/ui/overlay.cpp b/src/dusk/ui/overlay.cpp
index 837f8a5c5a..a222f1e8ff 100644
--- a/src/dusk/ui/overlay.cpp
+++ b/src/dusk/ui/overlay.cpp
@@ -1,20 +1,15 @@
#include "overlay.hpp"
-#include "Z2AudioLib/Z2SeMgr.h"
-#include "m_Do/m_Do_audio.h"
-
-#include
-#include
-#include
-
-#include "dusk/config.hpp"
-#include "dusk/settings.h"
+#include "aurora/lib/logging.hpp"
+#include "magic_enum.hpp"
#include
-#include
+
+#include "dusk/achievements.h"
namespace dusk::ui {
namespace {
+aurora::Module Log{"dusk::ui::overlay"};
const Rml::String kDocumentSource = R"RML(
@@ -22,259 +17,101 @@ const Rml::String kDocumentSource = R"RML(
-
)RML";
-int get_value(GraphicsOption option) {
- switch (option) {
- case GraphicsOption::InternalResolution:
- return getSettings().game.internalResolutionScale.getValue();
- case GraphicsOption::ShadowResolution:
- return getSettings().game.shadowResolutionMultiplier.getValue();
- case GraphicsOption::BloomMode:
- return static_cast(getSettings().game.bloomMode.getValue());
- case GraphicsOption::BloomMultiplier:
- return std::clamp(
- static_cast(getSettings().game.bloomMultiplier.getValue() * 100.0f + 0.5f), 0,
- 100);
+Rml::Element* create_toast(Rml::Element* parent, const Toast& toast) {
+ auto* elem = append(parent, "toast");
+ if (!toast.type.empty()) {
+ elem->SetClass(toast.type, true);
}
- return 0;
-}
-
-void set_value(GraphicsOption option, int value) {
- switch (option) {
- case GraphicsOption::InternalResolution:
- getSettings().game.internalResolutionScale.setValue(value);
- VISetFrameBufferScale(static_cast(value));
- break;
- case GraphicsOption::ShadowResolution:
- getSettings().game.shadowResolutionMultiplier.setValue(value);
- break;
- case GraphicsOption::BloomMode:
- getSettings().game.bloomMode.setValue(static_cast(std::clamp(
- value, static_cast(BloomMode::Off), static_cast(BloomMode::Dusk))));
- break;
- case GraphicsOption::BloomMultiplier:
- getSettings().game.bloomMultiplier.setValue(std::clamp(value, 0, 100) / 100.0f);
- break;
+ {
+ auto* heading = append(elem, "heading");
+ auto* span = append(heading, "span");
+ span->SetInnerRML(toast.title);
+ if (toast.type == "achievement") {
+ auto* icon = append(heading, "icon");
+ icon->SetClass("trophy", true);
+ mDoAud_seStartMenu(kSoundAchievementUnlock);
+ }
}
- config::Save();
-}
-
-Rml::Element* create_stepped_carousel_root(Rml::Element* parent) {
- auto* doc = parent->GetOwnerDocument();
- auto root = doc->CreateElement("div");
- root->SetClass("stepped-carousel", true);
- root->SetAttribute("tabindex", "0");
- return parent->AppendChild(std::move(root));
-}
-
-Rml::Element* create_stepped_carousel_arrow(
- Rml::Element* parent, const Rml::String& className, const Rml::String& label) {
- auto* doc = parent->GetOwnerDocument();
- auto button = doc->CreateElement("button");
- button->SetClass("stepped-carousel-arrow", true);
- button->SetClass(className, true);
- button->SetInnerRML(label);
- return parent->AppendChild(std::move(button));
+ {
+ auto* message = append(elem, "message");
+ auto* span = append(message, "span");
+ span->SetInnerRML(toast.content);
+ }
+ {
+ auto* progress = append(elem, "progress");
+ progress->SetAttribute("value", 1.f);
+ }
+ return elem;
}
} // namespace
-SteppedCarousel::SteppedCarousel(Rml::Element* parent, Props props)
- : Component(create_stepped_carousel_root(parent)), mProps(std::move(props)) {
- Rml::Element* prevElem = create_stepped_carousel_arrow(mRoot, "prev", "");
- mValueElem = append(mRoot, "div");
- mValueElem->SetClass("stepped-carousel-value", true);
- Rml::Element* nextElem = create_stepped_carousel_arrow(mRoot, "next", "");
-
- listen(prevElem, Rml::EventId::Click,
- [this](Rml::Event&) { handle_nav_command(NavCommand::Left); });
- listen(nextElem, Rml::EventId::Click,
- [this](Rml::Event&) { handle_nav_command(NavCommand::Right); });
- listen(mRoot, Rml::EventId::Keydown, [this](Rml::Event& event) {
- const auto cmd = map_nav_event(event);
- if (cmd != NavCommand::None && handle_nav_command(cmd)) {
- event.StopPropagation();
- }
- });
-}
-
-bool SteppedCarousel::focus() {
- return Component::focus();
-}
-
-void SteppedCarousel::update() {
- if (mValueElem == nullptr) {
- return;
- }
- const int value = std::clamp(mProps.getValue ? mProps.getValue() : 0, mProps.min, mProps.max);
- if (mProps.formatValue) {
- mValueElem->SetInnerRML(mProps.formatValue(value));
- } else {
- mValueElem->SetInnerRML(std::to_string(value));
- }
-}
-
-bool SteppedCarousel::handle_nav_command(NavCommand cmd) {
- if (cmd == NavCommand::Left) {
- const int value = mProps.getValue ? mProps.getValue() : 0;
- apply(std::clamp(value - mProps.step, mProps.min, mProps.max));
- return true;
- }
- if (cmd == NavCommand::Right) {
- const int value = mProps.getValue ? mProps.getValue() : 0;
- apply(std::clamp(value + mProps.step, mProps.min, mProps.max));
- return true;
- }
- return false;
-}
-
-void SteppedCarousel::apply(int value) {
- const int nextValue = std::clamp(value, mProps.min, mProps.max);
- const int currentValue =
- std::clamp(mProps.getValue ? mProps.getValue() : 0, mProps.min, mProps.max);
- if (nextValue == currentValue) {
- return;
- }
- mDoAud_seStartMenu(Z2SE_SY_NAME_CURSOR);
- if (mProps.onChange) {
- mProps.onChange(nextValue);
- }
-}
-
-Rml::String format_graphics_setting_value(GraphicsOption option, int value) {
- switch (option) {
- case GraphicsOption::InternalResolution:
- if (value <= 0) {
- return "Auto";
- } else {
- u32 width = 0;
- u32 height = 0;
- AuroraGetRenderSize(&width, &height);
- return fmt::format("{}× ({}×{})", value, width, height);
- }
- case GraphicsOption::ShadowResolution:
- return fmt::format("{}×", value);
- case GraphicsOption::BloomMode:
- switch (static_cast(value)) {
- case BloomMode::Off:
- return "Off";
- case BloomMode::Classic:
- return "Classic";
- case BloomMode::Dusk:
- return "Dusk";
- }
- break;
- case GraphicsOption::BloomMultiplier:
- return fmt::format("{}%", value);
- }
- return "";
-}
-
-Overlay::Overlay(OverlayProps props)
- : Document(kDocumentSource), mOption(props.option), mValueMin(props.valueMin),
- mValueMax(props.valueMax), mDefaultValue(props.defaultValue) {
- if (mDocument == nullptr) {
- return;
- }
-
- if (auto* title = mDocument->GetElementById("title")) {
- title->SetInnerRML(escape(props.title));
- }
- if (auto* description = mDocument->GetElementById("description")) {
- description->SetInnerRML(escape(props.helpText));
- }
- if (auto* carouselParent = mDocument->GetElementById("carousel-container")) {
- add_component(carouselParent,
- SteppedCarousel::Props{
- .min = mValueMin,
- .max = mValueMax,
- .step = 1,
- .getValue = [this] { return get_value(mOption); },
- .onChange = [this](int value) { set_value(mOption, value); },
- .formatValue =
- [this](int value) { return format_graphics_setting_value(mOption, value); },
- });
- }
-
- if (auto* footer = mDocument->GetElementById("footer")) {
- auto& returnButton = add_component(footer, "\xE2\x86\x90 Return", "footer-button")
- .on_pressed([this] { pop(); });
- returnButton.root()->SetClass("return", true);
- auto& resetButton =
- add_component(footer, "Reset to default", "footer-button").on_pressed([this] {
- reset_default();
- });
- resetButton.root()->SetClass("reset", true);
- }
-
- // Hide document after transition completion
- mRoot = mDocument->GetElementById("root");
- listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
- if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") &&
- Document::visible())
- {
- Document::hide(mPendingClose);
+Overlay::Overlay() : Document(kDocumentSource) {
+ listen(mDocument, Rml::EventId::Focus, [](Rml::Event&) { Log.warn("Overlay received focus"); });
+ listen(mDocument, Rml::EventId::Transitionend, [this](Rml::Event& event) {
+ if (event.GetTargetElement() == mCurrentToast) {
+ if (get_toasts().empty() ||
+ clock::now() >= mCurrentToastStartTime + get_toasts().front().duration)
+ {
+ mCurrentToast->SetPseudoClass("done", true);
+ }
}
});
}
void Overlay::show() {
- mDoAud_seStartMenu(Z2SE_SY_CURSOR_OK);
- Document::show();
- mRoot->SetAttribute("open", "");
-}
-
-void Overlay::hide(bool close) {
- mRoot->RemoveAttribute("open");
- if (close) {
- mPendingClose = true;
+ if (mDocument != nullptr) {
+ mDocument->Show(Rml::ModalFlag::None, Rml::FocusFlag::None, Rml::ScrollFlag::None);
}
}
void Overlay::update() {
- for (const auto& component : mComponents) {
- component->update();
- }
Document::update();
-}
-bool Overlay::focus() {
- for (const auto& component : mComponents) {
- if (component->focus()) {
- return true;
+ auto& toasts = get_toasts();
+ if (mCurrentToast == nullptr) {
+ if (!toasts.empty()) {
+ const auto& toast = toasts.front();
+ mCurrentToast = create_toast(mDocument, toast);
+ mCurrentToastStartTime = clock::now();
+ }
+ } else if (!toasts.empty()) {
+ const auto& toast = toasts.front();
+ const float duration = std::chrono::duration(toast.duration).count();
+ const float elapsed =
+ std::chrono::duration(clock::now() - mCurrentToastStartTime).count();
+ const float ratio = duration > 0.0f ? std::clamp(elapsed / duration, 0.0f, 1.0f) : 1.0f;
+ const auto remaining = 1.f - ratio;
+ Rml::ElementList list;
+ mDocument->GetElementsByTagName(list, "progress");
+ for (auto* elem : list) {
+ elem->SetAttribute("value", remaining);
+ }
+ if (remaining == 0.f) {
+ if (mCurrentToast->IsPseudoClassSet("done") ||
+ // Fallback for large gaps in time where we never actually opened it
+ !mCurrentToast->IsPseudoClassSet("opened"))
+ {
+ mCurrentToast->GetParentNode()->RemoveChild(mCurrentToast);
+ mCurrentToast = nullptr;
+ toasts.pop_front();
+ } else {
+ mCurrentToast->RemoveAttribute("open");
+ }
+ } else {
+ mCurrentToast->SetAttribute("open", "");
+ mCurrentToast->SetPseudoClass("opened", true);
}
}
- return false;
-}
-
-bool Overlay::visible() const {
- return mRoot->HasAttribute("open");
}
bool Overlay::handle_nav_command(Rml::Event& event, NavCommand cmd) {
- if (cmd == NavCommand::Cancel) {
- pop();
- return true;
- }
- return Document::handle_nav_command(event, cmd);
-}
-
-void Overlay::reset_default() {
- set_value(mOption, mDefaultValue);
+ Log.warn("Overlay received nav command: {}", magic_enum::enum_name(cmd));
+ return false;
}
} // namespace dusk::ui
diff --git a/src/dusk/ui/overlay.hpp b/src/dusk/ui/overlay.hpp
index c00cedfa9b..71e2e72470 100644
--- a/src/dusk/ui/overlay.hpp
+++ b/src/dusk/ui/overlay.hpp
@@ -1,90 +1,23 @@
#pragma once
-#include "button.hpp"
-#include "component.hpp"
#include "document.hpp"
-#include "ui.hpp"
-#include
-#include
-#include
-#include
-#include
+#include
namespace dusk::ui {
-class SteppedCarousel : public Component {
-public:
- struct Props {
- int min = 0;
- int max = 0;
- int step = 1;
- std::function getValue;
- std::function onChange;
- std::function formatValue;
- };
-
- SteppedCarousel(Rml::Element* parent, Props props);
-
- bool focus() override;
- void update() override;
-
-private:
- bool handle_nav_command(NavCommand cmd);
- void apply(int value);
-
- Props mProps;
- Rml::Element* mValueElem = nullptr;
-};
-
-enum class GraphicsOption {
- InternalResolution,
- ShadowResolution,
- BloomMode,
- BloomMultiplier,
-};
-
-Rml::String format_graphics_setting_value(GraphicsOption option, int value);
-
-struct OverlayProps {
- GraphicsOption option;
- Rml::String title;
- Rml::String helpText;
- int valueMin = 0;
- int valueMax = 0;
- int defaultValue = 0;
-};
-
class Overlay : public Document {
public:
- explicit Overlay(OverlayProps props);
+ Overlay();
void show() override;
- void hide(bool close) override;
void update() override;
- bool focus() override;
- bool visible() const override;
protected:
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
-private:
- template
- requires std::is_base_of_v T& add_component(Args&&... args) {
- auto child = std::make_unique(std::forward(args)...);
- T& ref = *child;
- mComponents.emplace_back(std::move(child));
- return ref;
- }
-
- void reset_default();
-
- GraphicsOption mOption;
- int mValueMin = 0;
- int mValueMax = 0;
- int mDefaultValue = 0;
- std::vector > mComponents;
- Rml::Element* mRoot;
+ Rml::Element* mCurrentToast = nullptr;
+ clock::time_point mCurrentToastStartTime;
};
} // namespace dusk::ui
diff --git a/src/dusk/ui/pane.cpp b/src/dusk/ui/pane.cpp
index 59989ad155..053f59b37e 100644
--- a/src/dusk/ui/pane.cpp
+++ b/src/dusk/ui/pane.cpp
@@ -58,7 +58,7 @@ Pane::Pane(Rml::Element* parent, Type type) : FluentComponent(createRoot(parent)
int i = focusedChild + direction;
while (i >= 0 && i < mChildren.size()) {
if (mChildren[i]->focus()) {
- mDoAud_seStartMenu(Z2SE_SY_NAME_CURSOR);
+ mDoAud_seStartMenu(kSoundItemFocus);
event.StopPropagation();
break;
}
diff --git a/src/dusk/ui/prelaunch.cpp b/src/dusk/ui/prelaunch.cpp
index 1e8c464b95..f4cf78c9d3 100644
--- a/src/dusk/ui/prelaunch.cpp
+++ b/src/dusk/ui/prelaunch.cpp
@@ -4,15 +4,18 @@
#include "dusk/file_select.hpp"
#include "dusk/iso_validate.hpp"
#include "dusk/main.h"
-#include "dusk/ui/prelaunch_options.hpp"
+#include "dusk/settings.h"
+#include "modal.hpp"
+#include "preset.hpp"
+#include "settings.hpp"
#include "version.h"
#include
-#include
#include
+#include "m_Do/m_Do_MemCard.h"
+
namespace dusk::ui {
-namespace {
const Rml::String kDocumentSource = R"RML(
@@ -20,6 +23,8 @@ const Rml::String kDocumentSource = R"RML(
+
+
@@ -49,6 +54,23 @@ constexpr std::array 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 +80,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 +99,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 +116,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 +135,20 @@ 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;
+ }
+ return false;
+}
+
void apply_intro_animation(Rml::Element* element, const char* delay_class) {
if (element == nullptr || delay_class == nullptr) {
return;
@@ -110,25 +157,53 @@ 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();
- mMenuButtons.push_back(
- std::make_unique(menuList, hasValidPath ? "Start Game" : "Select Disc Image"));
+ auto& state = prelaunch_state();
+ mMenuButtons.push_back(std::make_unique(
+ 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;
}
+
+ mDoAud_seStartMenu(kSoundPlay);
+
+ if (getSettings().audio.menuSounds) {
+ JAISoundHandle* handle = g_mEnvSeMgr.field_0x144.getHandle();
+ if (*handle) {
+ (*handle)->stop(60);
+ (*handle)->releaseHandle();
+ }
+ }
+
+ if (g_mDoMemCd_control.mCardCommand == mDoMemCd_Ctrl_c::Command_e::COMM_NONE_e) {
+ mDoMemCd_ThdInit();
+ }
+
IsGameLaunched = true;
+ if (!getSettings().backend.wasPresetChosen) {
+ push_document(std::make_unique());
+ }
hide(true);
});
apply_intro_animation(mMenuButtons.back()->root(), "delay-1");
mMenuButtons.push_back(std::make_unique(menuList, "Options"));
- mMenuButtons.back()->on_pressed([this] { push(std::make_unique()); });
+ mMenuButtons.back()->on_pressed([this] {
+ mRestartSuppressed = false;
+ push(std::make_unique(true));
+ });
apply_intro_animation(mMenuButtons.back()->root(), "delay-2");
mMenuButtons.push_back(std::make_unique(menuList, "Quit To Desktop"));
@@ -140,6 +215,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) {
@@ -157,6 +234,40 @@ void Prelaunch::show() {
Document::show();
mDocument->SetAttribute("open", "");
mRoot->SetAttribute("open", "");
+
+ if (is_restart_pending() && !mRestartSuppressed) {
+ const auto dismiss = [this](Modal& modal) {
+ mRestartSuppressed = true;
+ modal.pop();
+ };
+ std::vector actions;
+ if constexpr (dusk::SupportsProcessRestart) {
+ actions.push_back(ModalAction{
+ .label = "Restart later",
+ .onPressed = dismiss,
+ });
+ actions.push_back(ModalAction{
+ .label = "Restart now",
+ .onPressed = [](Modal&) { dusk::RequestRestart(); },
+ });
+ } else {
+ actions.push_back(ModalAction{
+ .label = "OK",
+ .onPressed = dismiss,
+ });
+ }
+ push(std::make_unique(Modal::Props{
+ .title = "Apply Options",
+ .bodyRml =
+ dusk::SupportsProcessRestart ?
+ "A restart is required to apply selected options. Restart now to "
+ "apply them immediately?" :
+ "A restart is required to apply selected options. Close and reopen "
+ "Dusk to apply them.",
+ .actions = std::move(actions),
+ .onDismiss = dismiss,
+ }));
+ }
}
void Prelaunch::hide(bool close) {
@@ -164,6 +275,8 @@ void Prelaunch::hide(bool close) {
if (!mEntranceAnimationStarted) {
// Close document immediately
Document::hide(true);
+ } else {
+ mPendingClose = true;
}
mDocument->RemoveAttribute("open");
} else {
@@ -173,12 +286,34 @@ 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 dismiss = [](Modal& modal) {
+ prelaunch_state().errorString.clear();
+ modal.pop();
+ };
+ push(std::make_unique(Modal::Props{
+ .title = "Invalid disc image",
+ .bodyRml = state.errorString,
+ .actions =
+ {
+ ModalAction{
+ .label = "OK",
+ .onPressed = dismiss,
+ },
+ },
+ .onDismiss = dismiss,
+ }));
+ }
+
+ const bool hasValidPath = prelaunch_state().selectedDiscIsValid;
+ mDocument->SetClass("disc-ready", hasValidPath);
+ if (hasValidPath) {
+ if (getSettings().backend.skipPreLaunchUI) {
+ hide(true);
+ }
IsGameLaunched = true;
}
@@ -188,7 +323,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 +332,13 @@ 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 +358,7 @@ bool Prelaunch::focus() {
if (mMenuButtons.empty()) {
return false;
}
- return mMenuButtons[0]->focus();
+ return mMenuButtons.front()->focus();
}
bool Prelaunch::visible() const {
@@ -253,6 +386,7 @@ bool Prelaunch::handle_nav_command(Rml::Event& event, NavCommand cmd) {
int i = ((focusedButton + direction) % n + n) % n;
while (i >= 0 && i < mMenuButtons.size()) {
if (mMenuButtons[i]->focus()) {
+ mDoAud_seStartMenu(kSoundItemFocus);
event.StopPropagation();
return true;
}
diff --git a/src/dusk/ui/prelaunch.hpp b/src/dusk/ui/prelaunch.hpp
index e3ab1260b5..e4d542cc2d 100644
--- a/src/dusk/ui/prelaunch.hpp
+++ b/src/dusk/ui/prelaunch.hpp
@@ -24,6 +24,7 @@ protected:
private:
bool mEntranceAnimationStarted = false;
+ bool mRestartSuppressed = false;
std::vector> mMenuButtons;
Rml::Element* mRoot = nullptr;
Rml::Element* mDiscStatus = nullptr;
@@ -34,17 +35,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
diff --git a/src/dusk/ui/prelaunch_options.cpp b/src/dusk/ui/prelaunch_options.cpp
deleted file mode 100644
index 8b82ec3790..0000000000
--- a/src/dusk/ui/prelaunch_options.cpp
+++ /dev/null
@@ -1,263 +0,0 @@
-#include "prelaunch_options.hpp"
-
-#include "dusk/config.hpp"
-#include "dusk/settings.h"
-#include "pane.hpp"
-#include "prelaunch.hpp"
-
-namespace dusk::ui {
-namespace {
-
-static constexpr std::array kLanguageNames = {
- "English", "German", "French", "Spanish", "Italian",
-};
-
-// TODO: Copied from ImGui prelaunch. Needs a refactor?
-bool try_parse_backend(std::string_view backend, AuroraBackend& outBackend) {
- if (backend == "auto") {
- outBackend = BACKEND_AUTO;
- return true;
- }
- if (backend == "d3d11") {
- outBackend = BACKEND_D3D11;
- return true;
- }
- if (backend == "d3d12") {
- outBackend = BACKEND_D3D12;
- return true;
- }
- if (backend == "metal") {
- outBackend = BACKEND_METAL;
- return true;
- }
- if (backend == "vulkan") {
- outBackend = BACKEND_VULKAN;
- return true;
- }
- if (backend == "opengl") {
- outBackend = BACKEND_OPENGL;
- return true;
- }
- if (backend == "opengles") {
- outBackend = BACKEND_OPENGLES;
- return true;
- }
- if (backend == "webgpu") {
- outBackend = BACKEND_WEBGPU;
- return true;
- }
- if (backend == "null") {
- outBackend = BACKEND_NULL;
- return true;
- }
-
- return false;
-}
-
-std::string_view backend_name(AuroraBackend backend) {
- switch (backend) {
- default:
- return "Auto";
- case BACKEND_D3D12:
- return "D3D12";
- case BACKEND_D3D11:
- return "D3D11";
- case BACKEND_METAL:
- return "Metal";
- case BACKEND_VULKAN:
- return "Vulkan";
- case BACKEND_OPENGL:
- return "OpenGL";
- case BACKEND_OPENGLES:
- return "OpenGL ES";
- case BACKEND_WEBGPU:
- return "WebGPU";
- case BACKEND_NULL:
- return "Null";
- }
-}
-
-std::string_view backend_id(AuroraBackend backend) {
- switch (backend) {
- default:
- return "auto";
- case BACKEND_D3D12:
- return "d3d12";
- case BACKEND_D3D11:
- return "d3d11";
- case BACKEND_METAL:
- return "metal";
- case BACKEND_VULKAN:
- return "vulkan";
- case BACKEND_OPENGL:
- return "opengl";
- case BACKEND_OPENGLES:
- return "opengles";
- case BACKEND_WEBGPU:
- return "webgpu";
- case BACKEND_NULL:
- return "null";
- }
-}
-
-std::vector available_backends() {
- std::vector backends;
- backends.emplace_back(BACKEND_AUTO);
- size_t backendCount = 0;
- const AuroraBackend* raw = aurora_get_available_backends(&backendCount);
- for (size_t i = 0; i < backendCount; ++i) {
- // Do not expose NULL or D3D11
- if (raw[i] != BACKEND_NULL && raw[i] != BACKEND_D3D11) {
- backends.emplace_back(raw[i]);
- }
- }
- return backends;
-}
-
-class LanguageSelect final : public SelectButton {
-public:
- explicit LanguageSelect(Rml::Element* parent) : SelectButton(parent, Props{.key = "Language"}) {}
-
- 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();
- }
- set_disabled(true);
- } else {
- set_disabled(false);
- }
-
- const auto lang = getSettings().game.language.getValue();
- auto value = static_cast(lang);
- if (value >= kLanguageNames.size()) {
- getSettings().game.language.setValue(GameLanguage::English);
- config::Save();
- value = static_cast(getSettings().game.language.getValue());
- }
- set_value_label(kLanguageNames[value]);
- SelectButton::update();
- }
-
-protected:
- bool handle_nav_command(NavCommand cmd) override {
- if (disabled()) {
- return false;
- }
- if (cmd != NavCommand::Confirm && cmd != NavCommand::Left && cmd != NavCommand::Right) {
- return false;
- }
-
- constexpr int n = static_cast(kLanguageNames.size());
- int idx = static_cast(getSettings().game.language.getValue());
- const int dir = (cmd == NavCommand::Left) ? -1 : 1;
- idx = ((idx + dir) % n + n) % n;
- getSettings().game.language.setValue(static_cast(idx));
- config::Save();
- return true;
- }
-};
-
-class BackendSelect final : public SelectButton {
-public:
- explicit BackendSelect(Rml::Element* parent) : SelectButton(parent, Props{.key = "Graphics Backend"}) {}
-
- void update() override {
- AuroraBackend configuredBackend = BACKEND_AUTO;
- const auto configuredId = getSettings().backend.graphicsBackend.getValue();
- if (!try_parse_backend(configuredId, configuredBackend)) {
- configuredBackend = BACKEND_AUTO;
- }
- // Do not expose NULL or D3D11
- if (configuredBackend == BACKEND_NULL || configuredBackend == BACKEND_D3D11) {
- getSettings().backend.graphicsBackend.setValue("auto");
- config::Save();
- configuredBackend = BACKEND_AUTO;
- }
-
- const auto backend = getSettings().backend.graphicsBackend.getValue();
- Rml::String value = backend_name(configuredBackend).data();
- if (backend != prelaunch_state().initialGraphicsBackend) {
- value += " (restart required)";
- }
- set_value_label(value);
- SelectButton::update();
- }
-
-protected:
- bool handle_nav_command(NavCommand cmd) override {
- if (cmd != NavCommand::Confirm && cmd != NavCommand::Left && cmd != NavCommand::Right) {
- return false;
- }
-
- const auto backends = available_backends();
- const int n = static_cast(backends.size());
- if (n <= 0) {
- return false;
- }
-
- AuroraBackend configuredBackend = BACKEND_AUTO;
- const auto configuredId = getSettings().backend.graphicsBackend.getValue();
- if (!try_parse_backend(configuredId, configuredBackend)) {
- configuredBackend = BACKEND_AUTO;
- }
-
- int idx = 0;
- for (int i = 0; i < n; ++i) {
- if (backends[static_cast(i)] == configuredBackend) {
- idx = i;
- break;
- }
- }
-
- 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(idx)])));
- config::Save();
- return true;
- }
-};
-
-class SaveTypeSelect final : public SelectButton {
-public:
- explicit SaveTypeSelect(Rml::Element* parent) : SelectButton(parent, Props{.key = "Save File Type"}) {}
-
- void update() override {
- const CARDFileType cft = static_cast(getSettings().backend.cardFileType.getValue());
- set_value_label(cft == CARD_GCIFOLDER ? "GCI Folder" : "Card Image");
- SelectButton::update();
- }
-
-protected:
- bool handle_nav_command(NavCommand cmd) override {
- if (cmd != NavCommand::Confirm && cmd != NavCommand::Left && cmd != NavCommand::Right) {
- return false;
- }
-
- CARDFileType cft = static_cast(getSettings().backend.cardFileType.getValue());
- const CARDFileType newValue = cft == CARD_GCIFOLDER ? CARD_RAWIMAGE : CARD_GCIFOLDER;
- getSettings().backend.cardFileType.setValue(newValue);
- config::Save();
- return true;
- }
-};
-
-} // namespace
-
-PrelaunchOptions::PrelaunchOptions() {
- add_tab("Options", [this](Rml::Element* content) {
- auto& leftPane = add_child(content, Pane::Type::Controlled);
- leftPane.add_child();
- leftPane.add_child();
- leftPane.add_child();
- });
-}
-
-} // namespace dusk::ui
diff --git a/src/dusk/ui/prelaunch_options.hpp b/src/dusk/ui/prelaunch_options.hpp
deleted file mode 100644
index bc08243ed6..0000000000
--- a/src/dusk/ui/prelaunch_options.hpp
+++ /dev/null
@@ -1,12 +0,0 @@
-#pragma once
-
-#include "window.hpp"
-
-namespace dusk::ui {
-
-class PrelaunchOptions : public Window {
-public:
- PrelaunchOptions();
-};
-
-} // namespace dusk::ui
diff --git a/src/dusk/ui/preset.cpp b/src/dusk/ui/preset.cpp
index c636402eb2..d83d30cda9 100644
--- a/src/dusk/ui/preset.cpp
+++ b/src/dusk/ui/preset.cpp
@@ -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
@@ -20,14 +18,12 @@ void applyPresetClassic() {
s.game.internalResolutionScale.setValue(1);
s.game.shadowResolutionMultiplier.setValue(1);
s.game.hideTvSettingsScreen.setValue(false);
- s.game.skipWarningScreen.setValue(false);
AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT);
}
void applyPresetDusk() {
auto& s = getSettings();
s.game.hideTvSettingsScreen.setValue(true);
- s.game.skipWarningScreen.setValue(true);
s.game.noReturnRupees.setValue(true);
s.game.disableRupeeCutscenes.setValue(true);
s.game.noSwordRecoil.setValue(true);
@@ -49,49 +45,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";
-
} // 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. "
"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 +79,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(col, Rml::String(preset.name));
@@ -129,26 +96,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();
@@ -174,7 +127,7 @@ bool PresetWindow::handle_nav_command(Rml::Event& event, NavCommand cmd) {
const int next = i + direction;
if (next >= 0 && next < static_cast(mButtons.size())) {
if (mButtons[next]->focus()) {
- mDoAud_seStartMenu(Z2SE_SY_NAME_CURSOR);
+ mDoAud_seStartMenu(kSoundItemFocus);
return true;
}
}
diff --git a/src/dusk/ui/preset.hpp b/src/dusk/ui/preset.hpp
index e51c769b1c..22b6eaf07e 100644
--- a/src/dusk/ui/preset.hpp
+++ b/src/dusk/ui/preset.hpp
@@ -1,25 +1,22 @@
#pragma once
#include "component.hpp"
-#include "document.hpp"
+#include "window.hpp"
+
#include
#include
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> mButtons;
};
diff --git a/src/dusk/ui/select_button.cpp b/src/dusk/ui/select_button.cpp
index ad98ed422e..313594836e 100644
--- a/src/dusk/ui/select_button.cpp
+++ b/src/dusk/ui/select_button.cpp
@@ -59,6 +59,7 @@ SelectButton& SelectButton::on_pressed(SelectButtonCallback callback) {
listen(Rml::EventId::Submit, [this, callback = std::move(callback)](Rml::Event& event) {
if (!disabled() && event.GetTargetElement() == mRoot) {
callback();
+ event.StopPropagation();
}
});
return *this;
diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp
index 96bf27d735..7979b22116 100644
--- a/src/dusk/ui/settings.cpp
+++ b/src/dusk/ui/settings.cpp
@@ -8,17 +8,143 @@
#include "dusk/config.hpp"
#include "dusk/imgui/ImGuiEngine.hpp"
#include "dusk/livesplit.h"
+#include "graphics_tuner.hpp"
#include "m_Do/m_Do_main.h"
#include "number_button.hpp"
-#include "overlay.hpp"
#include "pane.hpp"
+#include "prelaunch.hpp"
#include "ui.hpp"
#include
+#include "modal.hpp"
+
namespace dusk::ui {
namespace {
+constexpr std::array kLanguageNames = {
+ "English",
+ "German",
+ "French",
+ "Spanish",
+ "Italian",
+};
+
+constexpr std::array kCardFileTypes = {
+ "Card Image",
+ "GCI Folder",
+};
+
+bool try_parse_backend(std::string_view backend, AuroraBackend& outBackend) {
+ if (backend == "auto") {
+ outBackend = BACKEND_AUTO;
+ return true;
+ }
+ if (backend == "d3d11") {
+ outBackend = BACKEND_D3D11;
+ return true;
+ }
+ if (backend == "d3d12") {
+ outBackend = BACKEND_D3D12;
+ return true;
+ }
+ if (backend == "metal") {
+ outBackend = BACKEND_METAL;
+ return true;
+ }
+ if (backend == "vulkan") {
+ outBackend = BACKEND_VULKAN;
+ return true;
+ }
+ if (backend == "opengl") {
+ outBackend = BACKEND_OPENGL;
+ return true;
+ }
+ if (backend == "opengles") {
+ outBackend = BACKEND_OPENGLES;
+ return true;
+ }
+ if (backend == "webgpu") {
+ outBackend = BACKEND_WEBGPU;
+ return true;
+ }
+ if (backend == "null") {
+ outBackend = BACKEND_NULL;
+ return true;
+ }
+
+ return false;
+}
+
+std::string_view backend_name(AuroraBackend backend) {
+ switch (backend) {
+ default:
+ return "Auto";
+ case BACKEND_D3D12:
+ return "D3D12";
+ case BACKEND_D3D11:
+ return "D3D11";
+ case BACKEND_METAL:
+ return "Metal";
+ case BACKEND_VULKAN:
+ return "Vulkan";
+ case BACKEND_OPENGL:
+ return "OpenGL";
+ case BACKEND_OPENGLES:
+ return "OpenGL ES";
+ case BACKEND_WEBGPU:
+ return "WebGPU";
+ case BACKEND_NULL:
+ return "Null";
+ }
+}
+
+std::string_view backend_id(AuroraBackend backend) {
+ switch (backend) {
+ default:
+ return "auto";
+ case BACKEND_D3D12:
+ return "d3d12";
+ case BACKEND_D3D11:
+ return "d3d11";
+ case BACKEND_METAL:
+ return "metal";
+ case BACKEND_VULKAN:
+ return "vulkan";
+ case BACKEND_OPENGL:
+ return "opengl";
+ case BACKEND_OPENGLES:
+ return "opengles";
+ case BACKEND_WEBGPU:
+ return "webgpu";
+ case BACKEND_NULL:
+ return "null";
+ }
+}
+
+std::vector available_backends() {
+ std::vector backends;
+ backends.emplace_back(BACKEND_AUTO);
+ size_t backendCount = 0;
+ const AuroraBackend* raw = aurora_get_available_backends(&backendCount);
+ for (size_t i = 0; i < backendCount; ++i) {
+ // Do not expose NULL or D3D11
+ if (raw[i] != BACKEND_NULL && raw[i] != BACKEND_D3D11) {
+ backends.emplace_back(raw[i]);
+ }
+ }
+ return backends;
+}
+
+AuroraBackend configured_backend() {
+ AuroraBackend configuredBackend = BACKEND_AUTO;
+ const auto configuredId = getSettings().backend.graphicsBackend.getValue();
+ if (!try_parse_backend(configuredId, configuredBackend)) {
+ configuredBackend = BACKEND_AUTO;
+ }
+ return configuredBackend;
+}
+
void reset_for_speedrun_mode() {
mDoMain::developmentMode = -1;
@@ -130,8 +256,8 @@ SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar
-void overlay_control(
- Window& window, Pane& leftPane, Pane& rightPane, ConfigVar& var, const OverlayProps& props) {
+void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, ConfigVar& var,
+ const GraphicsTunerProps& props) {
leftPane.register_control(
leftPane
.add_select_button({
@@ -152,7 +278,7 @@ void overlay_control(
.on_nav_command([&window, props](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left ||
cmd == NavCommand::Right) {
- window.push(std::make_unique(props));
+ window.push(std::make_unique(props));
return true;
}
return false;
@@ -165,30 +291,165 @@ void overlay_control(
} // namespace
-SettingsWindow::SettingsWindow() {
+SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
+ if (prelaunch) {
+ mSuppressNavFallback = true;
+ add_tab("Prelaunch", [this](Rml::Element* content) {
+ auto& leftPane = add_child(content, Pane::Type::Controlled);
+ auto& rightPane = add_child(content, Pane::Type::Uncontrolled);
+
+ leftPane.register_control(
+ leftPane
+ .add_select_button({
+ .key = "Disc Image",
+ .getValue =
+ [] {
+ 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;
+ }
+ }
+ return display;
+ },
+ .isModified =
+ [] {
+ const auto& state = prelaunch_state();
+ const auto& initial = state.initialDiscPath;
+ return !initial.empty() && state.selectedDiscPath != initial;
+ },
+ })
+ .on_pressed([] { open_iso_picker(); }),
+ rightPane, [](Pane& pane) {
+ pane.add_rml("Set the disc image that Dusk uses to launch the game. "
+ "Changes require a restart.");
+ });
+ leftPane.register_control(
+ leftPane.add_select_button({
+ .key = "Language",
+ .getValue =
+ [] {
+ const auto& state = prelaunch_state();
+ if (!state.selectedDiscIsValid || !state.selectedDiscIsPal) {
+ return kLanguageNames[0];
+ }
+ const u8 idx = static_cast(getSettings().game.language.getValue());
+ return kLanguageNames[idx];
+ },
+ .isDisabled =
+ [] {
+ const auto& state = prelaunch_state();
+ return !state.selectedDiscIsValid || !state.selectedDiscIsPal;
+ },
+ .isModified =
+ [] {
+ return getSettings().game.language.getValue() !=
+ prelaunch_state().initialLanguage;
+ },
+ }),
+ rightPane, [](Pane& pane) {
+ for (int i = 0; i < kLanguageNames.size(); i++) {
+ pane.add_button({
+ .text = kLanguageNames[i],
+ .isSelected =
+ [i] {
+ return getSettings().game.language.getValue() ==
+ static_cast(i);
+ },
+ })
+ .on_pressed([i] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ getSettings().game.language.setValue(static_cast(i));
+ config::Save();
+ });
+ }
+ pane.add_rml(" Changes require a restart.");
+ });
+ leftPane.register_control(
+ leftPane.add_select_button({
+ .key = "Graphics Backend",
+ .getValue = [] { return Rml::String{backend_name(configured_backend())}; },
+ .isModified =
+ [] {
+ return getSettings().backend.graphicsBackend.getValue() !=
+ prelaunch_state().initialGraphicsBackend;
+ },
+ }),
+ rightPane, [](Pane& pane) {
+ const auto availableBackends = available_backends();
+ for (const auto backend : availableBackends) {
+ pane
+ .add_button({
+ .text = Rml::String{backend_name(backend)},
+ .isSelected = [backend] { return configured_backend() == backend; },
+ })
+ .on_pressed([backend] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ getSettings().backend.graphicsBackend.setValue(
+ std::string{backend_id(backend)});
+ config::Save();
+ });
+ }
+ pane.add_rml(" Changes require a restart.");
+ });
+ leftPane.register_control(
+ leftPane.add_select_button({
+ .key = "Save File Type",
+ .getValue =
+ [] {
+ return kCardFileTypes[getSettings().backend.cardFileType.getValue()];
+ },
+ .isModified =
+ [] {
+ return getSettings().backend.cardFileType.getValue() !=
+ prelaunch_state().initialCardFileType;
+ },
+ }),
+ rightPane, [](Pane& pane) {
+ for (int i = 0; i < kCardFileTypes.size(); i++) {
+ pane
+ .add_button({
+ .text = kCardFileTypes[i],
+ .isSelected =
+ [i] {
+ return getSettings().backend.cardFileType.getValue() == i;
+ },
+ })
+ .on_pressed([i] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ getSettings().backend.cardFileType.setValue(i);
+ config::Save();
+ });
+ }
+ });
+ });
+ }
+
add_tab("Graphics", [this](Rml::Element* content) {
auto& leftPane = add_child(content, Pane::Type::Controlled);
auto& rightPane = add_child(content, Pane::Type::Uncontrolled);
leftPane.add_section("Display");
- leftPane.register_control(
- leftPane.add_button("Toggle Fullscreen").on_pressed([] {
- getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
- VISetWindowFullscreen(getSettings().video.enableFullscreen);
- config::Save();
- }),
- rightPane, [](Pane& pane) { pane.clear(); }
- );
- leftPane.register_control(
- leftPane.add_button("Restore Default Window Size").on_pressed([] {
- getSettings().video.enableFullscreen.setValue(false);
- VISetWindowFullscreen(false);
- VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2);
- VICenterWindow();
- }),
- rightPane, [](Pane& pane) { pane.clear(); }
- );
+ leftPane.register_control(leftPane.add_button("Toggle Fullscreen").on_pressed([] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
+ VISetWindowFullscreen(getSettings().video.enableFullscreen);
+ config::Save();
+ }),
+ rightPane, [](Pane& pane) { pane.clear(); });
+ leftPane.register_control(leftPane.add_button("Restore Default Window Size").on_pressed([] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ getSettings().video.enableFullscreen.setValue(false);
+ VISetWindowFullscreen(false);
+ VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2);
+ VICenterWindow();
+ }),
+ rightPane, [](Pane& pane) { pane.clear(); });
config_bool_select(leftPane, rightPane, getSettings().video.enableVsync,
{
.key = "Enable VSync",
@@ -212,8 +473,9 @@ SettingsWindow::SettingsWindow() {
});
leftPane.add_section("Resolution");
- overlay_control(*this, leftPane, rightPane, getSettings().game.internalResolutionScale,
- OverlayProps{
+ graphics_tuner_control(*this, leftPane, rightPane,
+ getSettings().game.internalResolutionScale,
+ GraphicsTunerProps{
.option = GraphicsOption::InternalResolution,
.title = "Internal Resolution",
.helpText = kInternalResolutionHelpText,
@@ -221,8 +483,9 @@ SettingsWindow::SettingsWindow() {
.valueMax = 12,
.defaultValue = 0,
});
- overlay_control(*this, leftPane, rightPane, getSettings().game.shadowResolutionMultiplier,
- OverlayProps{
+ graphics_tuner_control(*this, leftPane, rightPane,
+ getSettings().game.shadowResolutionMultiplier,
+ GraphicsTunerProps{
.option = GraphicsOption::ShadowResolution,
.title = "Shadow Resolution",
.helpText = kShadowResolutionHelpText,
@@ -232,8 +495,8 @@ SettingsWindow::SettingsWindow() {
});
leftPane.add_section("Post-Processing");
- overlay_control(*this, leftPane, rightPane, getSettings().game.bloomMode,
- OverlayProps{
+ graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMode,
+ GraphicsTunerProps{
.option = GraphicsOption::BloomMode,
.title = "Bloom",
.helpText = kBloomHelpText,
@@ -241,8 +504,8 @@ SettingsWindow::SettingsWindow() {
.valueMax = static_cast(BloomMode::Dusk),
.defaultValue = static_cast(BloomMode::Classic),
});
- overlay_control(*this, leftPane, rightPane, getSettings().game.bloomMultiplier,
- OverlayProps{
+ graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMultiplier,
+ GraphicsTunerProps{
.option = GraphicsOption::BloomMultiplier,
.title = "Bloom Brightness",
.helpText = kBloomBrightnessHelpText,
@@ -597,11 +860,6 @@ SettingsWindow::SettingsWindow() {
.key = "Skip TV Settings Screen",
.helpText = "Skips the TV calibration screen shown when loading a save.",
});
- config_bool_select(leftPane, rightPane, getSettings().game.skipWarningScreen,
- {
- .key = "Skip Warning Screen",
- .helpText = "Skips the warning screen shown when starting the game.",
- });
config_bool_select(leftPane, rightPane, getSettings().backend.showPipelineCompilation,
{
.key = "Show Pipeline Compilation",
@@ -610,4 +868,31 @@ SettingsWindow::SettingsWindow() {
});
}
+void SettingsWindow::update() {
+ // Show disc validation error message if present
+ if (mPrelaunch && top_document() == this) {
+ auto& state = prelaunch_state();
+ if (!state.errorString.empty()) {
+ auto dismissInvalidDisc = [](Modal& modal) {
+ prelaunch_state().errorString.clear();
+ modal.pop();
+ };
+ push_document(std::make_unique(Modal::Props{
+ .title = "Invalid disc image",
+ .bodyRml = state.errorString,
+ .actions =
+ {
+ ModalAction{
+ .label = "OK",
+ .onPressed = dismissInvalidDisc,
+ },
+ },
+ .onDismiss = dismissInvalidDisc,
+ }));
+ }
+ }
+
+ Window::update();
+}
+
} // namespace dusk::ui
diff --git a/src/dusk/ui/settings.hpp b/src/dusk/ui/settings.hpp
index f6af1fd719..1d071e6575 100644
--- a/src/dusk/ui/settings.hpp
+++ b/src/dusk/ui/settings.hpp
@@ -5,7 +5,12 @@ namespace dusk::ui {
class SettingsWindow : public Window {
public:
- SettingsWindow();
+ SettingsWindow(bool prelaunch = false);
+
+ void update() override;
+
+protected:
+ bool mPrelaunch;
};
} // namespace dusk::ui
\ No newline at end of file
diff --git a/src/dusk/ui/tab_bar.cpp b/src/dusk/ui/tab_bar.cpp
index 77a699a591..422e824a20 100644
--- a/src/dusk/ui/tab_bar.cpp
+++ b/src/dusk/ui/tab_bar.cpp
@@ -47,7 +47,6 @@ TabBar::TabBar(Rml::Element* parent, Props props)
add_child(Button::Props{}, "close")
.on_nav_command([this](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm) {
- mDoAud_seStartMenu(Z2SE_SY_CURSOR_CANCEL);
mProps.onClose();
return true;
}
@@ -119,7 +118,9 @@ void TabBar::add_tab(const Rml::String& title, TabCallback callback) {
auto& button = add_child(Button::Props{title}, "tab");
button.on_nav_command([this, index](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm) {
- mDoAud_seStartMenu(mProps.autoSelect ? Z2SE_SY_NAME_CURSOR : Z2SE_SY_CURSOR_OK);
+ if (mProps.autoSelect) {
+ mDoAud_seStartMenu(kSoundTabChanged);
+ }
set_active_tab(index);
return true;
}
@@ -229,7 +230,7 @@ bool TabBar::handle_nav_command(Rml::Event& event, NavCommand cmd) {
while (i >= 0 && i < mTabs.size()) {
const bool changed = mProps.autoSelect ? set_active_tab(i) : focus_tab(i);
if (changed) {
- mDoAud_seStartMenu(Z2SE_SY_NAME_CURSOR);
+ mDoAud_seStartMenu(kSoundTabChanged);
return true;
}
i += direction;
diff --git a/src/dusk/ui/ui.cpp b/src/dusk/ui/ui.cpp
index 426901d8d9..367dd69086 100644
--- a/src/dusk/ui/ui.cpp
+++ b/src/dusk/ui/ui.cpp
@@ -10,6 +10,7 @@
#include "aurora/lib/window.hpp"
#include "input.hpp"
+#include "prelaunch.hpp"
#include "window.hpp"
namespace dusk::ui {
@@ -20,7 +21,10 @@ void load_font(const char* filename, bool fallback = false) {
}
bool sInitialized = false;
-std::vector > sDocuments;
+std::vector > sDocumentStack;
+// Documents that don't participate in the focus stack
+std::vector > sPassiveDocuments;
+std::deque sToasts;
} // namespace
@@ -45,15 +49,20 @@ bool initialize() noexcept {
}
void shutdown() noexcept {
- sDocuments.clear();
+ sDocumentStack.clear();
+ sPassiveDocuments.clear();
reset_input_state();
release_input_block();
sInitialized = false;
}
-Document& push_document(std::unique_ptr doc, bool show) noexcept {
+Document& push_document(std::unique_ptr doc, bool show, bool passive) noexcept {
Document& ret = *doc;
- sDocuments.push_back({std::move(doc)});
+ if (passive) {
+ sPassiveDocuments.push_back(std::move(doc));
+ } else {
+ sDocumentStack.push_back({std::move(doc)});
+ }
if (show) {
ret.show();
}
@@ -69,12 +78,19 @@ void show_top_document() noexcept {
}
bool any_document_visible() noexcept {
- return std::any_of(sDocuments.begin(), sDocuments.end(),
+ return std::any_of(sDocumentStack.begin(), sDocumentStack.end(),
[](const auto& doc) { return doc && doc->visible(); });
}
+bool is_prelaunch_open() noexcept {
+ return std::any_of(sDocumentStack.begin(), sDocumentStack.end(), [](const auto& doc) {
+ const auto* prelaunch = dynamic_cast(doc.get());
+ return prelaunch != nullptr && !prelaunch->pending_close() && !prelaunch->closed();
+ });
+}
+
Document* top_document() noexcept {
- for (auto& doc : std::views::reverse(sDocuments)) {
+ for (auto& doc : std::views::reverse(sDocumentStack)) {
if (!doc->closed() && !doc->pending_close()) {
return doc.get();
}
@@ -84,20 +100,31 @@ Document* top_document() noexcept {
void update() noexcept {
update_input();
- for (const auto& doc : sDocuments) {
+ for (const auto& doc : sDocumentStack) {
+ doc->update();
+ }
+ for (const auto& doc : sPassiveDocuments) {
doc->update();
}
// Remove closed documents
- const auto [first, last] =
- std::ranges::remove_if(sDocuments, [](const auto& doc) { return doc->closed(); });
- sDocuments.erase(first, last);
+ {
+ const auto [first, last] =
+ std::ranges::remove_if(sDocumentStack, [](const auto& doc) { return doc->closed(); });
+ sDocumentStack.erase(first, last);
+ }
+ {
+ const auto [first, last] = std::ranges::remove_if(
+ sPassiveDocuments, [](const auto& doc) { return doc->closed(); });
+ sPassiveDocuments.erase(first, last);
+ }
// If no documents have focus, explicitly focus the top one
if (auto* context = aurora::rmlui::get_context();
- context != nullptr && context->GetFocusElement() == nullptr)
+ context != nullptr && (context->GetFocusElement() == nullptr ||
+ context->GetFocusElement() == context->GetRootElement()))
{
- for (auto& doc : std::views::reverse(sDocuments)) {
+ for (auto& doc : std::views::reverse(sDocumentStack)) {
if (!doc->closed() && !doc->pending_close() && doc->focus()) {
break;
}
@@ -140,6 +167,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(
event.GetParameter("key_identifier", Rml::Input::KI_UNKNOWN));
@@ -201,4 +239,12 @@ Insets safe_area_insets(Rml::Context* context) noexcept {
};
}
+void push_toast(Toast toast) noexcept {
+ sToasts.push_back(std::move(toast));
+}
+
+std::deque& get_toasts() noexcept {
+ return sToasts;
+}
+
} // namespace dusk::ui
diff --git a/src/dusk/ui/ui.hpp b/src/dusk/ui/ui.hpp
index f5c0e6e63f..7f59114ca1 100644
--- a/src/dusk/ui/ui.hpp
+++ b/src/dusk/ui/ui.hpp
@@ -12,7 +12,43 @@
namespace dusk::ui {
class Document;
-class Popup;
+
+using clock = std::chrono::steady_clock;
+
+struct Toast {
+ Rml::String type;
+ Rml::String title;
+ Rml::String content;
+ clock::duration duration;
+};
+
+// Button clicked/pressed
+constexpr u32 kSoundClick = Z2SE_SY_CURSOR_OK;
+// "Play" button clicked/pressed
+constexpr u32 kSoundPlay = Z2SE_SY_ITEM_COMBINE_ON;
+
+// Menu button pressed (open/close menu bar or hide/show the active window)
+constexpr u32 kSoundMenuOpen = Z2SE_SY_MENU_SUB_IN;
+constexpr u32 kSoundMenuClose = Z2SE_SY_MENU_SUB_OUT;
+
+// Window opened/closed
+constexpr u32 kSoundWindowOpen = Z2SE_SY_MENU_NEXT;
+constexpr u32 kSoundWindowClose = Z2SE_SY_MENU_BACK;
+
+// Window tab changed
+constexpr u32 kSoundTabChanged = Z2SE_SY_MENU_CURSOR_COMMON;
+
+// Item within menu focused
+constexpr u32 kSoundItemFocus = Z2SE_SY_CURSOR_ITEM;
+// Item changed (e.g. number input left/right)
+constexpr u32 kSoundItemChange = Z2SE_SY_NAME_CURSOR;
+// Item enabled ("On")
+constexpr u32 kSoundItemEnable = Z2SE_SUBJ_VIEW_IN;
+// Item disabled ("Off")
+constexpr u32 kSoundItemDisable = Z2SE_SUBJ_VIEW_OUT;
+
+// Achievement unlocked
+constexpr u32 kSoundAchievementUnlock = Z2SE_NAVI_FLY;
struct Insets {
float top = 0.0f;
@@ -32,17 +68,21 @@ void shutdown() noexcept;
void handle_event(const SDL_Event& event) noexcept;
void update() noexcept;
-Document& push_document(std::unique_ptr doc, bool show = true) noexcept;
+Document& push_document(
+ std::unique_ptr doc, bool show = true, bool passive = false) 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) 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;
+void push_toast(Toast toast) noexcept;
+std::deque& get_toasts() noexcept;
+
} // namespace dusk::ui
diff --git a/src/dusk/ui/window.cpp b/src/dusk/ui/window.cpp
index fda098f63b..55ce96c69f 100644
--- a/src/dusk/ui/window.cpp
+++ b/src/dusk/ui/window.cpp
@@ -39,11 +39,24 @@ const Rml::String kDocumentSource = R"RML(
)RML";
+const Rml::String kDocumentSourceSmall = R"RML(
+
+
+
+
+
+
+
+
+
+
+)RML";
+
} // namespace
Window::Window() : Document(kDocumentSource), mRoot(mDocument->GetElementById("window")) {
mTabBar = std::make_unique(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);
}
});
@@ -91,6 +106,10 @@ Window::Window() : Document(kDocumentSource), mRoot(mDocument->GetElementById("w
void Window::show() {
Document::show();
mRoot->SetAttribute("open", "");
+ if (mInitialOpen) {
+ mDoAud_seStartMenu(kSoundWindowOpen);
+ mInitialOpen = false;
+ }
}
void Window::hide(bool close) {
@@ -139,6 +158,17 @@ bool Window::set_active_tab(int index) {
return mTabBar->set_active_tab(index);
}
+void Window::request_close() {
+ if (!consume_close_request()) {
+ mDoAud_seStartMenu(kSoundWindowClose);
+ pop();
+ }
+}
+
+bool Window::consume_close_request() {
+ return false;
+}
+
void Window::refresh_active_tab() {
mTabBar->refresh_active_tab();
}
@@ -176,25 +206,24 @@ bool Window::handle_nav_command(Rml::Event& event, NavCommand cmd) {
}
if (cmd == NavCommand::Confirm || cmd == NavCommand::Down) {
if (!mContentComponents.empty() && mContentComponents.front()->focus()) {
- mDoAud_seStartMenu(Z2SE_SY_NAME_CURSOR);
+ mDoAud_seStartMenu(kSoundItemFocus);
return true;
}
}
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 {
if (cmd == NavCommand::Up) {
if (focus()) {
- mDoAud_seStartMenu(Z2SE_SY_NAME_CURSOR);
+ mDoAud_seStartMenu(kSoundItemFocus);
return true;
}
return false;
@@ -250,4 +279,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
\ No newline at end of file
diff --git a/src/dusk/ui/window.hpp b/src/dusk/ui/window.hpp
index fb0b130159..5d732da70b 100644
--- a/src/dusk/ui/window.hpp
+++ b/src/dusk/ui/window.hpp
@@ -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
requires std::is_base_of_v T& add_child(Args&&... args) {
@@ -51,6 +54,21 @@ protected:
std::unique_ptr mTabBar;
std::vector > mContentComponents;
Insets mBodyPadding;
+ bool mInitialOpen = true;
+};
+
+// 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
diff --git a/src/m_Do/m_Do_graphic.cpp b/src/m_Do/m_Do_graphic.cpp
index f7866850df..90ffda0683 100644
--- a/src/m_Do/m_Do_graphic.cpp
+++ b/src/m_Do/m_Do_graphic.cpp
@@ -1237,8 +1237,12 @@ static void trimming(view_class* param_0, view_port_class* param_1) {
GXEnd();
}
+#ifndef TARGET_PC
+ // due to rounding, the scaled scissor region doesn't align with the untrimmed area
+ // this creates a gap when drawing the flipped image for mirror mode
GXSetScissor(param_1->scissor.x_orig, param_1->scissor.y_orig, param_1->scissor.width,
param_1->scissor.height);
+#endif
}
#if !PLATFORM_WII && !TARGET_PC
diff --git a/src/m_Do/m_Do_machine.cpp b/src/m_Do/m_Do_machine.cpp
index c92d32b27a..a8f4f6140f 100644
--- a/src/m_Do/m_Do_machine.cpp
+++ b/src/m_Do/m_Do_machine.cpp
@@ -31,6 +31,7 @@
#if TARGET_PC
#include
+#include "dusk/ui/ui.hpp"
#endif
#if !PLATFORM_GCN
@@ -1009,7 +1010,14 @@ int mDoMch_Create() {
JKRAram::setSZSBufferSize(0x2000);
mDoDvdThd::create(OSGetThreadPriority(OSGetCurrentThread()) - 2);
mDoDvdErr_ThdInit();
+
+#if TARGET_PC
+ if (!dusk::ui::is_prelaunch_open()) {
+ mDoMemCd_ThdInit();
+ }
+#else
mDoMemCd_ThdInit();
+#endif
return 1;
}
diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp
index 6d256ebf4f..203484af31 100644
--- a/src/m_Do/m_Do_main.cpp
+++ b/src/m_Do/m_Do_main.cpp
@@ -46,6 +46,7 @@
#include
#include
#include "SSystem/SComponent/c_API.h"
+#include "dusk/achievements.h"
#include "dusk/app_info.hpp"
#include "dusk/crash_reporting.h"
#include "dusk/dusk.h"
@@ -54,9 +55,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/menu_bar.hpp"
+#include "dusk/ui/overlay.hpp"
#include "dusk/ui/prelaunch.hpp"
#include "dusk/ui/preset.hpp"
#include "dusk/ui/ui.hpp"
@@ -109,9 +112,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));
@@ -280,6 +289,7 @@ void main01(void) {
dusk::gyro::read(pacing.sim_pace);
fapGm_Execute();
mDoAud_Execute();
+ dusk::AchievementSystem::get().tick();
dusk::game_clock::commit_sim_tick();
}
}
@@ -489,7 +499,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;
@@ -607,34 +617,51 @@ int game_main(int argc, char* argv[]) {
dusk::audio::EnableHrtf = dusk::getSettings().audio.enableHrtf;
dusk::ui::initialize();
+ dusk::ui::push_document(std::make_unique(), true, true);
+ dusk::ui::push_document(std::make_unique(), 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();
- 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(), true);
+ if (!dusk::getSettings().backend.skipPreLaunchUI) {
+ dusk::ui::push_document(std::make_unique(), 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;
@@ -642,13 +669,17 @@ 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::IsGameLaunched = true;
}
- dusk::ui::push_document(std::make_unique(), false);
if (!dusk::getSettings().backend.wasPresetChosen) {
dusk::ui::push_document(std::make_unique());
}