mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-05-30 08:26:24 -04:00
Merge branch 'main' of https://github.com/TwilitRealm/dusk into randomizer
This commit is contained in:
@@ -34,8 +34,8 @@ First, make sure your dump of the game is clean and supported by Dusk. You can d
|
||||
|
||||
- Extract the .zip file
|
||||
- Launch Dusk
|
||||
- Press **Select Disc Image**, navigate to your game dump, and select the file
|
||||
- Press **Start Game** to play!
|
||||
- Press **Select Disc Image** and provide the path to your supported game dump.
|
||||
- Press **Play**!
|
||||
|
||||
# Building
|
||||
|
||||
@@ -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).
|
||||
|
||||
<br/>
|
||||
<div align="center">
|
||||
<a href="https://github.com/encounter/aurora">
|
||||
<img src="assets/aurora-powered.png" alt="Powered by Aurora" width="800">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Vendored
+1
-1
Submodule extern/aurora updated: 518747aa86...f845a5a58c
+10
-8
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -50,8 +50,6 @@ public:
|
||||
bool hasSignal(const char* key) const;
|
||||
|
||||
std::vector<Achievement> getAchievements() const;
|
||||
bool hasPendingUnlock() const { return !m_pendingUnlocks.empty(); }
|
||||
std::string consumePendingUnlock();
|
||||
|
||||
private:
|
||||
struct Entry {
|
||||
@@ -68,7 +66,6 @@ private:
|
||||
std::unordered_set<std::string_view> m_signals;
|
||||
bool m_loaded = false;
|
||||
bool m_dirty = false;
|
||||
std::queue<std::string> m_pendingUnlocks;
|
||||
};
|
||||
|
||||
} // namespace dusk
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
#ifndef DUSK_MAIN_H
|
||||
#define DUSK_MAIN_H
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
namespace dusk {
|
||||
@@ -8,7 +12,17 @@ namespace dusk {
|
||||
extern bool IsShuttingDown;
|
||||
extern bool IsGameLaunched;
|
||||
extern bool IsFocusPaused;
|
||||
extern bool RestartRequested;
|
||||
extern std::filesystem::path ConfigPath;
|
||||
|
||||
#if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \
|
||||
(defined(TARGET_OS_TV) && TARGET_OS_TV)
|
||||
inline constexpr bool SupportsProcessRestart = false;
|
||||
#else
|
||||
inline constexpr bool SupportsProcessRestart = true;
|
||||
#endif
|
||||
|
||||
void RequestRestart() noexcept;
|
||||
}
|
||||
|
||||
#endif // DUSK_MAIN_H
|
||||
|
||||
@@ -67,7 +67,6 @@ struct UserSettings {
|
||||
// QoL
|
||||
ConfigVar<bool> enableQuickTransform;
|
||||
ConfigVar<bool> hideTvSettingsScreen;
|
||||
ConfigVar<bool> skipWarningScreen;
|
||||
ConfigVar<bool> biggerWallets;
|
||||
ConfigVar<bool> noReturnRupees;
|
||||
ConfigVar<bool> disableRupeeCutscenes;
|
||||
|
||||
+89
-122
@@ -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);
|
||||
}
|
||||
+72
-2
@@ -9,17 +9,44 @@ body {
|
||||
font-weight: normal;
|
||||
font-size: 20dp;
|
||||
color: #FFFFFF;
|
||||
background-color: #000000;
|
||||
decorator: image(../prelaunch-bg.png cover left center);
|
||||
filter: opacity(0);
|
||||
transition: filter 1s 0.2s linear-in-out;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.gradient {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* The color gradient from the Figma bands really badly. A fully black gradient does as well, but not as badly. */
|
||||
decorator: horizontal-gradient(#000000FF #00000000);
|
||||
}
|
||||
|
||||
body.mirrored .gradient {
|
||||
decorator: horizontal-gradient(#00000000 #000000FF);
|
||||
}
|
||||
|
||||
.background {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
decorator: image(../prelaunch-bg.png cover left center);
|
||||
opacity: 0;
|
||||
transition: opacity 1s linear-in-out;
|
||||
}
|
||||
|
||||
body[open] {
|
||||
filter: opacity(1);
|
||||
}
|
||||
|
||||
body[open] .background {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
body.disc-ready .background {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
content {
|
||||
display: block;
|
||||
width: 100%;
|
||||
@@ -35,6 +62,7 @@ content[open] {
|
||||
menu {
|
||||
position: absolute;
|
||||
left: 96dp;
|
||||
right: auto;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
/* Scale based on a reference screen width, 428/1216 */
|
||||
@@ -47,6 +75,11 @@ menu {
|
||||
gap: 48dp;
|
||||
}
|
||||
|
||||
body.mirrored menu {
|
||||
left: auto;
|
||||
right: 96dp;
|
||||
}
|
||||
|
||||
hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -55,6 +88,10 @@ hero {
|
||||
gap: 8dp;
|
||||
}
|
||||
|
||||
body.mirrored hero {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
hero img {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -79,6 +116,7 @@ hero img {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12dp;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#menu-list button {
|
||||
@@ -86,6 +124,7 @@ hero img {
|
||||
height: 54dp;
|
||||
padding: 8dp 16dp;
|
||||
border-radius: 8dp;
|
||||
text-align: left;
|
||||
text-transform: uppercase;
|
||||
font-family: "Fira Sans Condensed";
|
||||
font-size: 32dp;
|
||||
@@ -105,25 +144,56 @@ hero img {
|
||||
decorator: horizontal-gradient(#FEE685FF #FEE68500);
|
||||
}
|
||||
|
||||
body.mirrored #menu-list {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
body.mirrored #menu-list button {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
body.mirrored #menu-list button:hover,
|
||||
body.mirrored #menu-list button:focus-visible {
|
||||
decorator: horizontal-gradient(#FEE68500 #FEE685FF);
|
||||
}
|
||||
|
||||
disc-info {
|
||||
position: absolute;
|
||||
left: 96dp;
|
||||
right: auto;
|
||||
bottom: 72dp;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12dp;
|
||||
font-size: 24dp;
|
||||
font-effect: glow(0dp 4dp 0dp 4dp black);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
body.mirrored disc-info {
|
||||
left: auto;
|
||||
right: 96dp;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
version-info {
|
||||
position: absolute;
|
||||
right: 96dp;
|
||||
left: auto;
|
||||
bottom: 72dp;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12dp;
|
||||
text-align: right;
|
||||
font-size: 24dp;
|
||||
font-effect: glow(0dp 4dp 0dp 4dp black);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
body.mirrored version-info {
|
||||
right: auto;
|
||||
left: 96dp;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#disc-status {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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%;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,62 @@
|
||||
#include "JSystem/JGadget/define.h"
|
||||
#include <cstring>
|
||||
|
||||
#include "dusk/logging.h"
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/ui/ui.hpp"
|
||||
|
||||
namespace {
|
||||
static int sJaiSkip = -1;
|
||||
|
||||
static JSUList<JAIStream>* get_stream_list() {
|
||||
return Z2GetSoundMgr()->getStreamMgr()->getStreamList();
|
||||
}
|
||||
|
||||
static int get_stream_count(JSUList<JAIStream>* list) {
|
||||
int i = 0;
|
||||
for (JSULink<JAIStream>* l = list != nullptr ? list->getFirst() : nullptr; l != nullptr;
|
||||
l = l->getNext()) {
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
static void pause_stream(int skip_first, bool paused) {
|
||||
int i = 0;
|
||||
JSUList<JAIStream>* list = get_stream_list();
|
||||
for (JSULink<JAIStream>* l = list->getFirst(); l != nullptr; l = l->getNext(), ++i) {
|
||||
if (i >= skip_first) {
|
||||
l->getObject()->pause(paused);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void pause_streams(int skip_first) {
|
||||
if (!dusk::ui::is_prelaunch_open()) {
|
||||
return;
|
||||
}
|
||||
JSUList<JAIStream>* list = get_stream_list();
|
||||
if (list == nullptr || get_stream_count(list) <= skip_first) {
|
||||
return;
|
||||
}
|
||||
pause_stream(skip_first, true);
|
||||
sJaiSkip = skip_first;
|
||||
}
|
||||
|
||||
static void unpause_streams(bool require_prelaunch_hidden) {
|
||||
if (sJaiSkip < 0) {
|
||||
return;
|
||||
}
|
||||
if (require_prelaunch_hidden && dusk::ui::is_prelaunch_open()) {
|
||||
return;
|
||||
}
|
||||
pause_stream(sJaiSkip, false);
|
||||
sJaiSkip = -1;
|
||||
}
|
||||
} // namespace
|
||||
#endif
|
||||
|
||||
s16 dDemo_c::m_branchId = -1;
|
||||
|
||||
namespace {
|
||||
@@ -1006,7 +1062,16 @@ int dDemo_c::start(u8 const* p_data, cXyz* p_translation, f32 rotationY) {
|
||||
m_control->setSuspend(0);
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
const int existing_streams = get_stream_count(get_stream_list());
|
||||
#endif
|
||||
|
||||
m_control->forward(0);
|
||||
|
||||
#if TARGET_PC
|
||||
pause_streams(existing_streams);
|
||||
#endif
|
||||
|
||||
m_translation = p_translation;
|
||||
|
||||
if (m_translation != NULL) {
|
||||
@@ -1034,6 +1099,10 @@ static void dummyString2() {
|
||||
void dDemo_c::end() {
|
||||
JUT_ASSERT(1956, m_system != NULL);
|
||||
|
||||
#if TARGET_PC
|
||||
unpause_streams(false);
|
||||
#endif
|
||||
|
||||
m_control->destroyObject_all();
|
||||
m_object->remove();
|
||||
m_data = NULL;
|
||||
@@ -1054,6 +1123,10 @@ void dDemo_c::branch() {
|
||||
int dDemo_c::update() {
|
||||
JUT_ASSERT(2064, m_system != NULL);
|
||||
|
||||
#if TARGET_PC
|
||||
unpause_streams(true);
|
||||
#endif
|
||||
|
||||
if (m_data == NULL) {
|
||||
if (m_branchData == NULL) {
|
||||
return 0;
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
+2
-16
@@ -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;
|
||||
|
||||
@@ -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 <dusk/autosave.h>
|
||||
#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();
|
||||
|
||||
+14
-12
@@ -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 <filesystem>
|
||||
#include <algorithm>
|
||||
@@ -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<Achievement> AchievementSystem::getAchievements() const {
|
||||
std::vector<Achievement> 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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<std::string> m_notifyQueue;
|
||||
static constexpr float NOTIFY_DURATION = 4.0f;
|
||||
static constexpr float NOTIFY_FADE_TIME = 0.5f;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,282 +0,0 @@
|
||||
#include "imgui.h"
|
||||
|
||||
#include "ImGuiConfig.hpp"
|
||||
#include "ImGuiEngine.hpp"
|
||||
#include "ImGuiPreLaunchWindow.hpp"
|
||||
|
||||
#include "../file_select.hpp"
|
||||
#include "../iso_validate.hpp"
|
||||
#include "ImGuiConsole.hpp"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/settings.h"
|
||||
|
||||
#include <SDL3/SDL_dialog.h>
|
||||
#include <SDL3/SDL_filesystem.h>
|
||||
|
||||
#include "aurora/lib/internal.hpp"
|
||||
#include "aurora/lib/window.hpp"
|
||||
|
||||
namespace dusk {
|
||||
|
||||
typedef void (ImGuiPreLaunchWindow::*drawFunc)();
|
||||
|
||||
drawFunc drawTable[2] = {&ImGuiPreLaunchWindow::drawMainMenu, &ImGuiPreLaunchWindow::drawOptions};
|
||||
|
||||
static constexpr std::array<const char*, 5> skLanguageNames = {
|
||||
"English", "German", "French", "Spanish", "Italian"
|
||||
};
|
||||
|
||||
static constexpr std::array<SDL_DialogFileFilter, 2> skGameDiscFileFilters{{
|
||||
{"Game Disc Images", "iso;gcm;ciso;gcz;nfs;rvz;wbfs;wia;tgc"},
|
||||
{"All Files", "*"},
|
||||
}};
|
||||
|
||||
static std::string ShowIsoInvalidError(const iso::ValidationError code) {
|
||||
using namespace std::literals::string_literals;
|
||||
|
||||
switch (code) {
|
||||
case iso::ValidationError::IOError:
|
||||
return "Unknown IO error occurred"s;
|
||||
case iso::ValidationError::InvalidImage:
|
||||
return "Unable to interpret selected file as a disc image"s;
|
||||
case iso::ValidationError::WrongGame:
|
||||
return "Selected disc image is for a different game"s;
|
||||
case iso::ValidationError::WrongVersion:
|
||||
return "Selected disc image is for an unsupported version of the game. Only North American GameCube (NTSC/GZ2E01) is supported at this time."s;
|
||||
case iso::ValidationError::ExecutableMismatch:
|
||||
return "Selected disc image contains modified executable files."s;
|
||||
default:
|
||||
return "Unknown error"s;
|
||||
}
|
||||
}
|
||||
|
||||
static std::string_view card_type_name(CARDFileType type) {
|
||||
switch (type) {
|
||||
case CARD_GCIFOLDER:
|
||||
return "GCI Folder"sv;
|
||||
case CARD_RAWIMAGE:
|
||||
return "Card Image"sv;
|
||||
default:
|
||||
return ""sv;
|
||||
}
|
||||
}
|
||||
|
||||
void fileDialogCallback(void* userdata, const char* path, const char* error) {
|
||||
auto* self = static_cast<ImGuiPreLaunchWindow*>(userdata);
|
||||
if (error != nullptr) {
|
||||
self->m_selectedIsoPath.clear();
|
||||
self->m_errorString = fmt::format("File dialog error: {}", error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (path == nullptr) {
|
||||
self->m_selectedIsoPath.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
self->m_selectedIsoPath = path;
|
||||
self->m_isPal = iso::isPal(path);
|
||||
getSettings().backend.isoPath.setValue(self->m_selectedIsoPath);
|
||||
config::Save();
|
||||
}
|
||||
|
||||
ImGuiPreLaunchWindow::ImGuiPreLaunchWindow() = default;
|
||||
|
||||
bool ImGuiPreLaunchWindow::isSelectedPathValid() const {
|
||||
#if TARGET_ANDROID
|
||||
return !m_selectedIsoPath.empty(); // unsure why SDL_GetPathInfo doesnt work here
|
||||
#else
|
||||
return !m_selectedIsoPath.empty() && SDL_GetPathInfo(m_selectedIsoPath.c_str(), nullptr);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ImGuiPreLaunchWindow::draw() {
|
||||
if (m_IsFirstDraw) {
|
||||
m_selectedIsoPath = getSettings().backend.isoPath;
|
||||
m_isPal = !m_selectedIsoPath.empty() && iso::isPal(m_selectedIsoPath.c_str());
|
||||
m_initialGraphicsBackend = getSettings().backend.graphicsBackend;
|
||||
m_IsFirstDraw = false;
|
||||
}
|
||||
|
||||
if (isSelectedPathValid() && getSettings().backend.skipPreLaunchUI) {
|
||||
dusk::IsGameLaunched = true;
|
||||
return;
|
||||
}
|
||||
|
||||
auto& io = ImGui::GetIO();
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(io.DisplaySize.x, io.DisplaySize.y));
|
||||
ImGui::SetNextWindowPos(ImVec2(0, 0));
|
||||
ImGui::SetNextWindowBgAlpha(0.65f);
|
||||
|
||||
ImGui::Begin("Pre Launch Window", nullptr,
|
||||
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing |
|
||||
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
||||
|
||||
const auto& windowSize = ImGui::GetWindowSize();
|
||||
|
||||
for (int i = 0; i < 5; i++)
|
||||
ImGui::NewLine();
|
||||
|
||||
float iconSize = 150.f;
|
||||
ImGui::SameLine(windowSize.x / 2 - iconSize + (iconSize / 2));
|
||||
if (ImGuiEngine::orgIcon != 0) {
|
||||
ImGui::Image(ImGuiEngine::orgIcon, ImVec2{iconSize, iconSize});
|
||||
}
|
||||
ImGuiTextCenter("Twilit Realm presents");
|
||||
if (ImGuiEngine::duskLogo) {
|
||||
ImGui::NewLine();
|
||||
float width = iconSize * 2.5f;
|
||||
ImGui::SameLine(windowSize.x / 2 - width + (width / 2));
|
||||
ImGui::Image(ImGuiEngine::duskLogo, ImVec2{width, iconSize});
|
||||
} else {
|
||||
ImGui::PushFont(ImGuiEngine::fontExtraLarge);
|
||||
ImGuiTextCenter("Dusk");
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
(this->*drawTable[m_CurMenu])();
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void ImGuiPreLaunchWindow::drawMainMenu() {
|
||||
const auto& windowSize = ImGui::GetWindowSize();
|
||||
ImGui::SetCursorPosY(windowSize.y - 200);
|
||||
|
||||
ImGui::PushFont(ImGuiEngine::fontLarge);
|
||||
|
||||
if (!isSelectedPathValid()) {
|
||||
if (!m_errorString.empty()) {
|
||||
ImGuiTextCenter(m_errorString);
|
||||
}
|
||||
|
||||
if (ImGuiButtonCenter("Select disc image...")) {
|
||||
ShowFileSelect(&fileDialogCallback, this, aurora::window::get_sdl_window(),
|
||||
skGameDiscFileFilters.data(), int(skGameDiscFileFilters.size()), nullptr,
|
||||
false);
|
||||
}
|
||||
} else {
|
||||
if (ImGuiButtonCenter("Start game")) {
|
||||
dusk::IsGameLaunched = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ImGuiButtonCenter("Options")) {
|
||||
m_CurMenu = 1;
|
||||
}
|
||||
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
void ImGuiPreLaunchWindow::drawOptions() {
|
||||
const auto& windowSize = ImGui::GetWindowSize();
|
||||
|
||||
ImGui::NewLine();
|
||||
|
||||
ImGui::PushFont(ImGuiEngine::fontLarge);
|
||||
ImGuiTextCenter("Options");
|
||||
ImGui::Separator();
|
||||
ImGui::PopFont();
|
||||
|
||||
auto cursorY = ImGui::GetCursorPosY();
|
||||
float endCursorY = windowSize.y - 100;
|
||||
|
||||
float childWidth = windowSize.x - 400;
|
||||
|
||||
ImGui::SetCursorPosX(windowSize.x / 2 - (childWidth / 2));
|
||||
if (ImGui::BeginChild("OptionsChild", ImVec2(childWidth, endCursorY - cursorY),
|
||||
ImGuiChildFlags_None, ImGuiWindowFlags_NoBackground))
|
||||
{
|
||||
if (!m_errorString.empty()) {
|
||||
ImGuiTextCenter(m_errorString);
|
||||
}
|
||||
|
||||
ImGui::InputText("Game ISO Path", &m_selectedIsoPath, ImGuiInputTextFlags_ReadOnly);
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(m_selectedIsoPath == "" ? "Set" : "Change")) {
|
||||
ShowFileSelect(&fileDialogCallback, this, aurora::window::get_sdl_window(),
|
||||
skGameDiscFileFilters.data(), int(skGameDiscFileFilters.size()), nullptr,
|
||||
false);
|
||||
}
|
||||
|
||||
if (m_isPal) {
|
||||
auto selectedLanguage = getSettings().game.language.getValue();
|
||||
if (ImGui::BeginCombo("Language", skLanguageNames[static_cast<u8>(selectedLanguage)])) {
|
||||
for (u8 i = 0; i < skLanguageNames.size(); ++i) {
|
||||
if (ImGui::Selectable(skLanguageNames[i])) {
|
||||
getSettings().game.language.setValue(static_cast<GameLanguage>(i));
|
||||
config::Save();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
}
|
||||
|
||||
AuroraBackend configuredBackend = BACKEND_AUTO;
|
||||
const std::string& configuredBackendId = getSettings().backend.graphicsBackend;
|
||||
if (!try_parse_backend(configuredBackendId, configuredBackend)) {
|
||||
configuredBackend = BACKEND_AUTO;
|
||||
}
|
||||
|
||||
if (ImGui::BeginCombo("Graphics Backend", backend_name(configuredBackend).data())) {
|
||||
if (ImGui::Selectable("Auto", configuredBackend == BACKEND_AUTO)) {
|
||||
getSettings().backend.graphicsBackend.setValue("auto");
|
||||
config::Save();
|
||||
}
|
||||
|
||||
size_t backendCount = 0;
|
||||
const AuroraBackend* availableBackends = aurora_get_available_backends(&backendCount);
|
||||
for (size_t i = 0; i < backendCount; ++i) {
|
||||
const AuroraBackend backend = availableBackends[i];
|
||||
const bool isSelected = configuredBackend == backend;
|
||||
if (ImGui::Selectable(backend_name(backend).data(), isSelected)) {
|
||||
getSettings().backend.graphicsBackend.setValue(
|
||||
std::string(backend_id(backend)));
|
||||
config::Save();
|
||||
}
|
||||
if (isSelected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (configuredBackendId != m_initialGraphicsBackend) {
|
||||
ImGui::TextDisabled("Restart Required");
|
||||
}
|
||||
auto curFileType = (CARDFileType)getSettings().backend.cardFileType.getValue();
|
||||
|
||||
if (ImGui::BeginCombo("Save File Type", card_type_name(curFileType).data())) {
|
||||
|
||||
if (ImGui::Selectable("GCI Folder", curFileType == CARD_GCIFOLDER)) {
|
||||
getSettings().backend.cardFileType.setValue(CARD_GCIFOLDER);
|
||||
config::Save();
|
||||
}
|
||||
|
||||
if (ImGui::Selectable("Card Image", curFileType == CARD_RAWIMAGE)) {
|
||||
getSettings().backend.cardFileType.setValue(CARD_RAWIMAGE);
|
||||
config::Save();
|
||||
}
|
||||
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
ImGui::SetCursorPosY(endCursorY);
|
||||
ImGui::NewLine();
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::PushFont(ImGuiEngine::fontLarge);
|
||||
if (ImGuiButtonCenter("Back")) {
|
||||
m_CurMenu = 0;
|
||||
}
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
@@ -1,23 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk {
|
||||
class ImGuiPreLaunchWindow {
|
||||
private:
|
||||
int m_CurMenu = 0;
|
||||
bool m_IsFirstDraw = true;
|
||||
std::string m_initialGraphicsBackend;
|
||||
|
||||
bool isSelectedPathValid() const;
|
||||
|
||||
public:
|
||||
ImGuiPreLaunchWindow();
|
||||
void draw();
|
||||
|
||||
void drawMainMenu();
|
||||
void drawOptions();
|
||||
|
||||
std::string m_selectedIsoPath;
|
||||
std::string m_errorString;
|
||||
bool m_isPal = false;
|
||||
};
|
||||
} // namespace dusk
|
||||
+110
-7
@@ -5,17 +5,110 @@
|
||||
#endif
|
||||
|
||||
#include <aurora/main.h>
|
||||
#include "dusk/main.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#if !defined(_WIN32)
|
||||
#include <unistd.h>
|
||||
#if defined(__APPLE__)
|
||||
#include <mach-o/dyld.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
int game_main(int argc, char* argv[]);
|
||||
|
||||
namespace {
|
||||
|
||||
bool RestartProcess(int argc, char* argv[]) {
|
||||
#if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \
|
||||
(defined(TARGET_OS_TV) && TARGET_OS_TV)
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
return false;
|
||||
#elif _WIN32
|
||||
std::wstring commandLine = GetCommandLineW();
|
||||
STARTUPINFOW startupInfo{};
|
||||
startupInfo.cb = sizeof(startupInfo);
|
||||
PROCESS_INFORMATION processInfo{};
|
||||
if (!CreateProcessW(nullptr, commandLine.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr,
|
||||
&startupInfo, &processInfo))
|
||||
{
|
||||
fprintf(stderr, "Failed to restart Dusk: CreateProcessW error %lu\n", GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
CloseHandle(processInfo.hThread);
|
||||
CloseHandle(processInfo.hProcess);
|
||||
return true;
|
||||
#else
|
||||
std::filesystem::path executablePath;
|
||||
|
||||
#if defined(__APPLE__)
|
||||
uint32_t pathSize = 0;
|
||||
_NSGetExecutablePath(nullptr, &pathSize);
|
||||
if (pathSize > 0) {
|
||||
std::string path(pathSize, '\0');
|
||||
if (_NSGetExecutablePath(path.data(), &pathSize) == 0) {
|
||||
path.resize(std::strlen(path.c_str()));
|
||||
std::error_code ec;
|
||||
executablePath = std::filesystem::weakly_canonical(path, ec);
|
||||
if (ec) {
|
||||
executablePath = path;
|
||||
}
|
||||
}
|
||||
}
|
||||
#elif defined(__linux__)
|
||||
std::array<char, 4096> path{};
|
||||
const ssize_t len = readlink("/proc/self/exe", path.data(), path.size() - 1);
|
||||
if (len > 0) {
|
||||
path[static_cast<size_t>(len)] = '\0';
|
||||
executablePath = path.data();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (executablePath.empty() && argc > 0 && argv[0] != nullptr && argv[0][0] != '\0') {
|
||||
std::error_code ec;
|
||||
executablePath = std::filesystem::absolute(argv[0], ec);
|
||||
if (ec) {
|
||||
executablePath = argv[0];
|
||||
}
|
||||
}
|
||||
|
||||
if (executablePath.empty()) {
|
||||
fprintf(stderr, "Failed to restart Dusk: unable to resolve executable path\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<std::string> args;
|
||||
args.reserve(static_cast<size_t>(std::max(argc, 1)));
|
||||
args.push_back(executablePath.string());
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
args.emplace_back(argv[i] != nullptr ? argv[i] : "");
|
||||
}
|
||||
|
||||
std::vector<char*> execArgv;
|
||||
execArgv.reserve(args.size() + 1);
|
||||
for (auto& arg : args) {
|
||||
execArgv.push_back(arg.data());
|
||||
}
|
||||
execArgv.push_back(nullptr);
|
||||
|
||||
execv(executablePath.c_str(), execArgv.data());
|
||||
fprintf(stderr, "Failed to restart Dusk: execv failed: %s\n", std::strerror(errno));
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
bool ShouldShowWindowsConsole(int argc, char* argv[]) {
|
||||
if (const auto* env = std::getenv("DUSK_CONSOLE")) {
|
||||
@@ -53,19 +146,25 @@ void WindowsSetupConsole(bool showConsole) {
|
||||
SetConsoleOutputCP(CP_UTF8);
|
||||
|
||||
if (const HANDLE stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
stdoutHandle != INVALID_HANDLE_VALUE && stdoutHandle != nullptr) {
|
||||
stdoutHandle != INVALID_HANDLE_VALUE && stdoutHandle != nullptr)
|
||||
{
|
||||
DWORD consoleMode = 0;
|
||||
if (GetConsoleMode(stdoutHandle, &consoleMode)) {
|
||||
SetConsoleMode(stdoutHandle,
|
||||
consoleMode | ENABLE_PROCESSED_OUTPUT
|
||||
| ENABLE_VIRTUAL_TERMINAL_PROCESSING);
|
||||
consoleMode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int DuskMain(int argc, char* argv[]) {
|
||||
WindowsSetupConsole(ShouldShowWindowsConsole(argc, argv));
|
||||
return game_main(argc, argv);
|
||||
const int result = game_main(argc, argv);
|
||||
if constexpr (dusk::SupportsProcessRestart) {
|
||||
if (dusk::RestartRequested) {
|
||||
return RestartProcess(argc, argv) ? 0 : result;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> WideArgsToUtf8(int argc, wchar_t** argv) {
|
||||
@@ -81,8 +180,8 @@ std::vector<std::string> WideArgsToUtf8(int argc, wchar_t** argv) {
|
||||
}
|
||||
|
||||
std::vector<char> utf8Buffer(static_cast<size_t>(requiredSize));
|
||||
WideCharToMultiByte(CP_UTF8, 0, argv[i], -1, utf8Buffer.data(), requiredSize, nullptr,
|
||||
nullptr);
|
||||
WideCharToMultiByte(
|
||||
CP_UTF8, 0, argv[i], -1, utf8Buffer.data(), requiredSize, nullptr, nullptr);
|
||||
utf8Args.emplace_back(utf8Buffer.data());
|
||||
}
|
||||
|
||||
@@ -109,7 +208,11 @@ int RunWindowsGuiEntryPoint() {
|
||||
}
|
||||
#else
|
||||
int DuskMain(int argc, char* argv[]) {
|
||||
return game_main(argc, argv);
|
||||
const int result = game_main(argc, argv);
|
||||
if (dusk::RestartRequested && RestartProcess(argc, argv)) {
|
||||
return 0;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -59,16 +59,6 @@ void Component::set_disabled(bool value) {
|
||||
}
|
||||
}
|
||||
|
||||
Rml::Element* Component::append(Rml::Element* parent, const Rml::String& tag) {
|
||||
if (parent == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
auto* doc = parent->GetOwnerDocument();
|
||||
if (doc == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return parent->AppendChild(doc->CreateElement(tag));
|
||||
}
|
||||
void Component::listen(Rml::Element* element, Rml::EventId event,
|
||||
ScopedEventListener::Callback callback, bool capture) {
|
||||
if (element == nullptr) {
|
||||
|
||||
@@ -47,7 +47,6 @@ public:
|
||||
Rml::Element* root() const { return mRoot; }
|
||||
|
||||
protected:
|
||||
static Rml::Element* append(Rml::Element* parent, const Rml::String& tag);
|
||||
void clear_children();
|
||||
|
||||
Rml::Element* mRoot = nullptr;
|
||||
|
||||
@@ -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<s32>(i); },
|
||||
})
|
||||
.on_pressed([this, port, i] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
cancel_pending_binding();
|
||||
PADSetPortForIndex(i, port);
|
||||
PADSerializeMappings();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+76
-17
@@ -185,7 +185,10 @@ void populate_stage_picker(Pane& pane, std::function<Rml::String()> 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<ToggleEntry>& 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<ToggleEntry>& 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<u8>(itemId)); });
|
||||
.on_pressed([slot, itemId] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
dComIfGs_setItem(slot, static_cast<u8>(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<u8>(itemId)); },
|
||||
})
|
||||
.on_pressed([itemId] { toggle_item_first_bit(static_cast<u8>(itemId)); });
|
||||
.on_pressed([itemId] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
toggle_item_first_bit(static_cast<u8>(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::array<u8, Si
|
||||
.text = get_item_name(id),
|
||||
.isSelected = [id, &equip] { return equip == id; },
|
||||
})
|
||||
.on_pressed([id, &equip] { equip = id; });
|
||||
.on_pressed([id, &equip] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
equip = id;
|
||||
});
|
||||
};
|
||||
addOption(dItemNo_NONE_e);
|
||||
for (const auto item : entries) {
|
||||
@@ -985,7 +1019,10 @@ void populate_wallet_picker(Pane& pane) {
|
||||
.text = walletSizeNames[i],
|
||||
.isSelected = [i] { return get_player_status()->getWalletSize() == 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 <size_t Size>
|
||||
@@ -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>(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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
#include "graphics_tuner.hpp"
|
||||
|
||||
#include "Z2AudioLib/Z2SeMgr.h"
|
||||
#include "m_Do/m_Do_audio.h"
|
||||
|
||||
#include <dolphin/gx/GXAurora.h>
|
||||
#include <dolphin/vi.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/settings.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
<head>
|
||||
<link type="text/rcss" href="res/rml/tuner.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" class="tuner-root">
|
||||
<div class="tuner">
|
||||
<div class="header">
|
||||
<div id="title"></div>
|
||||
<div id="carousel-container" class="carousel-container"></div>
|
||||
</div>
|
||||
<div id="description" class="description"></div>
|
||||
<div class="divider"></div>
|
||||
<div id="footer" class="footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</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<int>(getSettings().game.bloomMode.getValue());
|
||||
case GraphicsOption::BloomMultiplier:
|
||||
return std::clamp(
|
||||
static_cast<int>(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<float>(value));
|
||||
break;
|
||||
case GraphicsOption::ShadowResolution:
|
||||
getSettings().game.shadowResolutionMultiplier.setValue(value);
|
||||
break;
|
||||
case GraphicsOption::BloomMode:
|
||||
getSettings().game.bloomMode.setValue(static_cast<BloomMode>(std::clamp(
|
||||
value, static_cast<int>(BloomMode::Off), static_cast<int>(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<BloomMode>(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<SteppedCarousel>(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<Button>(footer, "\xE2\x86\x90 Return", "footer-button")
|
||||
.on_pressed([this] { pop(); });
|
||||
returnButton.root()->SetClass("return", true);
|
||||
auto& resetButton =
|
||||
add_component<Button>(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
|
||||
@@ -0,0 +1,90 @@
|
||||
#pragma once
|
||||
|
||||
#include "button.hpp"
|
||||
#include "component.hpp"
|
||||
#include "document.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class SteppedCarousel : public Component {
|
||||
public:
|
||||
struct Props {
|
||||
int min = 0;
|
||||
int max = 0;
|
||||
int step = 1;
|
||||
std::function<int()> getValue;
|
||||
std::function<void(int)> onChange;
|
||||
std::function<Rml::String(int)> 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 <typename T, typename... Args>
|
||||
requires std::is_base_of_v<Component, T> T& add_component(Args&&... args) {
|
||||
auto child = std::make_unique<T>(std::forward<Args>(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<std::unique_ptr<Component> > mComponents;
|
||||
Rml::Element* mRoot;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "popup.hpp"
|
||||
#include "menu_bar.hpp"
|
||||
|
||||
#include <RmlUi/Core.h>
|
||||
|
||||
@@ -30,16 +30,20 @@ const Rml::String kDocumentSource = R"RML(
|
||||
<link type="text/rcss" href="res/rml/popup.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<popup id="popup"></popup>
|
||||
<popup id="popup" />
|
||||
</body>
|
||||
</rml>
|
||||
)RML";
|
||||
|
||||
}
|
||||
|
||||
Popup::Popup() : Document(kDocumentSource), mRoot(mDocument->GetElementById("popup")) {
|
||||
MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById("popup")) {
|
||||
mTabBar = std::make_unique<TabBar>(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<SettingsWindow>()); });
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -0,0 +1,75 @@
|
||||
#include "modal.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
Modal::Modal(Props props)
|
||||
: WindowSmall("modal", "modal-dialog"), mProps(std::move(props)) {
|
||||
auto* title = append(mDialog, "div");
|
||||
title->SetClass("preset-title", true);
|
||||
title->SetInnerRML(mProps.title);
|
||||
|
||||
auto* body = append(mDialog, "div");
|
||||
body->SetClass("preset-intro", true);
|
||||
body->SetInnerRML(mProps.bodyRml);
|
||||
|
||||
auto* actions = append(mDialog, "div");
|
||||
actions->SetClass("modal-actions", true);
|
||||
|
||||
for (auto& action : mProps.actions) {
|
||||
auto btn = std::make_unique<Button>(actions, action.label);
|
||||
btn->root()->SetClass("modal-btn", true);
|
||||
btn->on_pressed([this, callback = std::move(action.onPressed)] {
|
||||
if (callback) {
|
||||
callback(*this);
|
||||
}
|
||||
});
|
||||
mButtons.push_back(std::move(btn));
|
||||
}
|
||||
}
|
||||
|
||||
bool Modal::focus() {
|
||||
if (!mButtons.empty()) {
|
||||
return mButtons.front()->focus();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void Modal::dismiss() {
|
||||
if (mProps.onDismiss) {
|
||||
mProps.onDismiss(*this);
|
||||
return;
|
||||
}
|
||||
pop();
|
||||
}
|
||||
|
||||
bool Modal::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
if (cmd == NavCommand::Cancel || cmd == NavCommand::Menu) {
|
||||
mDoAud_seStartMenu(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<int>(mButtons.size()); ++i) {
|
||||
if (mButtons[i]->contains(target)) {
|
||||
const int next = i + direction;
|
||||
if (next >= 0 && next < static_cast<int>(mButtons.size()) && mButtons[next]->focus()) {
|
||||
mDoAud_seStartMenu(kSoundItemFocus);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "button.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
class Modal;
|
||||
|
||||
struct ModalAction {
|
||||
Rml::String label;
|
||||
std::function<void(Modal&)> onPressed;
|
||||
};
|
||||
|
||||
class Modal : public WindowSmall {
|
||||
public:
|
||||
struct Props {
|
||||
Rml::String title;
|
||||
Rml::String bodyRml;
|
||||
std::vector<ModalAction> actions;
|
||||
std::function<void(Modal&)> onDismiss;
|
||||
};
|
||||
|
||||
explicit Modal(Props props);
|
||||
|
||||
bool focus() override;
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
private:
|
||||
void dismiss();
|
||||
|
||||
Props mProps;
|
||||
std::vector<std::unique_ptr<Button> > mButtons;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+74
-237
@@ -1,20 +1,15 @@
|
||||
#include "overlay.hpp"
|
||||
|
||||
#include "Z2AudioLib/Z2SeMgr.h"
|
||||
#include "m_Do/m_Do_audio.h"
|
||||
|
||||
#include <dolphin/gx/GXAurora.h>
|
||||
#include <dolphin/vi.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/settings.h"
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "magic_enum.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
|
||||
#include "dusk/achievements.h"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
aurora::Module Log{"dusk::ui::overlay"};
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
@@ -22,259 +17,101 @@ const Rml::String kDocumentSource = R"RML(
|
||||
<link type="text/rcss" href="res/rml/overlay.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root" class="overlay-root">
|
||||
<div class="overlay">
|
||||
<div class="header">
|
||||
<div id="title"></div>
|
||||
<div id="carousel-container" class="carousel-container"></div>
|
||||
</div>
|
||||
<div id="description" class="description"></div>
|
||||
<div class="divider"></div>
|
||||
<div id="footer" class="footer"></div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</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<int>(getSettings().game.bloomMode.getValue());
|
||||
case GraphicsOption::BloomMultiplier:
|
||||
return std::clamp(
|
||||
static_cast<int>(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<float>(value));
|
||||
break;
|
||||
case GraphicsOption::ShadowResolution:
|
||||
getSettings().game.shadowResolutionMultiplier.setValue(value);
|
||||
break;
|
||||
case GraphicsOption::BloomMode:
|
||||
getSettings().game.bloomMode.setValue(static_cast<BloomMode>(std::clamp(
|
||||
value, static_cast<int>(BloomMode::Off), static_cast<int>(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<BloomMode>(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<SteppedCarousel>(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<Button>(footer, "\xE2\x86\x90 Return", "footer-button")
|
||||
.on_pressed([this] { pop(); });
|
||||
returnButton.root()->SetClass("return", true);
|
||||
auto& resetButton =
|
||||
add_component<Button>(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<float>(toast.duration).count();
|
||||
const float elapsed =
|
||||
std::chrono::duration<float>(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
|
||||
|
||||
+4
-71
@@ -1,90 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
#include "button.hpp"
|
||||
#include "component.hpp"
|
||||
#include "document.hpp"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class SteppedCarousel : public Component {
|
||||
public:
|
||||
struct Props {
|
||||
int min = 0;
|
||||
int max = 0;
|
||||
int step = 1;
|
||||
std::function<int()> getValue;
|
||||
std::function<void(int)> onChange;
|
||||
std::function<Rml::String(int)> 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 <typename T, typename... Args>
|
||||
requires std::is_base_of_v<Component, T> T& add_component(Args&&... args) {
|
||||
auto child = std::make_unique<T>(std::forward<Args>(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<std::unique_ptr<Component> > mComponents;
|
||||
Rml::Element* mRoot;
|
||||
Rml::Element* mCurrentToast = nullptr;
|
||||
clock::time_point mCurrentToastStartTime;
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+168
-34
@@ -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 <SDL3/SDL_dialog.h>
|
||||
#include <SDL3/SDL_filesystem.h>
|
||||
#include <aurora/lib/window.hpp>
|
||||
|
||||
#include "m_Do/m_Do_MemCard.h"
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
@@ -20,6 +23,8 @@ const Rml::String kDocumentSource = R"RML(
|
||||
<link type="text/rcss" href="res/rml/prelaunch.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="gradient" />
|
||||
<div class="background" />
|
||||
<content id="root" open>
|
||||
<menu>
|
||||
<hero class="intro-item delay-0">
|
||||
@@ -49,6 +54,23 @@ constexpr std::array<SDL_DialogFileFilter, 2> kDiscFileFilters{{
|
||||
{"All Files", "*"},
|
||||
}};
|
||||
|
||||
static std::string get_error_msg(iso::ValidationError error) {
|
||||
switch (error) {
|
||||
case iso::ValidationError::IOError:
|
||||
return "Unable to read the selected file.";
|
||||
case iso::ValidationError::InvalidImage:
|
||||
return "The selected file is not a valid disc image.";
|
||||
case iso::ValidationError::WrongGame:
|
||||
return "The selected game is not supported by Dusk.";
|
||||
case iso::ValidationError::WrongVersion:
|
||||
return "Dusk currently supports GameCube USA and PAL disc images only.";
|
||||
case iso::ValidationError::Success:
|
||||
return "The selected disc image is valid.";
|
||||
default:
|
||||
return "The selected disc image could not be validated.";
|
||||
}
|
||||
}
|
||||
|
||||
void file_dialog_callback(void*, const char* path, const char* error) {
|
||||
auto& state = prelaunch_state();
|
||||
if (error != nullptr) {
|
||||
@@ -58,14 +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<Button>(menuList, hasValidPath ? "Start Game" : "Select Disc Image"));
|
||||
auto& state = prelaunch_state();
|
||||
mMenuButtons.push_back(std::make_unique<Button>(
|
||||
menuList, state.selectedDiscIsValid ? "Play" : "Select Disc Image"));
|
||||
mMenuButtons.back()->on_pressed([this] {
|
||||
if (!is_selected_path_valid()) {
|
||||
if (!prelaunch_state().selectedDiscIsValid) {
|
||||
open_iso_picker();
|
||||
return;
|
||||
}
|
||||
|
||||
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<dusk::ui::PresetWindow>());
|
||||
}
|
||||
hide(true);
|
||||
});
|
||||
apply_intro_animation(mMenuButtons.back()->root(), "delay-1");
|
||||
|
||||
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Options"));
|
||||
mMenuButtons.back()->on_pressed([this] { push(std::make_unique<PrelaunchOptions>()); });
|
||||
mMenuButtons.back()->on_pressed([this] {
|
||||
mRestartSuppressed = false;
|
||||
push(std::make_unique<SettingsWindow>(true));
|
||||
});
|
||||
apply_intro_animation(mMenuButtons.back()->root(), "delay-2");
|
||||
|
||||
mMenuButtons.push_back(std::make_unique<Button>(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<ModalAction> 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>(Modal::Props{
|
||||
.title = "Apply Options",
|
||||
.bodyRml =
|
||||
dusk::SupportsProcessRestart ?
|
||||
"A restart is required to apply selected options.<br/><br/>Restart now to "
|
||||
"apply them immediately?" :
|
||||
"A restart is required to apply selected options.<br/><br/>Close and reopen "
|
||||
"Dusk to apply them.",
|
||||
.actions = std::move(actions),
|
||||
.onDismiss = 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>(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;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ protected:
|
||||
|
||||
private:
|
||||
bool mEntranceAnimationStarted = false;
|
||||
bool mRestartSuppressed = false;
|
||||
std::vector<std::unique_ptr<Button>> 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
|
||||
|
||||
@@ -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<const char*, 5> 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<AuroraBackend> available_backends() {
|
||||
std::vector<AuroraBackend> 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<u8>(lang);
|
||||
if (value >= kLanguageNames.size()) {
|
||||
getSettings().game.language.setValue(GameLanguage::English);
|
||||
config::Save();
|
||||
value = static_cast<u8>(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<int>(kLanguageNames.size());
|
||||
int idx = static_cast<int>(getSettings().game.language.getValue());
|
||||
const int dir = (cmd == NavCommand::Left) ? -1 : 1;
|
||||
idx = ((idx + dir) % n + n) % n;
|
||||
getSettings().game.language.setValue(static_cast<GameLanguage>(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<int>(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<size_t>(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<size_t>(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<CARDFileType>(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<CARDFileType>(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<Pane>(content, Pane::Type::Controlled);
|
||||
leftPane.add_child<LanguageSelect>();
|
||||
leftPane.add_child<BackendSelect>();
|
||||
leftPane.add_child<SaveTypeSelect>();
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
@@ -1,12 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "window.hpp"
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class PrelaunchOptions : public Window {
|
||||
public:
|
||||
PrelaunchOptions();
|
||||
};
|
||||
|
||||
} // namespace dusk::ui
|
||||
+7
-54
@@ -1,10 +1,8 @@
|
||||
#include "preset.hpp"
|
||||
|
||||
#include "Z2AudioLib/Z2SeMgr.h"
|
||||
#include "button.hpp"
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/settings.h"
|
||||
#include "m_Do/m_Do_audio.h"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <dolphin/gx/GXAurora.h>
|
||||
@@ -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>
|
||||
<head>
|
||||
<link type="text/rcss" href="res/rml/window.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<window id="window" class="small preset">
|
||||
<div id="preset-dialog" class="preset-dialog"></div>
|
||||
</window>
|
||||
</body>
|
||||
</rml>
|
||||
)RML";
|
||||
|
||||
} // namespace
|
||||
|
||||
PresetWindow::PresetWindow()
|
||||
: Document(kDocumentSource), mRoot(mDocument->GetElementById("window")) {
|
||||
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") &&
|
||||
Document::visible()) {
|
||||
Document::hide(mPendingClose);
|
||||
}
|
||||
});
|
||||
|
||||
auto* dialog = mDocument->GetElementById("preset-dialog");
|
||||
|
||||
auto* title = createElement(dialog, "div");
|
||||
PresetWindow::PresetWindow() : WindowSmall("preset", "preset-dialog") {
|
||||
auto* title = append(mDialog, "div");
|
||||
title->SetClass("preset-title", true);
|
||||
title->SetInnerRML("Welcome to Dusk!");
|
||||
|
||||
auto* intro = createElement(dialog, "div");
|
||||
auto* intro = append(mDialog, "div");
|
||||
intro->SetClass("preset-intro", true);
|
||||
intro->SetInnerRML(
|
||||
"Choose a preset to get started.<br/>"
|
||||
"You can change any setting later from the Settings menu.");
|
||||
|
||||
auto* grid = createElement(dialog, "div");
|
||||
auto* grid = append(mDialog, "div");
|
||||
grid->SetClass("preset-grid", true);
|
||||
|
||||
struct PresetInfo {
|
||||
@@ -112,7 +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<Button>(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<int>(mButtons.size())) {
|
||||
if (mButtons[next]->focus()) {
|
||||
mDoAud_seStartMenu(Z2SE_SY_NAME_CURSOR);
|
||||
mDoAud_seStartMenu(kSoundItemFocus);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,22 @@
|
||||
#pragma once
|
||||
#include "component.hpp"
|
||||
#include "document.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::ui {
|
||||
|
||||
class PresetWindow : public Document {
|
||||
class PresetWindow : public WindowSmall {
|
||||
public:
|
||||
PresetWindow();
|
||||
|
||||
void show() override;
|
||||
void hide(bool close) override;
|
||||
bool visible() const override;
|
||||
bool focus() override;
|
||||
|
||||
protected:
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
|
||||
private:
|
||||
Rml::Element* mRoot = nullptr;
|
||||
std::vector<std::unique_ptr<Component>> mButtons;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+320
-35
@@ -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 <algorithm>
|
||||
|
||||
#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<AuroraBackend> available_backends() {
|
||||
std::vector<AuroraBackend> 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<f
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void overlay_control(
|
||||
Window& window, Pane& leftPane, Pane& rightPane, ConfigVar<T>& var, const OverlayProps& props) {
|
||||
void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, ConfigVar<T>& 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<Overlay>(props));
|
||||
window.push(std::make_unique<GraphicsTuner>(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<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(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.<br/><br/>"
|
||||
"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<u8>(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<GameLanguage>(i);
|
||||
},
|
||||
})
|
||||
.on_pressed([i] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().game.language.setValue(static_cast<GameLanguage>(i));
|
||||
config::Save();
|
||||
});
|
||||
}
|
||||
pane.add_rml("<br/>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("<br/>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<Pane>(content, Pane::Type::Controlled);
|
||||
auto& rightPane = add_child<Pane>(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<int>(BloomMode::Dusk),
|
||||
.defaultValue = static_cast<int>(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>(Modal::Props{
|
||||
.title = "Invalid disc image",
|
||||
.bodyRml = state.errorString,
|
||||
.actions =
|
||||
{
|
||||
ModalAction{
|
||||
.label = "OK",
|
||||
.onPressed = dismissInvalidDisc,
|
||||
},
|
||||
},
|
||||
.onDismiss = dismissInvalidDisc,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
Window::update();
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
@@ -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
|
||||
@@ -47,7 +47,6 @@ TabBar::TabBar(Rml::Element* parent, Props props)
|
||||
add_child<Button>(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>(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;
|
||||
|
||||
+58
-12
@@ -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<std::unique_ptr<Document> > sDocuments;
|
||||
std::vector<std::unique_ptr<Document> > sDocumentStack;
|
||||
// Documents that don't participate in the focus stack
|
||||
std::vector<std::unique_ptr<Document> > sPassiveDocuments;
|
||||
std::deque<Toast> 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<Document> doc, bool show) noexcept {
|
||||
Document& push_document(std::unique_ptr<Document> 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<const Prelaunch*>(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<Rml::Input::KeyIdentifier>(
|
||||
event.GetParameter<int>("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<Toast>& get_toasts() noexcept {
|
||||
return sToasts;
|
||||
}
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
+44
-4
@@ -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<Document> doc, bool show = true) noexcept;
|
||||
Document& push_document(
|
||||
std::unique_ptr<Document> 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> 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<Toast>& get_toasts() noexcept;
|
||||
|
||||
} // namespace dusk::ui
|
||||
|
||||
+66
-8
@@ -39,11 +39,24 @@ const Rml::String kDocumentSource = R"RML(
|
||||
</rml>
|
||||
)RML";
|
||||
|
||||
const Rml::String kDocumentSourceSmall = R"RML(
|
||||
<rml>
|
||||
<head>
|
||||
<link type="text/rcss" href="res/rml/window.rcss" />
|
||||
</head>
|
||||
<body>
|
||||
<window id="window" class="small">
|
||||
<div id="dialog"/>
|
||||
</window>
|
||||
</body>
|
||||
</rml>
|
||||
)RML";
|
||||
|
||||
} // namespace
|
||||
|
||||
Window::Window() : Document(kDocumentSource), mRoot(mDocument->GetElementById("window")) {
|
||||
mTabBar = std::make_unique<TabBar>(mRoot, TabBar::Props{
|
||||
.onClose = [this] { pop(); },
|
||||
.onClose = [this] { request_close(); },
|
||||
.selectedTabIndex = 0,
|
||||
.autoSelect = true,
|
||||
});
|
||||
@@ -68,7 +81,9 @@ Window::Window() : Document(kDocumentSource), mRoot(mDocument->GetElementById("w
|
||||
|
||||
// Hide document after transition completion
|
||||
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
|
||||
if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") && Document::visible()) {
|
||||
if (event.GetTargetElement() == mRoot && !mRoot->HasAttribute("open") &&
|
||||
Document::visible())
|
||||
{
|
||||
Document::hide(mPendingClose);
|
||||
}
|
||||
});
|
||||
@@ -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
|
||||
@@ -31,12 +31,15 @@ public:
|
||||
bool set_active_tab(int index);
|
||||
|
||||
protected:
|
||||
void request_close();
|
||||
virtual bool consume_close_request();
|
||||
void add_tab(const Rml::String& title, TabBuilder builder);
|
||||
void refresh_active_tab();
|
||||
void update_safe_area() noexcept;
|
||||
void clear_content() noexcept;
|
||||
bool handle_nav_command(Rml::Event& event, NavCommand cmd) override;
|
||||
bool handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept;
|
||||
bool mSuppressNavFallback = false;
|
||||
|
||||
template <typename T, typename... Args>
|
||||
requires std::is_base_of_v<Component, T> T& add_child(Args&&... args) {
|
||||
@@ -51,6 +54,21 @@ protected:
|
||||
std::unique_ptr<TabBar> mTabBar;
|
||||
std::vector<std::unique_ptr<Component> > 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
#if TARGET_PC
|
||||
#include <assert.h>
|
||||
#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;
|
||||
}
|
||||
|
||||
+49
-18
@@ -46,6 +46,7 @@
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
#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<dusk::ui::Overlay>(), true, true);
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::MenuBar>(), false);
|
||||
|
||||
// Invalidate a bad saved isoPath so that Dusk can't get blocked from starting up
|
||||
const std::string p = dusk::getSettings().backend.isoPath;
|
||||
if (!p.empty() && dusk::iso::validate(p.c_str()) != dusk::iso::ValidationError::Success) {
|
||||
dusk::getSettings().backend.isoPath.setValue("");
|
||||
}
|
||||
|
||||
std::string dvd_path;
|
||||
bool dvd_opened = false;
|
||||
if (parsed_arg_options.count("dvd")) {
|
||||
dvd_path = parsed_arg_options["dvd"].as<std::string>();
|
||||
DuskLog.info("Loading DVD image from command line: {}", dvd_path);
|
||||
dvd_opened = aurora_dvd_open(dvd_path.c_str());
|
||||
if (!dvd_opened) {
|
||||
DuskLog.warn("Failed to open DVD image from command line: {}, opening prelaunch UI", dvd_path);
|
||||
if (dusk::iso::validate(dvd_path.c_str()) == dusk::iso::ValidationError::Success) {
|
||||
DuskLog.info("Loading DVD image from command line: {}", dvd_path);
|
||||
dvd_opened = aurora_dvd_open(dvd_path.c_str());
|
||||
if (!dvd_opened) {
|
||||
DuskLog.warn("Failed to open DVD image from command line: {}, opening prelaunch UI", dvd_path);
|
||||
} else {
|
||||
dusk::getSettings().backend.isoPath.setValue(dvd_path);
|
||||
dusk::config::Save();
|
||||
dusk::IsGameLaunched = true;
|
||||
}
|
||||
} else {
|
||||
dusk::getSettings().backend.isoPath.setValue(dvd_path);
|
||||
dusk::config::Save();
|
||||
dusk::IsGameLaunched = true;
|
||||
DuskLog.warn("DVD image from command line failed verification: {}, opening prelaunch UI", dvd_path);
|
||||
}
|
||||
}
|
||||
|
||||
if (!dvd_opened) {
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::Prelaunch>(), true);
|
||||
if (!dusk::getSettings().backend.skipPreLaunchUI) {
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::Prelaunch>(), true);
|
||||
|
||||
// pre game launch ui main loop
|
||||
if (!launchUILoop()) {
|
||||
dusk::ShutdownCrashReporting();
|
||||
// pre game launch ui main loop
|
||||
if (!launchUILoop()) {
|
||||
dusk::ShutdownCrashReporting();
|
||||
dusk::ShutdownFileLogging();
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
#ifdef DUSK_DISCORD
|
||||
dusk::discord::shutdown();
|
||||
dusk::discord::shutdown();
|
||||
#endif
|
||||
dusk::ui::shutdown();
|
||||
aurora_shutdown();
|
||||
return 0;
|
||||
dusk::ui::shutdown();
|
||||
aurora_shutdown();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
dvd_path = dusk::getSettings().backend.isoPath;
|
||||
@@ -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<dusk::ui::Popup>(), false);
|
||||
if (!dusk::getSettings().backend.wasPresetChosen) {
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::PresetWindow>());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user