mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-07 12:03:24 -04:00
New UI: Achievement toasts (#690)
* Rename Overlay -> GraphicsTuner, Popup -> MenuBar * Update GraphicsTuner CSS * WIP Overlay document & toasts * Achievement toasts * Cleanup
This commit is contained in:
+4
-2
@@ -1473,6 +1473,8 @@ set(DUSK_FILES
|
||||
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
|
||||
@@ -1484,8 +1486,8 @@ 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/preset.cpp
|
||||
|
||||
@@ -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
|
||||
|
||||
+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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
+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))
|
||||
@@ -338,7 +327,6 @@ namespace dusk {
|
||||
m_menuTools.ShowSaveEditor();
|
||||
m_menuTools.ShowStateShare();
|
||||
}
|
||||
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.
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,14 +30,14 @@ 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] {
|
||||
@@ -72,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);
|
||||
@@ -81,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) {
|
||||
@@ -89,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;
|
||||
}
|
||||
@@ -132,11 +132,11 @@ 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;
|
||||
}
|
||||
@@ -148,7 +148,7 @@ bool Popup::handle_nav_command(Rml::Event& event, NavCommand cmd) {
|
||||
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;
|
||||
+74
-239
@@ -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,261 +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(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 "";
|
||||
}
|
||||
|
||||
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] {
|
||||
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);
|
||||
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() {
|
||||
Document::show();
|
||||
mRoot->SetAttribute("open", "");
|
||||
mDoAud_seStartMenu(kSoundWindowOpen);
|
||||
}
|
||||
|
||||
void Overlay::hide(bool close) {
|
||||
mRoot->RemoveAttribute("open");
|
||||
if (close) {
|
||||
mPendingClose = true;
|
||||
mDoAud_seStartMenu(kSoundWindowClose);
|
||||
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
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "modal.hpp"
|
||||
#include "popup.hpp"
|
||||
#include "preset.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "version.h"
|
||||
|
||||
+14
-12
@@ -8,9 +8,9 @@
|
||||
#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"
|
||||
@@ -256,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({
|
||||
@@ -278,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;
|
||||
@@ -473,8 +473,9 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
});
|
||||
|
||||
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,
|
||||
@@ -482,8 +483,9 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.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,
|
||||
@@ -493,8 +495,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
});
|
||||
|
||||
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,
|
||||
@@ -502,8 +504,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.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,
|
||||
|
||||
+38
-12
@@ -21,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
|
||||
|
||||
@@ -46,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();
|
||||
}
|
||||
@@ -70,19 +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(sDocuments.begin(), sDocuments.end(), [](const auto& doc) {
|
||||
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();
|
||||
}
|
||||
@@ -92,21 +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->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;
|
||||
}
|
||||
@@ -221,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
|
||||
|
||||
+17
-2
@@ -12,7 +12,15 @@
|
||||
|
||||
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;
|
||||
@@ -39,6 +47,9 @@ 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;
|
||||
float right = 0.0f;
|
||||
@@ -57,7 +68,8 @@ 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;
|
||||
@@ -70,4 +82,7 @@ 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
|
||||
|
||||
+11
-2
@@ -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"
|
||||
@@ -57,8 +58,10 @@
|
||||
#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"
|
||||
#include "version.h"
|
||||
|
||||
@@ -281,6 +284,7 @@ void main01(void) {
|
||||
dusk::gyro::read(pacing.sim_pace);
|
||||
fapGm_Execute();
|
||||
mDoAud_Execute();
|
||||
dusk::AchievementSystem::get().tick();
|
||||
dusk::game_clock::commit_sim_tick();
|
||||
}
|
||||
}
|
||||
@@ -597,7 +601,8 @@ int game_main(int argc, char* argv[]) {
|
||||
dusk::audio::EnableHrtf = dusk::getSettings().audio.enableHrtf;
|
||||
|
||||
dusk::ui::initialize();
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::Popup>(), false);
|
||||
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;
|
||||
@@ -659,6 +664,10 @@ int game_main(int argc, char* argv[]) {
|
||||
dusk::IsGameLaunched = true;
|
||||
}
|
||||
|
||||
if (!dusk::getSettings().backend.wasPresetChosen) {
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::PresetWindow>());
|
||||
}
|
||||
|
||||
dusk::version::init();
|
||||
LanguageInit();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user