Merge branch 'main' into debug-fly-cam

This commit is contained in:
Pheenoh
2026-05-04 19:26:31 -06:00
60 changed files with 2979 additions and 1403 deletions
+27 -16
View File
@@ -1,37 +1,48 @@
![DuskLogo](res/logo-mascot.png)
<div align="center">
<img src="res/logo-mascot.png" alt="Logo" width="640">
- ### **[Official Website](https://twilitrealm.dev)**
- ### **[Discord](https://discord.gg/QACynxeyna)**
<p align="center">
<a href="https://twilitrealm.dev">Official Website</a>
<a href="https://discord.gg/QACynxeyna">Discord</a>
</p>
</div>
# Overview
Dusk is a reverse-engineered reimplementation of Twilight Princess.
It aims to be as accurate as possible to the original while also providing new options, enhancements, and tools to customize your experience.
# Setup
**⚠️ Dusk does NOT provide any copyrighted assets. You must provide your own copy of the game.**
### 1. Verify your ROM dump
First make sure your dump of the game is clean and supported by Dusk. You can do this by checking the sha1 hash of your dump against this list of supported versions.
> [!IMPORTANT]
> Dusk does *not* provide any copyrighted assets. You must provide your own copy of the original game.
| Version | sha1 hash |
|--------------| ---------------------------------------- |
| GameCube USA | 75edd3ddff41f125d1b4ce1a40378f1b565519e7 |
| GameCube PAL | 2601822a488eeb86fb89db16ca8f29c2c953e1ca |
### 1. Verify your dump
First, make sure your dump of the game is clean and supported by Dusk. You can do this by checking the SHA-1 hash of your dump against this list of supported versions:
| Version | SHA-1 hash |
|--------------| ------------------------------------------ |
| GameCube USA | `75edd3ddff41f125d1b4ce1a40378f1b565519e7` |
| GameCube EUR | `2601822a488eeb86fb89db16ca8f29c2c953e1ca` |
### 2. Download [Dusk](https://github.com/TwilitRealm/dusk/releases)
### 3. Setup the game
- Extract the zip folder
- Launch Dusk
- Select Options, then set the ISO Path to your supported game dump
- Press Start Game to play!
![Dusk options](assets/dusk_options.png)
- Extract the .zip file
- Launch Dusk
- Press **Select Disc Image**, navigate to your game dump, and select the file
- Press **Start Game** to play!
# Building
If you'd like to build Dusk from source, please read the [build instructions](docs/building.md).
Pull Requests are welcomed! Note that we do not accept contributions that are primarily AI generated and will close your PR if we suspect as much.
Pull requests are welcomed! Note that we do not accept contributions that are primarily AI-generated and will close your PR if we suspect as much.
# 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).
Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

+1 -1
+6 -4
View File
@@ -1449,8 +1449,6 @@ set(DUSK_FILES
src/dusk/imgui/ImGuiMenuTools.hpp
src/dusk/imgui/ImGuiPreLaunchWindow.cpp
src/dusk/imgui/ImGuiPreLaunchWindow.hpp
src/dusk/imgui/ImGuiFirstRunPreset.hpp
src/dusk/imgui/ImGuiFirstRunPreset.cpp
src/dusk/imgui/ImGuiProcessOverlay.cpp
src/dusk/imgui/ImGuiCameraOverlay.cpp
src/dusk/imgui/ImGuiHeapOverlay.cpp
@@ -1461,16 +1459,20 @@ set(DUSK_FILES
src/dusk/imgui/ImGuiSaveEditor.cpp
src/dusk/imgui/ImGuiStateShare.hpp
src/dusk/imgui/ImGuiStateShare.cpp
src/dusk/imgui/ImGuiAchievements.hpp
src/dusk/imgui/ImGuiAchievements.cpp
src/dusk/ui/bool_button.cpp
src/dusk/ui/bool_button.hpp
src/dusk/ui/button.cpp
src/dusk/ui/button.hpp
src/dusk/ui/component.cpp
src/dusk/ui/component.hpp
src/dusk/ui/controller_config.cpp
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
+11
View File
@@ -25,6 +25,10 @@ public:
int Draw();
int Delete();
#if TARGET_PC
void onInterpCallback();
#endif
enum Param_e {
LOCK_e = (1 << 6), NO_BASE_DISP = (1 << 7)
};
@@ -50,6 +54,13 @@ private:
/* 0x1020 */ dCcD_Cyl mCylinderCollider;
/* 0x115C */ s32 mStopSwingingFrames;
#if TARGET_PC
cXyz mChainInterpPrev[64];
cXyz mChainInterpCurr[64];
bool mChainInterpPrevValid;
bool mChainInterpCurrValid;
#endif
// Number of chain models
u32 getArg0() {
return fopAcM_GetParamBit(this, 0, 6);
+1 -1
View File
@@ -56,6 +56,7 @@ struct UserSettings {
ConfigVar<int> fanfareVolume;
ConfigVar<bool> enableReverb;
ConfigVar<bool> enableHrtf;
ConfigVar<bool> menuSounds;
} audio;
// Game settings
@@ -155,7 +156,6 @@ struct UserSettings {
ConfigVar<bool> showPipelineCompilation;
ConfigVar<bool> wasPresetChosen;
ConfigVar<bool> enableCrashReporting;
ConfigVar<bool> duskMenuOpen;
ConfigVar<int> cardFileType;
} backend;
};
+13
View File
@@ -5,6 +5,7 @@
#include "Z2AudioLib/Z2EnvSeMgr.h"
#include "Z2AudioLib/Z2LinkMgr.h"
#include "dusk/audio.h"
#include "dusk/settings.h"
class mDoAud_zelAudio_c : public Z2AudioMgr {
public:
@@ -132,6 +133,18 @@ inline void mDoAud_seStart(u32 i_sfxID, const Vec* i_sePos, u32 param_2, s8 i_re
-1.0f, -1.0f, 0);
}
#if TARGET_PC
inline void mDoAud_seStartMenu(u32 i_sfxID) {
if (!mDoAud_zelAudio_c::isInitFlag()) {
return;
}
if (!dusk::getSettings().audio.menuSounds.getValue()) {
return;
}
mDoAud_seStart(i_sfxID, nullptr, 0, 0);
}
#endif
inline void mDoAud_seStartLevel(u32 i_sfxID, const Vec* i_sePos, u32 param_2, s8 i_reverb) {
DUSK_AUDIO_SKIP()
Z2AudioMgr::getInterface()->seStartLevel(i_sfxID, i_sePos, param_2, i_reverb, 1.0f, 1.0f,
Binary file not shown.
Binary file not shown.
+4 -4
View File
@@ -25,7 +25,6 @@ body {
justify-content: flex-end;
align-items: stretch;
decorator: vertical-gradient(#00000000 #151610F2);
padding: 48dp 0 40dp 0;
filter: opacity(0);
transition: filter 0.2s linear-in-out;
}
@@ -42,18 +41,17 @@ body {
display: flex;
flex-direction: column;
gap: 24dp;
padding: 0 32dp;
padding: 48dp 64dp;
}
@media (max-height: 800dp) {
.overlay-root {
min-height: 38%;
padding: 32dp 0 28dp 0;
}
.overlay {
gap: 16dp;
padding: 0 24dp;
padding: 32dp 48dp;
}
}
@@ -144,4 +142,6 @@ footer-button.reset {
background-color: transparent;
opacity: 1;
cursor: pointer;
font-family: "Material Symbols Rounded";
font-weight: normal;
}
+36 -11
View File
@@ -12,7 +12,8 @@ body {
background-color: #000000;
decorator: image(../prelaunch-bg.png cover left center);
filter: opacity(0);
transition: filter 1s 0.1s linear-in-out;
transition: filter 1s 0.2s linear-in-out;
z-index: -1;
}
body[open] {
@@ -104,13 +105,14 @@ hero img {
decorator: horizontal-gradient(#FEE685FF #FEE68500);
}
disk-status {
disc-info {
position: absolute;
left: 96dp;
bottom: 72dp;
display: flex;
flex-direction: column;
gap: 8dp;
gap: 12dp;
font-size: 24dp;
}
version-info {
@@ -119,30 +121,53 @@ version-info {
bottom: 72dp;
display: flex;
flex-direction: column;
gap: 8dp;
gap: 12dp;
text-align: right;
}
.status,
.version {
font-size: 24dp;
}
.status,
.update {
#disc-status {
display: flex;
align-items: center;
gap: 8dp;
}
#disc-status[status=good] {
color: #D8F999;
}
.status[bad] {
#disc-status[status=bad] {
color: #FFC9C9;
}
#disc-status icon {
display: block;
width: 24dp;
height: 24dp;
font-family: "Material Symbols Rounded";
font-weight: normal;
font-size: 24dp;
}
#disc-status[status=good] icon {
decorator: text("&#xe5ca;" center center);
}
#disc-status[status=bad] icon {
decorator: text("&#xe5cd;" center center);
}
#disc-version {
font-size: 20dp;
}
/* TODO: Hidden until an actual update checker is introduced */
.update {
display: none;
font-size: 16dp;
font-weight: bold;
cursor: pointer;
color: #D8F999;
}
.detail,
+48
View File
@@ -1,10 +1,21 @@
tab-bar {
display: flex;
position: relative;
min-width: 0;
overflow: auto hidden;
clip: always;
text-transform: uppercase;
}
tab-bar scrollbarhorizontal,
tab-bar scrollbarhorizontal sliderarrowdec,
tab-bar scrollbarhorizontal sliderarrowinc,
tab-bar scrollbarhorizontal slidertrack,
tab-bar scrollbarhorizontal sliderbar {
width: 0;
height: 0;
}
tab-bar tab {
flex: 0 0 auto;
padding: 0 24dp;
@@ -31,3 +42,40 @@ tab-bar tab:hover {
tab-bar tab:active {
decorator: vertical-gradient(#c2a42d10 #c2a42d40);
}
tab-bar[closable] tab-end-spacer {
display: block;
flex: 0 0 64dp;
width: 64dp;
pointer-events: none;
}
tab-bar[closable] close {
display: block;
position: fixed;
top: 8dp;
right: 8dp;
z-index: 1;
width: 48dp;
height: 48dp;
font-family: "Material Symbols Rounded";
font-weight: normal;
font-size: 24dp;
color: rgba(224, 219, 200, 70%);
backdrop-filter: blur(2dp);
border-radius: 6dp;
decorator: text("&#xe5cd;" center center);
transition: color background-color 0.12s linear-in-out;
cursor: pointer;
}
tab-bar[closable] close:hover,
tab-bar[closable] close:focus-visible {
color: #fff;
background-color: rgba(194, 164, 45, 24%);
}
tab-bar[closable] close:active {
color: #fff;
background-color: rgba(194, 164, 45, 40%);
}
+166 -4
View File
@@ -3,6 +3,7 @@
}
body {
display: flex;
width: 100%;
height: 100%;
padding: 64dp;
@@ -17,6 +18,7 @@ window {
display: flex;
flex-flow: column;
height: 100%;
width: 100%;
max-width: 1088dp;
max-height: 768dp;
margin: auto;
@@ -32,6 +34,15 @@ window {
transition: filter transform 0.2s cubic-in-out;
}
window.small {
height: auto;
width: auto;
}
window.preset {
min-width: 650dp;
}
window[open] {
filter: opacity(1);
transform: scale(1);
@@ -62,7 +73,7 @@ window tab-bar tab {
window content {
display: flex;
flex: 1 1 0;
flex: 1 1 auto;
min-width: 0;
min-height: 0;
overflow: hidden;
@@ -72,7 +83,6 @@ window content pane {
display: flex;
flex-flow: column;
flex: 1 1 0;
height: 100%;
min-width: 0;
min-height: 0;
padding: 24dp;
@@ -225,15 +235,167 @@ select-button key {
font-weight: bold;
font-size: 18dp;
text-transform: uppercase;
flex: 1 0 auto;
flex: 0 1 auto;
}
select-button value {
margin-left: auto;
flex: 1 1 auto;
text-align: right;
font-size: 20dp;
}
select-button value.modified {
font-weight: bold;
}
select-button input {
text-align: right;
font-size: 20dp;
}
icon {
font-family: "Material Symbols Rounded";
font-weight: normal;
display: inline-block;
vertical-align: middle;
}
icon.warning {
width: 1em;
height: 1em;
decorator: text("&#xe002;" center center);
color: #ffcc00;
}
.achievement-row {
display: flex;
align-items: flex-start;
gap: 10dp;
padding: 12dp 0;
border-bottom: 1dp rgba(146, 135, 91, 30%);
}
.achievement-info {
display: block;
flex: 1 1 0;
min-width: 0;
}
.achievement-header {
display: flex;
align-items: center;
}
.achievement-name {
flex: 1;
font-weight: bold;
}
.achievement-name.unlocked {
color: #ffa826;
}
.achievement-badge {
font-size: 14dp;
opacity: 0.7;
}
.achievement-badge.unlocked {
color: #44cc55;
opacity: 1;
}
.achievement-badge.locked {
color: #cc4444;
opacity: 1;
}
.achievement-desc {
display: block;
color: rgba(224, 219, 200, 55%);
font-size: 16dp;
margin: 4dp 0 0 0;
}
.achievement-progress {
display: block;
font-size: 13dp;
color: rgba(224, 219, 200, 45%);
}
progressbar {
display: block;
width: 100%;
height: 6dp;
border-radius: 3dp;
background-color: rgba(255, 255, 255, 10%);
margin: 6dp 0 2dp 0;
}
progressbar.progress-done fill {
background-color: #44aa22;
border-radius: 3dp;
}
progressbar.progress-ongoing fill {
background-color: #2255bb;
border-radius: 3dp;
}
button.achievement-clear {
flex: 0 0 auto;
align-self: center;
font-size: 14dp;
padding: 2dp 8dp;
opacity: 0.45;
}
.preset-dialog {
display: flex;
flex-flow: column;
padding: 32dp;
gap: 20dp;
flex: 0 1 auto;
}
.preset-title {
display: block;
font-family: "Fira Sans Condensed";
font-weight: bold;
font-size: 30dp;
text-align: center;
}
.preset-intro {
display: block;
font-size: 18dp;
text-align: center;
color: rgba(224, 219, 200, 65%);
}
.preset-grid {
display: flex;
flex-direction: row;
gap: 20dp;
flex: 0 1 auto;
align-items: flex-start;
}
.preset-col {
display: flex;
flex-flow: column;
gap: 12dp;
flex: 1 1 0;
}
button.preset-btn {
font-size: 22dp;
padding: 20dp 16dp;
}
.preset-desc {
display: block;
font-size: 16dp;
color: rgba(224, 219, 200, 65%);
text-align: center;
}
+6
View File
@@ -2,6 +2,7 @@
#include "Z2AudioLib/Z2SoundInfo.h"
#if TARGET_PC
#include "dusk/audio/DuskDsp.hpp"
#include "dusk/settings.h"
#include <cmath>
#endif
#include "Z2AudioLib/Z2Calc.h"
@@ -705,6 +706,11 @@ f32 Z2Audience::calcRelPosVolume(const Vec& param_0, f32 param_1, int camID) {
f32 Z2Audience::calcRelPosPan(const Vec& param_0, int camID) {
Vec local_54 = param_0;
local_54.y = 0.0f;
#if TARGET_PC
if (dusk::getSettings().game.enableMirrorMode) {
local_54.x = -local_54.x;
}
#endif
f32 dVar6 = VECMag(&local_54);
if (dVar6 < 0.1f) {
+6
View File
@@ -517,6 +517,12 @@ void daE_OctBg_c::core_fish_attack() {
field_0xbaf = cM_rndFX(80.0f) + 100.0f;
}
}
#if AVOID_UB
else {
in_f31 = cM_rndF(400.0f) + 80.0f;
field_0xbaf = cM_rndFX(80.0f) + 100.0f;
}
#endif
} else if (current.pos.abs(cStack_5c) < 400.0f) {
in_f31 = cM_rndF(50.0f) + 20.0f;
field_0xbaf = cM_rndFX(20.0f) + 40.0f;
+2
View File
@@ -14,6 +14,7 @@
#include "d/actor/d_a_obj_automata.h"
#include "d/d_msg_object.h"
#include "d/actor/d_a_obj_scannon.h"
#include "dusk/frame_interpolation.h"
#include <cstring>
const daNpc_Toby_HIOParam daNpc_Toby_Param_c::m = {
@@ -1398,6 +1399,7 @@ int daNpc_Toby_c::cutRepairSCannon(int arg0) {
old.pos = current.pos;
setAngle(cM_deg2s(5.0f * f32(mPath.getArg0())));
mEventTimer = mPath.getArg2();
dusk::frame_interp::request_presentation_sync();
}
} else if (!mHide) {
mHide = 1;
+51
View File
@@ -11,6 +11,8 @@
#include "d/d_bg_w.h"
#include "d/d_cc_uty.h"
#include "d/d_com_inf_game.h"
#include "dusk/frame_interpolation.h"
#include "dusk/settings.h"
struct daObjKLift00_HIO_c : public mDoHIO_entry_c {
daObjKLift00_HIO_c();
@@ -295,6 +297,11 @@ int daObjKLift00_c::Create() {
if(getLock())
mStopSwingingFrames = 5;
#if TARGET_PC
mChainInterpPrevValid = false;
mChainInterpCurrValid = false;
#endif
return 1;
}
@@ -436,6 +443,34 @@ int daObjKLift00_c::Execute(Mtx** i_mtx) {
return 1;
}
#if TARGET_PC
static void klift00_interp_callback(bool isSimFrame, void* pUserWork) {
static_cast<daObjKLift00_c*>(pUserWork)->onInterpCallback();
}
void daObjKLift00_c::onInterpCallback() {
if (!mChainInterpPrevValid || !mChainInterpCurrValid) {
return;
}
const f32 alpha = dusk::frame_interp::get_interpolation_step();
cXyz savedPositions[64];
for (int i = 0; i < mNumChains; i++) {
savedPositions[i] = mChainPositions[i].mCurrentPos;
const cXyz& p0 = mChainInterpPrev[i];
const cXyz& p1 = mChainInterpCurr[i];
mChainPositions[i].mCurrentPos = p0 + (p1 - p0) * alpha;
}
setMtx();
for (int i = 0; i < mNumChains; i++) {
mChainPositions[i].mCurrentPos = savedPositions[i];
}
}
#endif
int daObjKLift00_c::Draw() {
g_env_light.settingTevStruct(16, &current.pos, &tevStr);
g_env_light.setLightTevColorType_MAJI(mpLiftPlatform, &tevStr);
@@ -457,6 +492,22 @@ int daObjKLift00_c::Draw() {
dComIfGd_setList();
#if TARGET_PC
if (dusk::getSettings().game.enableFrameInterpolation) {
if (mChainInterpCurrValid) {
memcpy(mChainInterpPrev, mChainInterpCurr, mNumChains * sizeof(cXyz));
mChainInterpPrevValid = true;
}
for (int i = 0; i < mNumChains; i++) {
mChainInterpCurr[i] = mChainPositions[i].mCurrentPos;
}
mChainInterpCurrValid = true;
dusk::frame_interp::add_interpolation_callback(&klift00_interp_callback, this);
}
#endif
return 1;
}
+31 -4
View File
@@ -1,3 +1,5 @@
#include <memory>
#include "aurora/lib/logging.hpp"
#include "os_report.h"
@@ -21,10 +23,35 @@ static bool checkEnabled() {
static std::string FormatToString(const char* msg, va_list list) {
int ret = vsnprintf(nullptr, 0, msg, list);
std::string buf(ret, '\0');
vsnprintf(buf.data(), buf.size(), msg, list);
buf.pop_back();
return buf;
if (ret <= 0) {
return {};
}
++ret;
std::unique_ptr<char[]> buf(new char[ret]);
vsnprintf(buf.get(), ret, msg, list);
buf[ret - 1] = '\0';
return {buf.get()};
}
void OSReport(const char* fmt, ...) {
if (!checkEnabled()) {
return;
}
va_list args;
va_start(args, fmt);
const auto str = FormatToString(fmt, args);
va_end(args);
Log.info("{}", str);
}
void OSPanic(const char* file, int line, const char* fmt, ...) {
va_list args;
va_start(args, fmt);
const auto str = FormatToString(fmt, args);
va_end(args);
Log.fatal("[{}:{}] {}", file, line, str);
}
void OSReport_Error(const char* fmt, ...) {
-245
View File
@@ -1,245 +0,0 @@
#include "ImGuiAchievements.hpp"
#include "ImGuiConfig.hpp"
#include "dusk/achievements.h"
#include "dusk/settings.h"
#include "fmt/format.h"
#include "imgui.h"
namespace dusk {
void ImGuiAchievements::notify(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 ImGuiAchievements::showNotification() {
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);
}
void ImGuiAchievements::draw(bool& open) {
showNotification();
if (!open) {
return;
}
ImGui::SetNextWindowSizeConstraints(ImVec2(800, 200), ImVec2(1280, 900));
ImGui::SetNextWindowSize(ImVec2(800, 480), ImGuiCond_FirstUseEver);
if (!ImGui::Begin(
"Achievements", &open,
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)
)
{
ImGui::End();
return;
}
const auto achievements = AchievementSystem::get().getAchievements();
int unlocked = 0;
for (const auto& a : achievements) {
if (a.unlocked) {
++unlocked;
}
}
ImGui::Text("%d / %d achievements unlocked", unlocked, (int)achievements.size());
ImGui::SameLine();
config::ImGuiCheckbox("Notifications", getSettings().game.enableAchievementNotifications);
ImGui::Separator();
static const struct {
AchievementCategory cat;
const char* label;
ImVec4 color;
} ACHIEVEMENT_CATEGORIES[] = {
{AchievementCategory::Story, "Story", ImVec4(1.0f, 0.82f, 0.1f, 1.0f)},
{AchievementCategory::Collection, "Collection", ImVec4(0.3f, 0.85f, 0.4f, 1.0f)},
{AchievementCategory::Challenge, "Challenge", ImVec4(1.0f, 0.65f, 0.15f, 1.0f)},
{AchievementCategory::Minigame, "Minigame", ImVec4(0.5f, 0.85f, 1.0f, 1.0f)},
{AchievementCategory::Misc, "Misc", ImVec4(0.65f, 0.65f, 0.65f, 1.0f)},
{AchievementCategory::Glitched, "Glitched", ImVec4(0.75f, 0.4f, 1.0f, 1.0f)},
};
const float footerHeight = ImGui::GetStyle().ItemSpacing.y + ImGui::GetFrameHeightWithSpacing();
if (ImGui::BeginTabBar("##achievement_tabs", ImGuiTabBarFlags_FittingPolicyScroll)) {
for (const auto& catInfo : ACHIEVEMENT_CATEGORIES) {
int catTotal = 0, catUnlocked = 0;
for (const auto& a : achievements) {
if (a.category == catInfo.cat) {
++catTotal;
if (a.unlocked) {
++catUnlocked;
}
}
}
if (catTotal == 0) {
continue;
}
const std::string tabLabel = fmt::format("{} ({}/{})###{}", catInfo.label, catUnlocked, catTotal, catInfo.label);
ImGui::PushStyleColor(ImGuiCol_Text, catInfo.color);
const bool tabOpen = ImGui::BeginTabItem(tabLabel.c_str());
ImGui::PopStyleColor();
if (tabOpen) {
ImGui::BeginChild(
"##cat_list",
ImVec2(0, -footerHeight),
ImGuiChildFlags_None,
ImGuiWindowFlags_NoBackground
);
ImGui::Spacing();
for (const auto& a : achievements) {
if (a.category != catInfo.cat) {
continue;
}
ImGui::PushID(a.key);
ImGui::BeginGroup();
ImGui::PushStyleColor(
ImGuiCol_Text,
a.unlocked ?
ImVec4(1.0f, 0.65f, 0.15f, 1.0f) :
ImGui::GetStyleColorVec4(ImGuiCol_Text)
);
ImGui::TextUnformatted(a.name);
ImGui::PopStyleColor();
const char* statusLabel = a.unlocked ? "[Unlocked]" : "[Locked]";
ImGui::SameLine(
ImGui::GetContentRegionMax().x -
ImGui::CalcTextSize(statusLabel).x
);
if (a.unlocked) {
ImGui::TextColored(ImVec4(0.2f, 0.8f, 0.2f, 1.0f), "%s", statusLabel);
} else {
ImGui::TextColored(ImVec4(0.8f, 0.2f, 0.2f, 1.0f), "%s", statusLabel);
}
ImGui::TextDisabled("%s", a.description);
if (a.isCounter) {
const float fraction = a.goal > 0 ? (float)(a.progress) / (float)(a.goal) : 1.0f;
const std::string overlay = fmt::format("{} / {}", a.progress, a.goal);
ImGui::PushStyleColor(
ImGuiCol_PlotHistogram,
a.unlocked ?
ImVec4(0.4f, 0.7f, 0.1f, 1.0f) :
ImVec4(0.2f, 0.45f, 0.8f, 1.0f)
);
ImGui::ProgressBar(fraction, ImVec2(-1.0f, 0.0f), overlay.c_str());
ImGui::PopStyleColor();
}
ImGui::EndGroup();
if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) {
ImGui::OpenPopup("##ctx");
}
if (ImGui::BeginPopup("##ctx")) {
ImGui::TextDisabled("%s", a.name);
ImGui::Separator();
if (ImGui::MenuItem("Clear Achievement")) {
AchievementSystem::get().clearOne(a.key);
}
ImGui::EndPopup();
}
ImGui::Spacing();
ImGui::PopID();
}
ImGui::EndChild();
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
}
ImGui::Separator();
ImGui::Spacing();
if (ImGui::Button("Clear All Achievements")) {
ImGui::OpenPopup("##confirm_clear");
}
if (ImGui::BeginPopup("##confirm_clear")) {
ImGui::Text("Reset all achievement progress?");
ImGui::Spacing();
if (ImGui::Button("Yes, reset all")) {
AchievementSystem::get().clearAll();
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (ImGui::Button("Cancel")) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
ImGui::End();
}
} // namespace dusk
-23
View File
@@ -1,23 +0,0 @@
#pragma once
#include <queue>
#include <string>
namespace dusk {
class ImGuiAchievements {
public:
void draw(bool& open);
void notify(std::string name);
private:
void showNotification();
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;
};
} // namespace dusk
+9 -69
View File
@@ -10,10 +10,10 @@
#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_events.h"
#include "SDL3/SDL_mouse.h"
#include "aurora/lib/window.hpp"
#include "dusk/achievements.h"
#include "dusk/audio/DuskAudioSystem.h"
#include "dusk/config.hpp"
@@ -22,6 +22,8 @@
#include "dusk/livesplit.h"
#include "dusk/main.h"
#include "dusk/settings.h"
#include "f_pc/f_pc_manager.h"
#include "f_pc/f_pc_name.h"
#include "m_Do/m_Do_controller_pad.h"
#include "m_Do/m_Do_main.h"
#include "tracy/Tracy.hpp"
@@ -35,14 +37,6 @@ using namespace std::string_literals;
using namespace std::string_view_literals;
namespace {
ImVec2 TouchEventToScreenPos(const SDL_TouchFingerEvent& touch) {
const AuroraWindowSize size = aurora::window::get_window_size();
return ImVec2{
touch.x * static_cast<float>(size.width),
touch.y * static_cast<float>(size.height),
};
}
ImGuiWindow* FindDragScrollWindow(ImGuiWindow* window) {
while (window != nullptr) {
const bool canScrollX = window->ScrollMax.x > 0.0f;
@@ -241,48 +235,7 @@ namespace dusk {
ImGuiConsole::ImGuiConsole() {}
void ImGuiConsole::HandleSDLEvent(const SDL_Event& event) {
if (!IsGameLaunched) {
return;
}
switch (event.type) {
case SDL_EVENT_FINGER_DOWN:
if (!m_touchTapActive) {
m_touchTapActive = true;
m_touchTapMoved = false;
m_touchTapFingerId = event.tfinger.fingerID;
m_touchTapStartPos = TouchEventToScreenPos(event.tfinger);
}
break;
case SDL_EVENT_FINGER_MOTION:
if (m_touchTapActive && m_touchTapFingerId == event.tfinger.fingerID) {
const auto currentPos = TouchEventToScreenPos(event.tfinger);
const auto delta = currentPos - m_touchTapStartPos;
if (ImLengthSqr(delta) > 144.0f) {
m_touchTapMoved = true;
}
}
break;
case SDL_EVENT_FINGER_UP:
if (m_touchTapActive && m_touchTapFingerId == event.tfinger.fingerID) {
const bool shouldToggle =
!m_touchTapMoved && (m_isHidden || !ImGui::GetIO().WantCaptureMouse);
m_touchTapActive = false;
m_touchTapMoved = false;
if (shouldToggle) {
m_isHidden = !m_isHidden;
getSettings().backend.duskMenuOpen.setValue(!m_isHidden);
Save();
}
}
break;
case SDL_EVENT_FINGER_CANCELED:
m_touchTapActive = false;
m_touchTapMoved = false;
break;
default:
break;
}
(void)event;
}
void ImGuiConsole::UpdateSettings() {
@@ -310,7 +263,8 @@ namespace dusk {
}
}
if ((ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) &&
if (!fpcM_SearchByName(fpcNm_LOGO_SCENE_e) &&
(ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) &&
ImGui::IsKeyPressed(ImGuiKey_R))
{
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
@@ -320,23 +274,14 @@ namespace dusk {
ImGuiMenuGame::ToggleFullscreen();
}
if (ImGui::IsKeyPressed(ImGuiKey_Escape) && getSettings().video.enableFullscreen) {
ImGuiMenuGame::ToggleFullscreen();
}
// if (!dusk::IsGameLaunched) {
// m_preLaunchWindow.draw();
// }
m_isHidden = !getSettings().backend.duskMenuOpen;
if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_F1)) {
m_isHidden = !m_isHidden;
}
bool showMenu = !m_isHidden;
if (getSettings().backend.duskMenuOpen != showMenu) {
getSettings().backend.duskMenuOpen.setValue(showMenu);
Save();
}
// The menu bar renders with ImGuiCol_WindowBg behind it. We just want ImGuiCol_MenuBarBg,
// so make the window bg fully transparent temporarily
@@ -359,14 +304,9 @@ namespace dusk {
}
ImGui::PopStyleColor();
if (!getSettings().backend.wasPresetChosen) {
m_firstRunPreset.draw();
return;
}
if (dusk::IsGameLaunched && !m_isLaunchInitialized) {
AddToast(ImGui::GetIO().MouseSource == ImGuiMouseSource_TouchScreen ?
"Tap to toggle menu"s :
"3-finger tap to toggle menu"s :
"Press F1 to toggle menu"s,
4.f);
m_isLaunchInitialized = true;
@@ -402,7 +342,7 @@ namespace dusk {
m_menuTools.ShowSaveEditor();
m_menuTools.ShowStateShare();
}
m_menuTools.ShowAchievements();
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.
-7
View File
@@ -6,9 +6,7 @@
#include <string_view>
#include <aurora/aurora.h>
#include <SDL3/SDL_touch.h>
#include "ImGuiFirstRunPreset.hpp"
#include "ImGuiMenuGame.hpp"
#include "ImGuiMenuTools.hpp"
#include "ImGuiPreLaunchWindow.hpp"
@@ -42,15 +40,10 @@ private:
bool m_isHidden = true;
bool m_isLaunchInitialized = false;
bool m_touchTapActive = false;
bool m_touchTapMoved = false;
SDL_FingerID m_touchTapFingerId = 0;
ImVec2 m_touchTapStartPos = {};
ImGuiWindow* m_dragScrollWindow = nullptr;
ImVec2 m_dragScrollLastMousePos = {};
std::deque<Toast> m_toasts;
ImGuiFirstRunPreset m_firstRunPreset;
ImGuiMenuGame m_menuGame;
ImGuiPreLaunchWindow m_preLaunchWindow;
-141
View File
@@ -1,141 +0,0 @@
#include "ImGuiFirstRunPreset.hpp"
#include "imgui.h"
#include "ImGuiConsole.hpp"
#include "ImGuiEngine.hpp"
#include "dusk/settings.h"
#include "dusk/config.hpp"
#include <dusk/dusk.h>
namespace dusk {
static void ApplyPresetClassic() {
auto& s = getSettings();
s.video.lockAspectRatio.setValue(true);
s.game.bloomMode.setValue(BloomMode::Classic);
AuroraSetViewportPolicy(AURORA_VIEWPORT_FIT);
}
static void ApplyPresetHD() {
auto& s = getSettings();
s.game.bloomMode.setValue(BloomMode::Classic);
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);
s.game.fastClimbing.setValue(true);
s.game.noMissClimbing.setValue(true);
s.game.fastTears.setValue(true);
s.game.biggerWallets.setValue(true);
s.game.invertCameraXAxis.setValue(true);
s.game.freeCamera.setValue(true);
s.game.no2ndFishForCat.setValue(true);
}
static void ApplyPresetDusk() {
ApplyPresetHD();
auto& s = getSettings();
s.game.enableAchievementNotifications.setValue(true);
s.game.enableQuickTransform.setValue(true);
s.game.instantSaves.setValue(true);
s.game.midnasLamentNonStop.setValue(true);
s.game.enableFrameInterpolation.setValue(true);
s.game.sunsSong.setValue(true);
s.game.bloomMode.setValue(BloomMode::Dusk);
s.game.autoSave.setValue(true);
}
// =========================================================================
void ImGuiFirstRunPreset::draw() {
const char* modalTitle = "Welcome to Dusk!";
if (m_done) return;
if (!m_opened) {
ImGui::OpenPopup(modalTitle);
m_opened = true;
}
const ImGuiViewport* viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_Always, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowSize(ImVec2(800.0f * ImGuiScale(), 0.0f), ImGuiCond_Always);
if (!ImGui::BeginPopupModal(modalTitle, nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoMove)) {
// force the user to actually pick one, and not just hit escape to skip the dialog
m_opened = false;
return;
}
ImGui::TextWrapped("Choose a preset to get started. You can change any setting later from the Enhancements menu.");
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
int chosen = -1;
if (ImGui::BeginTable("##presets", 5, ImGuiTableFlags_None)) {
ImGui::TableSetupColumn(nullptr, ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn(nullptr, ImGuiTableColumnFlags_WidthFixed, 16.0f * ImGuiScale());
ImGui::TableSetupColumn(nullptr, ImGuiTableColumnFlags_WidthStretch);
ImGui::TableSetupColumn(nullptr, ImGuiTableColumnFlags_WidthFixed, 16.0f * ImGuiScale());
ImGui::TableSetupColumn(nullptr, ImGuiTableColumnFlags_WidthStretch);
ImGui::TableNextRow();
ImGui::PushFont(ImGuiEngine::fontLarge);
ImGui::TableSetColumnIndex(0);
if (ImGui::Button("Classic##btn", ImVec2(ImGui::GetContentRegionAvail().x, 80.0f * ImGuiScale()))) {
chosen = 0;
}
ImGui::TableSetColumnIndex(2);
if (ImGui::Button("HD##btn", ImVec2(ImGui::GetContentRegionAvail().x, 80.0f * ImGuiScale()))) {
chosen = 1;
}
ImGui::TableSetColumnIndex(4);
if (ImGui::Button("Dusk##btn", ImVec2(ImGui::GetContentRegionAvail().x, 80.0f * ImGuiScale())))
{
chosen = 2;
}
ImGui::PopFont();
ImGui::TableNextRow();
ImGui::TableSetColumnIndex(0);
ImGui::Spacing();
ImGui::TextWrapped("All enhancements disabled to match the GameCube version. Good for speedrunning or simple nostalgia!");
ImGui::TableSetColumnIndex(2);
ImGui::Spacing();
ImGui::TextWrapped("Some enhancements enabled to match the HD version. A good starting point for most players!");
ImGui::TableSetColumnIndex(4);
ImGui::Spacing();
ImGui::TextWrapped("More enhancements enabled than the HD preset. Veteran players will appreciate the additional tweaks!");
ImGui::EndTable();
}
if (chosen >= 0) {
if (chosen == 0) ApplyPresetClassic();
if (chosen == 1) ApplyPresetHD();
if (chosen == 2) ApplyPresetDusk();
getSettings().backend.wasPresetChosen.setValue(true);
config::Save();
m_done = true;
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
}
} // namespace dusk
-14
View File
@@ -1,14 +0,0 @@
#pragma once
namespace dusk {
class ImGuiFirstRunPreset {
public:
void draw();
private:
bool m_opened = false;
bool m_done = false;
};
} // namespace dusk
+58 -5
View File
@@ -66,7 +66,6 @@ namespace dusk {
ImGui::EndDisabled();
}
ImGui::MenuItem("Achievements", nullptr, &m_showAchievements);
#if DUSK_CAN_OPEN_DATA_FOLDER
ImGui::Separator();
@@ -269,11 +268,65 @@ namespace dusk {
ImGui::PopFont();
}
void ImGuiMenuTools::ShowAchievements() {
m_achievementsWindow.draw(m_showAchievements);
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::notifyAchievement(std::string name) {
m_achievementsWindow.notify(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);
}
}
+7 -4
View File
@@ -2,10 +2,10 @@
#define DUSK_IMGUI_MENUTOOLS_HPP
#include <aurora/aurora.h>
#include <queue>
#include <string>
#include "imgui.h"
#include "ImGuiAchievements.hpp"
#include "ImGuiSaveEditor.hpp"
#include "ImGuiStateShare.hpp"
@@ -27,8 +27,8 @@ namespace dusk {
void ShowAudioDebug();
void ShowSaveEditor();
void ShowStateShare();
void ShowAchievements();
void notifyAchievement(std::string name);
void showAchievementNotification();
private:
bool m_showDebugOverlay = false;
@@ -69,8 +69,11 @@ namespace dusk {
bool m_showStateShare = false;
ImGuiStateShare m_stateShare;
bool m_showAchievements = false;
ImGuiAchievements m_achievementsWindow;
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;
};
}
+7 -7
View File
@@ -18,6 +18,7 @@ UserSettings g_userSettings = {
.fanfareVolume {"audio.fanfareVolume", 100},
.enableReverb {"audio.enableReverb", true},
.enableHrtf {"audio.enableHrtf", false},
.menuSounds {"audio.menuSounds", true},
},
.game = {
@@ -25,8 +26,8 @@ UserSettings g_userSettings = {
// Quality of Life
.enableQuickTransform {"game.enableQuickTransform", false},
.hideTvSettingsScreen {"game.hideTvSettingsScreen", false},
.skipWarningScreen {"game.skipWarningScreen", false},
.hideTvSettingsScreen {"game.hideTvSettingsScreen", true},
.skipWarningScreen {"game.skipWarningScreen", true},
.biggerWallets {"game.biggerWallets", false},
.noReturnRupees {"game.noReturnRupees", false},
.disableRupeeCutscenes {"game.disableRupeeCutscenes", false},
@@ -47,11 +48,11 @@ UserSettings g_userSettings = {
.enableMirrorMode {"game.enableMirrorMode", false},
.disableMainHUD {"game.disableMainHUD", false},
.pauseOnFocusLost {"game.pauseOnFocusLost", false},
.enableLinkDollRotation = {"game.enableLinkDollRotation", false },
.enableAchievementNotifications {"game.enableAchievementNotifications", false},
.enableLinkDollRotation = {"game.enableLinkDollRotation", false},
.enableAchievementNotifications {"game.enableAchievementNotifications", true},
// Graphics
.bloomMode {"game.bloomMode", BloomMode::Classic},
.bloomMode {"game.bloomMode", BloomMode::Dusk},
.bloomMultiplier {"game.bloomMultiplier", 1.0f},
.disableWaterRefraction {"game.disableWaterRefraction", false},
.enableFrameInterpolation {"game.enableFrameInterpolation", false},
@@ -114,7 +115,6 @@ UserSettings g_userSettings = {
.showPipelineCompilation {"backend.showPipelineCompilation", false},
.wasPresetChosen {"backend.wasPresetChosen", false},
.enableCrashReporting {"backend.enableCrashReporting", true},
.duskMenuOpen {"backend.duskMenuOpen", false},
.cardFileType {"backend.cardFileType", static_cast<int>(CARD_GCIFOLDER)}
}
};
@@ -137,6 +137,7 @@ void registerSettings() {
Register(g_userSettings.audio.fanfareVolume);
Register(g_userSettings.audio.enableReverb);
Register(g_userSettings.audio.enableHrtf);
Register(g_userSettings.audio.menuSounds);
// Game
Register(g_userSettings.game.language);
@@ -212,7 +213,6 @@ void registerSettings() {
Register(g_userSettings.backend.showPipelineCompilation);
Register(g_userSettings.backend.wasPresetChosen);
Register(g_userSettings.backend.enableCrashReporting);
Register(g_userSettings.backend.duskMenuOpen);
Register(g_userSettings.backend.cardFileType);
}
+208
View File
@@ -0,0 +1,208 @@
#include "achievements.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "dusk/achievements.h"
#include "fmt/format.h"
#include "m_Do/m_Do_audio.h"
#include "nav_types.hpp"
#include "pane.hpp"
namespace dusk::ui {
namespace {
struct CategoryInfo {
AchievementCategory cat;
const char* label;
};
constexpr CategoryInfo kCategories[] = {
{AchievementCategory::Story, "Story"},
{AchievementCategory::Collection, "Collection"},
{AchievementCategory::Challenge, "Challenge"},
{AchievementCategory::Minigame, "Minigame"},
{AchievementCategory::Misc, "Misc"},
{AchievementCategory::Glitched, "Glitched"},
};
Rml::String build_achievement_info_rml(const Achievement& a) {
Rml::String s = fmt::format(
R"(<div class="achievement-header">)"
R"(<span class="achievement-name{}">{}</span>)"
R"(<span class="achievement-badge{}">{}</span>)"
R"(</div>)"
R"(<p class="achievement-desc">{}</p>)",
a.unlocked ? " unlocked" : "",
a.name,
a.unlocked ? " unlocked" : " locked",
a.unlocked ? "Unlocked" : "Locked",
a.description
);
if (a.isCounter) {
float fraction = a.goal > 0 ? float(a.progress) / float(a.goal) : 1.0f;
s += fmt::format(
R"(<progressbar value="{:.3f}" class="{}"/>)"
R"(<span class="achievement-progress">{} / {}</span>)",
fraction,
a.unlocked ? "progress-done" : "progress-ongoing",
a.progress,
a.goal
);
}
return s;
}
class AchievementRow : public FluentComponent<AchievementRow> {
public:
AchievementRow(Rml::Element* parent, const Achievement& a)
: FluentComponent(createRowRoot(parent))
{
auto& btn = add_child<Button>(Button::Props{"×"});
mClearButton = &btn;
btn.root()->SetClass("achievement-clear", true);
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);
AchievementSystem::get().clearOne(key.c_str());
resetConfirm();
} else {
mConfirming = true;
mClearButton->set_text("Clear?");
}
return true;
}
if (cmd == NavCommand::Cancel && mConfirming) {
resetConfirm();
return true;
}
return false;
});
Component::listen(btn.root(), Rml::EventId::Blur, [this](Rml::Event&) {
resetConfirm();
});
auto* infoDiv = append(mRoot, "div");
infoDiv->SetClass("achievement-info", true);
infoDiv->SetInnerRML(build_achievement_info_rml(a));
}
bool focus() override { return mClearButton->focus(); }
private:
static Rml::Element* createRowRoot(Rml::Element* parent) {
auto* doc = parent->GetOwnerDocument();
auto elem = doc->CreateElement("div");
elem->SetClass("achievement-row", true);
return parent->AppendChild(std::move(elem));
}
void resetConfirm() {
mConfirming = false;
mClearButton->set_text("×");
}
Button* mClearButton = nullptr;
bool mConfirming = false;
};
} // namespace
AchievementsWindow::AchievementsWindow() {
const auto all = AchievementSystem::get().getAchievements();
for (const auto& catInfo : kCategories) {
int catTotal = 0;
for (const auto& a : all) {
if (a.category == catInfo.cat) {
++catTotal;
}
}
if (catTotal == 0) {
continue;
}
add_tab(catInfo.label, [this, cat = catInfo.cat](Rml::Element* content) {
const auto achievements = AchievementSystem::get().getAchievements();
int total = 0, unlocked = 0;
for (const auto& a : achievements) {
if (a.category == cat) {
++total;
if (a.unlocked) {
++unlocked;
}
}
}
auto& pane = add_child<Pane>(content, Pane::Type::Controlled);
pane.add_section(fmt::format("{} / {} unlocked", unlocked, total));
for (const auto& a : achievements) {
if (a.category != cat) {
continue;
}
pane.add_child<AchievementRow>(a);
}
pane.add_section("Actions");
auto& clearAllBtn = pane.add_button("Clear All Achievements");
auto* clearAllPtr = &clearAllBtn;
auto confirmingAll = std::make_shared<bool>(false);
clearAllBtn.on_nav_command([clearAllPtr, confirmingAll](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm) {
if (*confirmingAll) {
mDoAud_seStartMenu(Z2SE_SY_CURSOR_OK);
AchievementSystem::get().clearAll();
*confirmingAll = false;
clearAllPtr->set_text("Clear All Achievements");
} else {
*confirmingAll = true;
clearAllPtr->set_text("Are you sure?");
}
return true;
}
if (cmd == NavCommand::Cancel && *confirmingAll) {
*confirmingAll = false;
clearAllPtr->set_text("Clear All Achievements");
return true;
}
return false;
});
clearAllBtn.listen(Rml::EventId::Blur, [clearAllPtr, confirmingAll](Rml::Event&) {
*confirmingAll = false;
clearAllPtr->set_text("Clear All Achievements");
});
pane.finalize();
});
}
}
void AchievementsWindow::update() {
const auto current = AchievementSystem::get().getAchievements();
bool dirty = current.size() != mSnapshot.size();
if (!dirty) {
for (size_t i = 0; i < current.size(); ++i) {
if (current[i].progress != mSnapshot[i].progress ||
current[i].unlocked != mSnapshot[i].unlocked)
{
dirty = true;
break;
}
}
}
if (dirty) {
mSnapshot = current;
refresh_active_tab();
}
Window::update();
}
} // namespace dusk::ui
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include "dusk/achievements.h"
#include "window.hpp"
#include <vector>
namespace dusk::ui {
class AchievementsWindow : public Window {
public:
AchievementsWindow();
void update() override;
private:
std::vector<Achievement> mSnapshot;
};
} // namespace dusk::ui
+19 -3
View File
@@ -1,11 +1,25 @@
#include "bool_button.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
namespace dusk::ui {
BoolButton::BoolButton(Rml::Element* parent, Props props)
: BaseControlledSelectButton(parent, {std::move(props.key)}),
: BaseControlledSelectButton(parent,
{
.key = std::move(props.key),
.icon = std::move(props.icon),
}),
mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)),
mIsDisabled(std::move(props.isDisabled)) {}
mIsDisabled(std::move(props.isDisabled)), mIsModified(std::move(props.isModified)) {}
bool BoolButton::modified() const {
if (mIsModified) {
return mIsModified();
}
return BaseControlledSelectButton::modified();
}
bool BoolButton::disabled() const {
if (mIsDisabled) {
@@ -20,7 +34,9 @@ Rml::String BoolButton::format_value() {
bool BoolButton::handle_nav_command(NavCommand cmd) {
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || cmd == NavCommand::Right) {
mSetValue(!mGetValue());
const bool newValue = !mGetValue();
mSetValue(newValue);
mDoAud_seStartMenu(newValue ? Z2SE_SY_CURSOR_OK : Z2SE_SY_CURSOR_CANCEL);
return true;
}
return false;
+4
View File
@@ -7,13 +7,16 @@ class BoolButton : public BaseControlledSelectButton {
public:
struct Props {
Rml::String key;
Rml::String icon;
std::function<bool()> getValue;
std::function<void(bool)> setValue;
std::function<bool()> isDisabled;
std::function<bool()> isModified;
};
BoolButton(Rml::Element* parent, Props props);
bool modified() const override;
bool disabled() const override;
protected:
@@ -24,6 +27,7 @@ private:
std::function<int()> mGetValue;
std::function<void(int)> mSetValue;
std::function<bool()> mIsDisabled;
std::function<bool()> mIsModified;
};
} // namespace dusk::ui
+4
View File
@@ -2,6 +2,9 @@
#include "ui.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
#include <utility>
namespace dusk::ui {
@@ -34,6 +37,7 @@ 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;
}
-9
View File
@@ -66,15 +66,6 @@ public:
return static_cast<Derived&>(*this);
}
Derived& on_focus(ScopedEventListener::Callback callback) {
return listen(
Rml::EventId::Focus, [this, callback = std::move(callback)](Rml::Event& event) {
if (!disabled()) {
callback(event);
}
});
}
Derived& on_nav_command(std::function<bool(Rml::Event&, NavCommand)> callback) {
listen(Rml::EventId::Click, [this, callback](Rml::Event& event) {
if (!disabled() && callback(event, NavCommand::Confirm)) {
+636
View File
@@ -0,0 +1,636 @@
#include "controller_config.hpp"
#include "bool_button.hpp"
#include "button.hpp"
#include "pane.hpp"
#include "select_button.hpp"
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_keyboard.h>
#include <fmt/format.h>
#include <array>
#include <string>
#include <utility>
#include <vector>
namespace dusk::ui {
namespace {
Rml::String current_controller_name(int port) {
const char* name = PADGetName(port);
return name == nullptr ? "None" : name;
}
Rml::String controller_index_name(u32 index) {
const char* name = PADGetNameForControllerIndex(index);
if (name == nullptr) {
return fmt::format("Controller {}", index + 1);
}
return name;
}
SDL_Gamepad* gamepad_for_port(int port) {
const s32 index = PADGetIndexForPort(port);
if (index < 0) {
return nullptr;
}
return PADGetSDLGamepadForIndex(static_cast<u32>(index));
}
struct SpecificButtonName {
SDL_GamepadType type;
const char* name;
};
struct ButtonNames {
SDL_GamepadButton button;
std::vector<SpecificButtonName> names;
};
// clang-format off
const std::vector<ButtonNames> kGamepadButtonNames = {
{ SDL_GAMEPAD_BUTTON_LEFT_STICK, {
{SDL_GAMEPAD_TYPE_PS3, "L3"},
{SDL_GAMEPAD_TYPE_PS4, "L3"},
{SDL_GAMEPAD_TYPE_PS5, "L3"},
{SDL_GAMEPAD_TYPE_XBOX360, "Left Stick"},
{SDL_GAMEPAD_TYPE_XBOXONE, "Left Stick"},
{SDL_GAMEPAD_TYPE_GAMECUBE, "Control Stick"},
}},
{ SDL_GAMEPAD_BUTTON_RIGHT_STICK, {
{SDL_GAMEPAD_TYPE_PS3, "R3"},
{SDL_GAMEPAD_TYPE_PS4, "R3"},
{SDL_GAMEPAD_TYPE_PS5, "R3"},
{SDL_GAMEPAD_TYPE_XBOX360, "Right Stick"},
{SDL_GAMEPAD_TYPE_XBOXONE, "Right Stick"},
{SDL_GAMEPAD_TYPE_GAMECUBE, "C Stick"},
}},
{ SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, {
{SDL_GAMEPAD_TYPE_PS3, "L1"},
{SDL_GAMEPAD_TYPE_PS4, "L1"},
{SDL_GAMEPAD_TYPE_PS5, "L1"},
{SDL_GAMEPAD_TYPE_XBOX360, "LB"},
{SDL_GAMEPAD_TYPE_XBOXONE, "LB"},
}},
{ SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, {
{SDL_GAMEPAD_TYPE_PS3, "R1"},
{SDL_GAMEPAD_TYPE_PS4, "R1"},
{SDL_GAMEPAD_TYPE_PS5, "R1"},
{SDL_GAMEPAD_TYPE_XBOX360, "RB"},
{SDL_GAMEPAD_TYPE_XBOXONE, "RB"},
{SDL_GAMEPAD_TYPE_GAMECUBE, "Z"},
}},
{ SDL_GAMEPAD_BUTTON_BACK, {
{SDL_GAMEPAD_TYPE_PS3, "Select"},
{SDL_GAMEPAD_TYPE_PS4, "Share"},
{SDL_GAMEPAD_TYPE_PS5, "Create"},
{SDL_GAMEPAD_TYPE_XBOX360, "Back"},
{SDL_GAMEPAD_TYPE_XBOXONE, "View"},
}},
{ SDL_GAMEPAD_BUTTON_START, {
{SDL_GAMEPAD_TYPE_PS3, "Start"},
{SDL_GAMEPAD_TYPE_PS4, "Options"},
{SDL_GAMEPAD_TYPE_PS5, "Options"},
{SDL_GAMEPAD_TYPE_XBOX360, "Start"},
{SDL_GAMEPAD_TYPE_XBOXONE, "Menu"},
{SDL_GAMEPAD_TYPE_GAMECUBE, "Start/Pause"},
}},
};
// clang-format on
Rml::String native_button_name(SDL_Gamepad* gamepad, u32 buttonUntyped) {
if (buttonUntyped == PAD_NATIVE_BUTTON_INVALID) {
return "Not bound";
}
auto button = static_cast<SDL_GamepadButton>(buttonUntyped);
if (gamepad != nullptr) {
switch (SDL_GetGamepadButtonLabel(gamepad, button)) {
case SDL_GAMEPAD_BUTTON_LABEL_A:
return "A";
case SDL_GAMEPAD_BUTTON_LABEL_B:
return "B";
case SDL_GAMEPAD_BUTTON_LABEL_X:
return "X";
case SDL_GAMEPAD_BUTTON_LABEL_Y:
return "Y";
case SDL_GAMEPAD_BUTTON_LABEL_CROSS:
return "Cross";
case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE:
return "Circle";
case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE:
return "Triangle";
case SDL_GAMEPAD_BUTTON_LABEL_SQUARE:
return "Square";
default:
break;
}
}
const SDL_GamepadType type =
gamepad != nullptr ? SDL_GetGamepadType(gamepad) : SDL_GAMEPAD_TYPE_UNKNOWN;
for (const auto& buttonNames : kGamepadButtonNames) {
if (buttonNames.button != button) {
continue;
}
for (const auto& name : buttonNames.names) {
if (name.type == type) {
return name.name;
}
}
}
switch (button) {
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
return "D-pad left";
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
return "D-pad right";
case SDL_GAMEPAD_BUTTON_DPAD_UP:
return "D-pad up";
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
return "D-pad down";
default:
break;
}
if (const char* name = PADGetNativeButtonName(buttonUntyped)) {
return name;
}
return "Unknown";
}
Rml::String native_axis_name(const PADAxisMapping& mapping, SDL_Gamepad* gamepad) {
if (mapping.nativeAxis.nativeAxis != -1) {
Rml::String value = PADGetNativeAxisName(mapping.nativeAxis);
if (mapping.padAxis != PAD_AXIS_TRIGGER_L && mapping.padAxis != PAD_AXIS_TRIGGER_R) {
value += mapping.nativeAxis.sign == AXIS_SIGN_POSITIVE ? "+" : "-";
}
return value;
}
if (mapping.nativeButton != -1) {
return native_button_name(gamepad, static_cast<u32>(mapping.nativeButton));
}
return "Not bound";
}
bool is_dpad_button(PADButton button) {
return button == PAD_BUTTON_UP || button == PAD_BUTTON_DOWN || button == PAD_BUTTON_LEFT ||
button == PAD_BUTTON_RIGHT;
}
bool is_action_button(PADButton button) {
return button == PAD_BUTTON_A || button == PAD_BUTTON_B || button == PAD_BUTTON_X ||
button == PAD_BUTTON_Y || button == PAD_BUTTON_START || button == PAD_TRIGGER_Z;
}
bool input_neutral(int port) {
if (port < 0) {
return true;
}
return PADGetNativeButtonPressed(port) == -1 && PADGetNativeAxisPulled(port).nativeAxis == -1;
}
// A Keydown event with KI_ESCAPE may have been dispatched from the controller bindings,
// so instead poll the keyboard input directly for Escape-to-unbind
bool keyboard_escape_pressed() {
int keyCount = 0;
const bool* keys = SDL_GetKeyboardState(&keyCount);
return keys != nullptr && SDL_SCANCODE_ESCAPE < keyCount && keys[SDL_SCANCODE_ESCAPE];
}
} // namespace
ControllerConfigWindow::ControllerConfigWindow() {
listen(
Rml::EventId::Keydown,
[this](Rml::Event& event) {
if (capture_active() || mSuppressNavigationUntilNeutral) {
event.StopPropagation();
}
},
true);
if (auto* context = mDocument != nullptr ? mDocument->GetContext() : nullptr) {
if (auto* root = context->GetRootElement()) {
mListeners.emplace_back(std::make_unique<ScopedEventListener>(
root, "controllerchange", [this](Rml::Event&) { refresh_controller_page(); }));
}
}
for (int port = PAD_CHAN0; port < PAD_CHANMAX; ++port) {
add_tab(fmt::format("Port {}", port + 1), [this, port](Rml::Element* content) {
if (mPendingPort != -1 && mPendingPort != port) {
cancel_pending_binding();
}
build_port_tab(content, port);
});
}
}
void ControllerConfigWindow::hide(bool close) {
cancel_pending_binding();
Window::hide(close);
}
void ControllerConfigWindow::update() {
poll_pending_binding();
Window::update();
}
void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) {
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
mRightPane = &rightPane;
mActivePort = port;
auto addPageButton = [this, &leftPane, &rightPane, port](
Page page, Rml::String key, auto getValue) {
leftPane.register_control(leftPane.add_select_button({
.key = std::move(key),
.getValue = std::move(getValue),
}),
rightPane, [this, port, page](Pane& pane) {
mPage = page;
render_page(pane, port, page);
});
};
addPageButton(Page::Controller, "Controller", [port] { return current_controller_name(port); });
addPageButton(Page::Buttons, "Buttons", [] { return Rml::String(">"); });
addPageButton(Page::Triggers, "Triggers", [] { return Rml::String(">"); });
addPageButton(Page::Sticks, "Sticks", [] { return Rml::String(">"); });
leftPane.add_section("Options");
leftPane.register_control(leftPane.add_child<BoolButton>(BoolButton::Props{
.key = "Enable Dead Zones",
.getValue =
[port] {
PADDeadZones* deadZones = PADGetDeadZones(port);
return deadZones != nullptr && deadZones->useDeadzones;
},
.setValue =
[port](bool value) {
if (PADDeadZones* deadZones = PADGetDeadZones(port)) {
deadZones->useDeadzones = value;
PADSerializeMappings();
}
},
.isDisabled = [port] { return PADGetDeadZones(port) == nullptr; },
}),
rightPane, [](Pane& pane) {
pane.add_text("Apply configured dead zones to the sticks and analog triggers.");
});
leftPane.register_control(leftPane.add_child<BoolButton>(BoolButton::Props{
.key = "Emulate Triggers",
.getValue =
[port] {
PADDeadZones* deadZones = PADGetDeadZones(port);
return deadZones != nullptr && deadZones->emulateTriggers;
},
.setValue =
[port](bool value) {
if (PADDeadZones* deadZones = PADGetDeadZones(port)) {
deadZones->emulateTriggers = value;
PADSerializeMappings();
}
},
.isDisabled = [port] { return PADGetDeadZones(port) == nullptr; },
}),
rightPane, [](Pane& pane) {
pane.add_text("Treat analog trigger movement as digital L and R button input.");
});
render_page(rightPane, port, mPage);
}
void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) {
pane.clear();
switch (page) {
case Page::Controller: {
const u32 controllerCount = PADCount();
if (controllerCount == 0) {
pane.add_text("No controllers detected");
break;
}
pane.add_button({
.text = "None",
.isSelected = [port] { return PADGetIndexForPort(port) < 0; },
})
.on_pressed([this, port] {
cancel_pending_binding();
PADClearPort(port);
PADSerializeMappings();
});
for (u32 i = 0; i < controllerCount; ++i) {
pane.add_button(
{
.text = controller_index_name(i),
.isSelected =
[port, i] { return PADGetIndexForPort(port) == static_cast<s32>(i); },
})
.on_pressed([this, port, i] {
cancel_pending_binding();
PADSetPortForIndex(i, port);
PADSerializeMappings();
});
}
break;
}
case Page::Buttons: {
u32 buttonCount = 0;
PADButtonMapping* mappings = PADGetButtonMappings(port, &buttonCount);
if (mappings == nullptr) {
pane.add_text("No controller selected");
break;
}
SDL_Gamepad* gamepad = gamepad_for_port(port);
pane.add_section("Buttons");
for (u32 i = 0; i < buttonCount; ++i) {
PADButtonMapping& mapping = mappings[i];
if (!is_action_button(mapping.padButton)) {
continue;
}
pane.add_select_button({
.key = PADGetButtonName(mapping.padButton),
.getValue =
[this, &mapping, gamepad] {
if (mPendingButtonMapping == &mapping) {
return pending_button_label();
}
return native_button_name(
gamepad, mapping.nativeButton);
},
})
.on_pressed([this, port, &mapping] {
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
mPendingButtonMapping = &mapping;
});
}
pane.add_section("D-Pad");
for (u32 i = 0; i < buttonCount; ++i) {
PADButtonMapping& mapping = mappings[i];
if (!is_dpad_button(mapping.padButton)) {
continue;
}
pane.add_select_button({
.key = PADGetButtonName(mapping.padButton),
.getValue =
[this, &mapping, gamepad] {
if (mPendingButtonMapping == &mapping) {
return pending_button_label();
}
return native_button_name(
gamepad, mapping.nativeButton);
},
})
.on_pressed([this, port, &mapping] {
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
mPendingButtonMapping = &mapping;
});
}
break;
}
case Page::Triggers: {
u32 axisCount = 0;
PADAxisMapping* axes = PADGetAxisMappings(port, &axisCount);
u32 buttonCount = 0;
PADButtonMapping* buttons = PADGetButtonMappings(port, &buttonCount);
if (axes == nullptr && buttons == nullptr) {
pane.add_text("No controller selected");
break;
}
SDL_Gamepad* gamepad = gamepad_for_port(port);
pane.add_section("Analog");
constexpr std::array<PADAxis, 2> kTriggerAxes = {PAD_AXIS_TRIGGER_L, PAD_AXIS_TRIGGER_R};
if (axes != nullptr) {
for (PADAxis axis : kTriggerAxes) {
if (axis >= axisCount) {
continue;
}
PADAxisMapping& mapping = axes[axis];
pane.add_select_button({
.key = PADGetAxisName(mapping.padAxis),
.getValue =
[this, &mapping, gamepad] {
if (mPendingAxisMapping == &mapping) {
return pending_axis_label();
}
return native_axis_name(mapping, gamepad);
},
})
.on_pressed([this, port, &mapping] {
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
mPendingAxisMapping = &mapping;
});
}
}
pane.add_section("Digital");
if (buttons != nullptr) {
for (u32 i = 0; i < buttonCount; ++i) {
PADButtonMapping& mapping = buttons[i];
if (mapping.padButton != PAD_TRIGGER_L && mapping.padButton != PAD_TRIGGER_R) {
continue;
}
pane.add_select_button({
.key = PADGetButtonName(mapping.padButton),
.getValue =
[this, &mapping, gamepad] {
if (mPendingButtonMapping == &mapping) {
return pending_button_label();
}
return native_button_name(
gamepad, mapping.nativeButton);
},
})
.on_pressed([this, port, &mapping] {
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
mPendingButtonMapping = &mapping;
});
}
}
break;
}
case Page::Sticks: {
u32 axisCount = 0;
PADAxisMapping* axes = PADGetAxisMappings(port, &axisCount);
if (axes == nullptr) {
pane.add_text("No controller selected");
break;
}
SDL_Gamepad* gamepad = gamepad_for_port(port);
auto addAxis = [&](PADAxis axis) {
if (axis >= axisCount) {
return;
}
PADAxisMapping& mapping = axes[axis];
pane.add_select_button({
.key = PADGetAxisDirectionLabel(mapping.padAxis),
.getValue =
[this, &mapping, gamepad] {
if (mPendingAxisMapping == &mapping) {
return pending_axis_label();
}
return native_axis_name(mapping, gamepad);
},
})
.on_pressed([this, port, &mapping] {
cancel_pending_binding();
mPendingPort = port;
mPendingBindingArmed = false;
mPendingAxisMapping = &mapping;
});
};
pane.add_section("Control Stick");
addAxis(PAD_AXIS_LEFT_Y_POS);
addAxis(PAD_AXIS_LEFT_Y_NEG);
addAxis(PAD_AXIS_LEFT_X_NEG);
addAxis(PAD_AXIS_LEFT_X_POS);
pane.add_section("C Stick");
addAxis(PAD_AXIS_RIGHT_Y_POS);
addAxis(PAD_AXIS_RIGHT_Y_NEG);
addAxis(PAD_AXIS_RIGHT_X_NEG);
addAxis(PAD_AXIS_RIGHT_X_POS);
break;
}
}
}
void ControllerConfigWindow::refresh_controller_page() {
if (!visible() || mPage != Page::Controller || mRightPane == nullptr) {
return;
}
render_page(*mRightPane, mActivePort, Page::Controller);
}
void ControllerConfigWindow::poll_pending_binding() {
if (mSuppressNavigationUntilNeutral && input_neutral(mSuppressNavigationPort)) {
mSuppressNavigationUntilNeutral = false;
mSuppressNavigationPort = -1;
}
if (!capture_active()) {
return;
}
if (keyboard_escape_pressed()) {
unmap_pending_binding();
return;
}
if (!mPendingBindingArmed) {
if (pending_input_neutral()) {
mPendingBindingArmed = true;
}
return;
}
if (mPendingButtonMapping != nullptr) {
const s32 nativeButton = PADGetNativeButtonPressed(mPendingPort);
if (nativeButton != -1) {
const int completedPort = mPendingPort;
mPendingButtonMapping->nativeButton = static_cast<u32>(nativeButton);
finish_pending_binding(completedPort);
}
return;
}
if (mPendingAxisMapping != nullptr) {
const PADSignedNativeAxis nativeAxis = PADGetNativeAxisPulled(mPendingPort);
if (nativeAxis.nativeAxis != -1) {
const int completedPort = mPendingPort;
mPendingAxisMapping->nativeAxis = nativeAxis;
mPendingAxisMapping->nativeButton = -1;
finish_pending_binding(completedPort);
return;
}
const s32 nativeButton = PADGetNativeButtonPressed(mPendingPort);
if (nativeButton != -1) {
const int completedPort = mPendingPort;
mPendingAxisMapping->nativeAxis = {-1, AXIS_SIGN_POSITIVE};
mPendingAxisMapping->nativeButton = nativeButton;
finish_pending_binding(completedPort);
}
}
}
void ControllerConfigWindow::finish_pending_binding(int completedPort) {
mPendingButtonMapping = nullptr;
mPendingAxisMapping = nullptr;
mPendingPort = -1;
mPendingBindingArmed = false;
mSuppressNavigationUntilNeutral = true;
mSuppressNavigationPort = completedPort;
PADSerializeMappings();
}
void ControllerConfigWindow::unmap_pending_binding() {
if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr) {
return;
}
const int completedPort = mPendingPort;
if (mPendingButtonMapping != nullptr) {
mPendingButtonMapping->nativeButton = PAD_NATIVE_BUTTON_INVALID;
}
if (mPendingAxisMapping != nullptr) {
mPendingAxisMapping->nativeAxis = {-1, AXIS_SIGN_POSITIVE};
mPendingAxisMapping->nativeButton = -1;
}
finish_pending_binding(completedPort);
}
bool ControllerConfigWindow::capture_active() const {
return mPendingButtonMapping != nullptr || mPendingAxisMapping != nullptr;
}
bool ControllerConfigWindow::pending_input_neutral() const {
return input_neutral(mPendingPort);
}
Rml::String ControllerConfigWindow::pending_button_label() const {
return mPendingBindingArmed ? "Press a button..." : "Waiting...";
}
Rml::String ControllerConfigWindow::pending_axis_label() const {
return mPendingBindingArmed ? "Move axis or press a button..." : "Waiting...";
}
void ControllerConfigWindow::cancel_pending_binding() {
if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr &&
!mSuppressNavigationUntilNeutral)
{
return;
}
mPendingButtonMapping = nullptr;
mPendingAxisMapping = nullptr;
mPendingPort = -1;
mPendingBindingArmed = false;
mSuppressNavigationUntilNeutral = false;
mSuppressNavigationPort = -1;
}
} // namespace dusk::ui
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include "window.hpp"
#include <pad.h>
namespace dusk::ui {
class ControllerConfigWindow : public Window {
public:
ControllerConfigWindow();
void update() override;
void hide(bool close) override;
private:
enum class Page {
Controller,
Buttons,
Triggers,
Sticks,
};
void build_port_tab(Rml::Element* content, int port);
void render_page(class Pane& pane, int port, Page page);
void refresh_controller_page();
void poll_pending_binding();
void finish_pending_binding(int completedPort);
void unmap_pending_binding();
bool capture_active() const;
bool pending_input_neutral() const;
Rml::String pending_button_label() const;
Rml::String pending_axis_label() const;
void cancel_pending_binding();
Page mPage = Page::Controller;
Pane* mRightPane = nullptr;
int mActivePort = 0;
int mPendingPort = -1;
bool mPendingBindingArmed = false;
bool mSuppressNavigationUntilNeutral = false;
int mSuppressNavigationPort = -1;
PADButtonMapping* mPendingButtonMapping = nullptr;
PADAxisMapping* mPendingAxisMapping = nullptr;
};
} // namespace dusk::ui
+16 -2
View File
@@ -3,6 +3,9 @@
#include "aurora/rmlui.hpp"
#include "ui.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
namespace dusk::ui {
namespace {
@@ -17,7 +20,7 @@ Rml::ElementDocument* load_document(const Rml::String& source) {
} // namespace
Document::Document(const Rml::String& source) : mDocument(load_document(source)) {
// Block events while hidden (except for Menu command)
// Block events while hidden (except for Menu command); play nav sounds when visible
listen(
Rml::EventId::Keydown,
[this](Rml::Event& event) {
@@ -38,8 +41,18 @@ Document::Document(const Rml::String& source) : mDocument(load_document(source))
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
const auto cmd = map_nav_event(event);
if (cmd != NavCommand::None && handle_nav_command(event, cmd)) {
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);
}
});
}
@@ -100,6 +113,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);
toggle();
return true;
}
+4
View File
@@ -31,6 +31,10 @@ public:
show();
}
}
void push(std::unique_ptr<Document> document) {
push_document(std::move(document));
hide(false);
}
void pop() {
hide(true);
show_top_document();
+295 -307
View File
@@ -1295,73 +1295,71 @@ EditorWindow::EditorWindow() {
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
leftPane.add_section("Player");
leftPane
.add_child<StringButton>(StringButton::Props{
.key = "Player Name",
.getValue = get_player_name,
.setValue = set_player_name,
.maxLength = 16,
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<StringButton>(StringButton::Props{
.key = "Horse Name",
.getValue = get_horse_name,
.setValue = set_horse_name,
.maxLength = 16,
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
leftPane.register_control(leftPane.add_child<StringButton>(StringButton::Props{
.key = "Player Name",
.getValue = get_player_name,
.setValue = set_player_name,
.maxLength = 16,
}),
rightPane, {});
leftPane.register_control(leftPane.add_child<StringButton>(StringButton::Props{
.key = "Horse Name",
.getValue = get_horse_name,
.setValue = set_horse_name,
.maxLength = 16,
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Max Health",
.getValue = [] { return get_player_status()->getMaxLife(); },
.setValue = [](int value) { return get_player_status()->setMaxLife(value); },
.max = UINT16_MAX, // TODO: actual max
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Health",
.getValue = [] { return get_player_status()->getLife(); },
.setValue = [](int value) { return get_player_status()->setLife(value); },
.max = UINT16_MAX, // TODO: actual max
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Rupees",
.getValue = [] { return get_player_status()->getRupee(); },
.setValue = [](int value) { return get_player_status()->setRupee(value); },
.max = get_player_status()->getRupeeMax(),
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Max Oil",
.getValue = [] { return get_player_status()->getMaxOil(); },
.setValue = [](int value) { return get_player_status()->setMaxOil(value); },
.max = UINT16_MAX, // TODO: actual max
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Oil",
.getValue = [] { return get_player_status()->getOil(); },
.setValue = [](int value) { return get_player_status()->setOil(value); },
.max = UINT16_MAX, // TODO: actual max
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
}),
rightPane, {});
leftPane.add_section("Equipment");
const auto genSelectItemComboBox = [&leftPane, &rightPane](
const Rml::String& label, u8& selectItemData) {
leftPane
.add_select_button({
leftPane.register_control(
leftPane.add_select_button({
.key = label,
.getValue = [&selectItemData] { return item_label_for_slot(selectItemData); },
})
.on_focus([&rightPane, &selectItemData](Rml::Event&) {
populate_select_item_picker(rightPane, selectItemData);
}),
rightPane, [&selectItemData](Pane& pane) {
populate_select_item_picker(pane, selectItemData);
});
};
genSelectItemComboBox("Equip X", get_player_status()->mSelectItem[0]);
@@ -1369,80 +1367,80 @@ EditorWindow::EditorWindow() {
genSelectItemComboBox("Combo Equip X", get_player_status()->mMixItem[0]);
genSelectItemComboBox("Combo Equip Y", get_player_status()->mMixItem[1]);
leftPane
.add_select_button({
leftPane.register_control(
leftPane.add_select_button({
.key = "Clothes",
.getValue = [] { return get_item_name(get_player_status()->mSelectEquip[0]); },
})
.on_focus([&rightPane](Rml::Event&) { populate_select_clothes_picker(rightPane); });
leftPane
.add_select_button({
}),
rightPane, [](Pane& pane) { populate_select_clothes_picker(pane); });
leftPane.register_control(
leftPane.add_select_button({
.key = "Sword",
.getValue = [] { return get_item_name(get_player_status()->mSelectEquip[1]); },
})
.on_focus([&rightPane](Rml::Event&) {
}),
rightPane, [](Pane& pane) {
populate_select_equip_picker(
rightPane, get_player_status()->mSelectEquip[1], swordEntries);
pane, get_player_status()->mSelectEquip[1], swordEntries);
});
leftPane
.add_select_button({
leftPane.register_control(
leftPane.add_select_button({
.key = "Shield",
.getValue = [] { return get_item_name(get_player_status()->mSelectEquip[2]); },
})
.on_focus([&rightPane](Rml::Event&) {
}),
rightPane, [](Pane& pane) {
populate_select_equip_picker(
rightPane, get_player_status()->mSelectEquip[2], shieldEntries);
pane, get_player_status()->mSelectEquip[2], shieldEntries);
});
leftPane
.add_select_button({
leftPane.register_control(
leftPane.add_select_button({
.key = "Scent",
.getValue = [] { return get_item_name(get_player_status()->mSelectEquip[3]); },
})
.on_focus([&rightPane](Rml::Event&) {
}),
rightPane, [](Pane& pane) {
populate_select_equip_picker(
rightPane, get_player_status()->mSelectEquip[3], smellEntries);
pane, get_player_status()->mSelectEquip[3], smellEntries);
});
leftPane
.add_select_button({
leftPane.register_control(
leftPane.add_select_button({
.key = "Wallet Size",
.getValue = [] { return walletSizeNames[get_player_status()->getWalletSize()]; },
})
.on_focus([&rightPane](Rml::Event&) { populate_wallet_picker(rightPane); });
leftPane
.add_select_button({
}),
rightPane, [](Pane& pane) { populate_wallet_picker(pane); });
leftPane.register_control(
leftPane.add_select_button({
.key = "Form",
.getValue = [] { return formNames[get_player_status()->getTransformStatus()]; },
})
.on_focus([&rightPane](Rml::Event&) { populate_form_picker(rightPane); });
}),
rightPane, [](Pane& pane) { populate_form_picker(pane); });
leftPane.add_section("World");
leftPane
.add_child<NumberButton>(NumberButton::Props{
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Day",
.getValue = [] { return get_player_status_b()->getDate(); },
.setValue =
[](int value) { get_player_status_b()->setDate(static_cast<u16>(value)); },
.max = UINT16_MAX,
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Hour",
.getValue = [] { return dKy_getdaytime_hour(); },
.setValue = [](int value) { set_clock_time(value, dKy_getdaytime_minute()); },
.max = 23,
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Minute",
.getValue = [] { return dKy_getdaytime_minute(); },
.setValue = [](int value) { set_clock_time(dKy_getdaytime_hour(), value); },
.max = 59,
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Transform Level",
.getValue =
[] {
@@ -1455,10 +1453,10 @@ EditorWindow::EditorWindow() {
static_cast<u8>((1u << value) - 1u);
},
.max = 3,
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Twilight Clear Level",
.getValue =
[] {
@@ -1471,8 +1469,8 @@ EditorWindow::EditorWindow() {
static_cast<u8>((1u << value) - 1u);
},
.max = 3,
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
}),
rightPane, {});
});
add_tab("Location", [this](Rml::Element* content) {
@@ -1481,33 +1479,36 @@ EditorWindow::EditorWindow() {
leftPane.add_section("Save Location");
leftPane
.add_select_button({
.key = "Stage",
.getValue =
[] {
return stage_label_for_file(fixed_string(get_player_return_place()->mName));
},
})
.on_focus([&rightPane](Rml::Event&) {
populate_stage_picker(
rightPane, [] { return fixed_string(get_player_return_place()->mName); },
[](const char* stageFile) {
set_fixed_string(get_player_return_place()->mName, Rml::String(stageFile));
});
})
.register_control(leftPane.add_select_button({
.key = "Stage",
.getValue =
[] {
return stage_label_for_file(
fixed_string(get_player_return_place()->mName));
},
}),
rightPane,
[](Pane& pane) {
populate_stage_picker(
pane, [] { return fixed_string(get_player_return_place()->mName); },
[](const char* stageFile) {
set_fixed_string(
get_player_return_place()->mName, Rml::String(stageFile));
});
})
.set_disabled(true);
leftPane
.add_child<NumberButton>(NumberButton::Props{
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Room",
.getValue = [] { return get_player_return_place()->mRoomNo; },
.setValue =
[](int value) { get_player_return_place()->mRoomNo = static_cast<s8>(value); },
.min = std::numeric_limits<s8>::min(),
.max = std::numeric_limits<s8>::max(),
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Spawn ID",
.getValue = [] { return get_player_return_place()->mPlayerStatus; },
.setValue =
@@ -1515,74 +1516,76 @@ EditorWindow::EditorWindow() {
get_player_return_place()->mPlayerStatus = static_cast<u8>(value);
},
.max = std::numeric_limits<u8>::max(),
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
}),
rightPane, {});
leftPane.add_section("Horse Location");
leftPane
.add_child<StringButton>(StringButton::Props{
.key = "Horse Position",
.getValue =
[] {
const auto* horsePlace = get_horse_place();
return fmt::format("{}, {}, {}", static_cast<float>(horsePlace->mPos.x),
static_cast<float>(horsePlace->mPos.y),
static_cast<float>(horsePlace->mPos.z));
},
.setValue =
[](Rml::String value) {
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
if (parse_vec3(value, x, y, z)) {
auto* horsePlace = get_horse_place();
horsePlace->mPos.x = x;
horsePlace->mPos.y = y;
horsePlace->mPos.z = z;
}
},
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
leftPane.register_control(leftPane.add_child<StringButton>(StringButton::Props{
.key = "Horse Position",
.getValue =
[] {
const auto* horsePlace = get_horse_place();
return fmt::format("{}, {}, {}",
static_cast<float>(horsePlace->mPos.x),
static_cast<float>(horsePlace->mPos.y),
static_cast<float>(horsePlace->mPos.z));
},
.setValue =
[](Rml::String value) {
float x = 0.0f;
float y = 0.0f;
float z = 0.0f;
if (parse_vec3(value, x, y, z)) {
auto* horsePlace = get_horse_place();
horsePlace->mPos.x = x;
horsePlace->mPos.y = y;
horsePlace->mPos.z = z;
}
},
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Horse Angle",
.getValue = [] { return get_horse_place()->mAngleY; },
.setValue = [](int value) { get_horse_place()->mAngleY = static_cast<s16>(value); },
.min = std::numeric_limits<s16>::min(),
.max = std::numeric_limits<s16>::max(),
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
}),
rightPane, {});
leftPane
.add_select_button({
.key = "Horse Stage",
.getValue =
[] { return stage_label_for_file(fixed_string(get_horse_place()->mName)); },
})
.on_focus([&rightPane](Rml::Event&) {
populate_stage_picker(
rightPane, [] { return fixed_string(get_horse_place()->mName); },
[](const char* stageFile) {
set_fixed_string(get_horse_place()->mName, Rml::String(stageFile));
});
})
.register_control(
leftPane.add_select_button({
.key = "Horse Stage",
.getValue =
[] { return stage_label_for_file(fixed_string(get_horse_place()->mName)); },
}),
rightPane,
[](Pane& pane) {
populate_stage_picker(
pane, [] { return fixed_string(get_horse_place()->mName); },
[](const char* stageFile) {
set_fixed_string(get_horse_place()->mName, Rml::String(stageFile));
});
})
.set_disabled(true);
leftPane
.add_child<NumberButton>(NumberButton::Props{
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Horse Room",
.getValue = [] { return get_horse_place()->mRoomNo; },
.setValue = [](int value) { get_horse_place()->mRoomNo = static_cast<s8>(value); },
.min = std::numeric_limits<s8>::min(),
.max = std::numeric_limits<s8>::max(),
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Horse Spawn ID",
.getValue = [] { return get_horse_place()->mSpawnId; },
.setValue = [](int value) { get_horse_place()->mSpawnId = static_cast<u8>(value); },
.max = std::numeric_limits<u8>::max(),
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
}),
rightPane, {});
});
add_tab("Inventory", [this](Rml::Element* content) {
@@ -1590,44 +1593,41 @@ EditorWindow::EditorWindow() {
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
leftPane.add_section("Item Wheel");
leftPane.add_button("Default All")
.on_pressed([&rightPane] {
for (int slot = 0; slot < 24; ++slot) {
dComIfGs_setItem(slot, get_slot_default(slot));
}
rightPane.clear();
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane.add_button("Clear All")
.on_pressed([&rightPane] {
for (int slot = 0; slot < 24; ++slot) {
dComIfGs_setItem(slot, dItemNo_NONE_e);
}
rightPane.clear();
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane.register_control(leftPane.add_button("Default All").on_pressed([&rightPane] {
for (int slot = 0; slot < 24; ++slot) {
dComIfGs_setItem(slot, get_slot_default(slot));
}
rightPane.clear();
}),
rightPane, {});
leftPane.register_control(leftPane.add_button("Clear All").on_pressed([&rightPane] {
for (int slot = 0; slot < 24; ++slot) {
dComIfGs_setItem(slot, dItemNo_NONE_e);
}
rightPane.clear();
}),
rightPane, {});
for (int slot = 0; slot < 24; ++slot) {
leftPane
.add_select_button({
leftPane.register_control(
leftPane.add_select_button({
.key = fmt::format("Slot {0:02d}", slot),
.getValue = [slot] { return get_item_name(get_player_item()->mItems[slot]); },
})
.on_focus([&rightPane, slot](
Rml::Event&) { populate_item_slot_picker(rightPane, slot); });
}),
rightPane, [slot](Pane& pane) { populate_item_slot_picker(pane, slot); });
}
leftPane.add_section("Amounts");
leftPane
.add_child<NumberButton>(NumberButton::Props{
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Arrows Amount",
.getValue = [] { return get_player_item_record()->mArrowNum; },
.setValue =
[](int value) { get_player_item_record()->mArrowNum = static_cast<u8>(value); },
.max = std::numeric_limits<u8>::max(),
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Slingshot Amount",
.getValue = [] { return get_player_item_record()->mPachinkoNum; },
.setValue =
@@ -1635,11 +1635,11 @@ EditorWindow::EditorWindow() {
get_player_item_record()->mPachinkoNum = static_cast<u8>(value);
},
.max = std::numeric_limits<u8>::max(),
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
}),
rightPane, {});
for (int bag = 0; bag < 3; ++bag) {
leftPane
.add_child<NumberButton>(NumberButton::Props{
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = fmt::format("Bomb Bag {} Amount", bag + 1),
.getValue = [bag] { return get_player_item_record()->mBombNum[bag]; },
.setValue =
@@ -1647,12 +1647,12 @@ EditorWindow::EditorWindow() {
get_player_item_record()->mBombNum[bag] = static_cast<u8>(value);
},
.max = std::numeric_limits<u8>::max(),
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
}),
rightPane, {});
}
for (int bottle = 0; bottle < 4; ++bottle) {
leftPane
.add_child<NumberButton>(NumberButton::Props{
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = fmt::format("Bottle {} Amount", bottle + 1),
.getValue = [bottle] { return get_player_item_record()->mBottleNum[bottle]; },
.setValue =
@@ -1660,175 +1660,165 @@ EditorWindow::EditorWindow() {
get_player_item_record()->mBottleNum[bottle] = static_cast<u8>(value);
},
.max = std::numeric_limits<u8>::max(),
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
}),
rightPane, {});
}
leftPane.add_section("Capacities");
leftPane
.add_child<NumberButton>(NumberButton::Props{
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Arrows Max",
.getValue = [] { return get_player_item_max()->mItemMax[0]; },
.setValue =
[](int value) { get_player_item_max()->mItemMax[0] = static_cast<u8>(value); },
.max = std::numeric_limits<u8>::max(),
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Normal Bombs Max",
.getValue = [] { return get_player_item_max()->mItemMax[1]; },
.setValue =
[](int value) { get_player_item_max()->mItemMax[1] = static_cast<u8>(value); },
.max = std::numeric_limits<u8>::max(),
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Water Bombs Max",
.getValue = [] { return get_player_item_max()->mItemMax[2]; },
.setValue =
[](int value) { get_player_item_max()->mItemMax[2] = static_cast<u8>(value); },
.max = std::numeric_limits<u8>::max(),
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Bomblings Max",
.getValue = [] { return get_player_item_max()->mItemMax[3]; },
.setValue =
[](int value) { get_player_item_max()->mItemMax[3] = static_cast<u8>(value); },
.max = std::numeric_limits<u8>::max(),
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
}),
rightPane, {});
leftPane.add_section("Flags");
leftPane
.add_select_button({
.key = "Obtained Items",
.getValue = [] { return "Edit"; },
})
.on_focus([&rightPane](Rml::Event&) { populate_item_flag_picker(rightPane); });
leftPane.register_control(leftPane.add_select_button({
.key = "Obtained Items",
.getValue = [] { return "Edit"; },
}),
rightPane, [](Pane& pane) { populate_item_flag_picker(pane); });
});
add_tab("Collection", [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("Equipment");
leftPane
.add_select_button({
leftPane.register_control(
leftPane.add_select_button({
.key = "Swords",
.getValue =
[] {
return count_label(
count_item_first_bits(swordEntries), swordEntries.size());
},
})
.on_focus([&rightPane](Rml::Event&) {
populate_toggle_group(rightPane, item_toggle_entries(swordEntries));
});
leftPane
.add_select_button({
}),
rightPane,
[](Pane& pane) { populate_toggle_group(pane, item_toggle_entries(swordEntries)); });
leftPane.register_control(
leftPane.add_select_button({
.key = "Shields",
.getValue =
[] {
return count_label(
count_item_first_bits(shieldEntries), shieldEntries.size());
},
})
.on_focus([&rightPane](Rml::Event&) {
populate_toggle_group(rightPane, item_toggle_entries(shieldEntries));
});
leftPane
.add_select_button({
.key = "Clothing",
.getValue = [] { return count_label(count_clothing(), 4); },
})
.on_focus([&rightPane](Rml::Event&) { populate_collect_clothes_picker(rightPane); });
}),
rightPane,
[](Pane& pane) { populate_toggle_group(pane, item_toggle_entries(shieldEntries)); });
leftPane.register_control(leftPane.add_select_button({
.key = "Clothing",
.getValue = [] { return count_label(count_clothing(), 4); },
}),
rightPane, [](Pane& pane) { populate_collect_clothes_picker(pane); });
leftPane.add_section("Key Items");
leftPane
.add_select_button({
leftPane.register_control(
leftPane.add_select_button({
.key = "Fused Shadows",
.getValue =
[] {
return count_label(
count_collect_crystals(fusedShadowEntries), fusedShadowEntries.size());
},
})
.on_focus([&rightPane](Rml::Event&) {
populate_toggle_group(
rightPane, collect_crystal_toggle_entries(fusedShadowEntries));
}),
rightPane, [](Pane& pane) {
populate_toggle_group(pane, collect_crystal_toggle_entries(fusedShadowEntries));
});
leftPane
.add_select_button({
leftPane.register_control(
leftPane.add_select_button({
.key = "Mirror Shards",
.getValue =
[] {
return count_label(
count_collect_mirrors(mirrorShardEntries), mirrorShardEntries.size());
},
})
.on_focus([&rightPane](Rml::Event&) {
populate_toggle_group(rightPane, collect_mirror_toggle_entries(mirrorShardEntries));
}),
rightPane, [](Pane& pane) {
populate_toggle_group(pane, collect_mirror_toggle_entries(mirrorShardEntries));
});
leftPane.add_section("Health & Souls");
leftPane
.add_select_button({
leftPane.register_control(
leftPane.add_select_button({
.key = "Poe Souls",
.getValue = [] { return fmt::format("{} / 60", dComIfGs_getPohSpiritNum()); },
})
.on_focus([&rightPane](Rml::Event&) { populate_poe_souls_picker(rightPane); });
leftPane
.add_select_button({
.key = "Max Life",
.getValue = [] { return max_life_label(); },
})
.on_focus([&rightPane](Rml::Event&) { populate_max_life_picker(rightPane); });
}),
rightPane, [](Pane& pane) { populate_poe_souls_picker(pane); });
leftPane.register_control(leftPane.add_select_button({
.key = "Max Life",
.getValue = [] { return max_life_label(); },
}),
rightPane, [](Pane& pane) { populate_max_life_picker(pane); });
leftPane.add_section("Golden Bugs");
for (const auto& bug : bugSpeciesEntries) {
leftPane
.add_select_button({
.key = bug.name,
.getValue = [bug] { return bug_species_label(bug); },
})
.on_focus([&rightPane, bug](
Rml::Event&) { populate_bug_species_picker(rightPane, bug); });
leftPane.register_control(leftPane.add_select_button({
.key = bug.name,
.getValue = [bug] { return bug_species_label(bug); },
}),
rightPane, [bug](Pane& pane) { populate_bug_species_picker(pane, bug); });
}
leftPane.add_section("Skills");
leftPane
.add_select_button({
leftPane.register_control(
leftPane.add_select_button({
.key = "Hidden Skills",
.getValue =
[] {
return count_label(
count_event_bits(hiddenSkillEntries), hiddenSkillEntries.size());
},
})
.on_focus([&rightPane](Rml::Event&) {
populate_toggle_group(rightPane, event_toggle_entries(hiddenSkillEntries));
}),
rightPane, [](Pane& pane) {
populate_toggle_group(pane, event_toggle_entries(hiddenSkillEntries));
});
leftPane.add_section("Logs");
leftPane
.add_select_button({
leftPane.register_control(
leftPane.add_select_button({
.key = "Postman Letters",
.getValue = [] { return count_label(count_letters(), letterSenders.size()); },
})
.on_focus([&rightPane](Rml::Event&) { populate_letters_picker(rightPane); });
}),
rightPane, [](Pane& pane) { populate_letters_picker(pane); });
leftPane.add_section("Fishing Log");
for (const auto& fish : fishSpeciesEntries) {
leftPane
.add_select_button({
.key = fish.name,
.getValue = [fish] { return fish_species_label(fish); },
})
.on_focus([&rightPane, fish](
Rml::Event&) { populate_fish_species_picker(rightPane, fish); });
leftPane.register_control(leftPane.add_select_button({
.key = fish.name,
.getValue = [fish] { return fish_species_label(fish); },
}),
rightPane, [fish](Pane& pane) { populate_fish_species_picker(pane, fish); });
}
});
@@ -1841,8 +1831,8 @@ EditorWindow::EditorWindow() {
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
leftPane.add_section("Records");
leftPane
.add_child<NumberButton>(NumberButton::Props{
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "STAR Game Time (ms)",
.getValue =
[] {
@@ -1854,10 +1844,10 @@ EditorWindow::EditorWindow() {
get_minigame()->setHookGameTime(static_cast<u32>(std::max(0, value)));
},
.max = std::numeric_limits<int>::max(),
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Snowboard Race Time (ms)",
.getValue =
[] {
@@ -1869,10 +1859,10 @@ EditorWindow::EditorWindow() {
get_minigame()->setRaceGameTime(static_cast<u32>(std::max(0, value)));
},
.max = std::numeric_limits<int>::max(),
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_child<NumberButton>(NumberButton::Props{
}),
rightPane, {});
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Fruit-Pop-Flight Score",
.getValue =
[] {
@@ -1884,8 +1874,8 @@ EditorWindow::EditorWindow() {
get_minigame()->setBalloonScore(static_cast<u32>(std::max(0, value)));
},
.max = std::numeric_limits<int>::max(),
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
}),
rightPane, {});
});
add_tab("Config", [this](Rml::Element* content) {
@@ -1893,25 +1883,23 @@ EditorWindow::EditorWindow() {
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
leftPane.add_section("Options");
leftPane
.add_child<BoolButton>(BoolButton::Props{
leftPane.register_control(
leftPane.add_child<BoolButton>(BoolButton::Props{
.key = "Enable Vibration",
.getValue = [] { return get_player_config()->getVibration() != 0; },
.setValue = [](bool value) { get_player_config()->setVibration(value); },
})
.on_focus([&rightPane](Rml::Event&) { rightPane.clear(); });
leftPane
.add_select_button({
.key = "Target Type",
.getValue = [] { return target_type_label(); },
})
.on_focus([&rightPane](Rml::Event&) { populate_target_type_picker(rightPane); });
leftPane
.add_select_button({
.key = "Sound",
.getValue = [] { return sound_mode_label(); },
})
.on_focus([&rightPane](Rml::Event&) { populate_sound_mode_picker(rightPane); });
}),
rightPane, {});
leftPane.register_control(leftPane.add_select_button({
.key = "Target Type",
.getValue = [] { return target_type_label(); },
}),
rightPane, [](Pane& pane) { populate_target_type_picker(pane); });
leftPane.register_control(leftPane.add_select_button({
.key = "Sound",
.getValue = [] { return sound_mode_label(); },
}),
rightPane, [](Pane& pane) { populate_sound_mode_picker(pane); });
});
}
+12 -1
View File
@@ -10,9 +10,20 @@ ScopedEventListener::ScopedEventListener(
mElement->AddEventListener(mEvent, this, mCapture);
}
ScopedEventListener::ScopedEventListener(
Rml::Element* element, Rml::String event, Callback callback, bool capture)
: mElement(element), mEventName(std::move(event)), mCapture(capture),
mCallback(std::move(callback)) {
mElement->AddEventListener(mEventName, this, mCapture);
}
ScopedEventListener::~ScopedEventListener() {
if (mElement != nullptr) {
mElement->RemoveEventListener(mEvent, this, mCapture);
if (!mEventName.empty()) {
mElement->RemoveEventListener(mEventName, this, mCapture);
} else {
mElement->RemoveEventListener(mEvent, this, mCapture);
}
mElement = nullptr;
}
}
+3
View File
@@ -12,6 +12,8 @@ public:
ScopedEventListener(
Rml::Element* element, Rml::EventId event, Callback callback, bool capture = false);
ScopedEventListener(
Rml::Element* element, Rml::String event, Callback callback, bool capture = false);
~ScopedEventListener() override;
ScopedEventListener(const ScopedEventListener&) = delete;
@@ -25,6 +27,7 @@ public:
private:
Rml::Element* mElement = nullptr;
Rml::EventId mEvent = Rml::EventId::Invalid;
Rml::String mEventName;
bool mCapture = false;
Callback mCallback;
};
+201 -14
View File
@@ -5,6 +5,7 @@
#include <RmlUi/Core.h>
#include <SDL3/SDL_gamepad.h>
#include <SDL3/SDL_timer.h>
#include <SDL3/SDL_touch.h>
#include <aurora/rmlui.hpp>
#include <dolphin/pad.h>
@@ -22,6 +23,10 @@ constexpr double kGamepadMenuChordGraceDuration = 0.12;
constexpr Sint16 kGamepadAxisPressThreshold = 16384;
constexpr Sint16 kGamepadAxisReleaseThreshold = 12000;
constexpr int kGamepadAxisDirectionCount = SDL_GAMEPAD_AXIS_COUNT * 2;
constexpr int kMenuTapFingerCount = 3;
constexpr float kMenuTapMoveThreshold = 12.0f;
constexpr double kMenuTapMaxDownSpan = 0.18;
constexpr double kMenuTapMaxDuration = 0.55;
struct GamepadRepeatState {
Rml::Input::KeyIdentifier key = Rml::Input::KI_UNKNOWN;
@@ -32,11 +37,26 @@ struct GamepadRepeatState {
bool pending = false;
};
struct TouchTapFinger {
SDL_FingerID id = 0;
Rml::Vector2f startPosition;
bool active = false;
};
struct TouchTapState {
std::array<TouchTapFinger, kMenuTapFingerCount> fingers;
int activeCount = 0;
double firstDownAt = 0.0;
bool candidate = false;
bool failed = false;
};
bool sPadInputBlocked = false;
std::array<GamepadRepeatState, SDL_GAMEPAD_BUTTON_COUNT> sGamepadButtonRepeats;
std::array<GamepadRepeatState, kGamepadAxisDirectionCount> sGamepadAxisRepeats;
std::array<u32, PAD_MAX_CONTROLLERS> sPadHoldMasks;
std::array<bool, PAD_MAX_CONTROLLERS> sMenuChordConsumed;
TouchTapState sTouchMenuTap;
double now_seconds() noexcept {
return static_cast<double>(SDL_GetTicksNS()) / 1000000000.0;
@@ -55,13 +75,38 @@ bool has_menu_chord_part_held(u32 port) noexcept {
return (held & (PAD_TRIGGER_R | PAD_BUTTON_START)) != 0;
}
bool should_block_pad_for_menu_chord() noexcept {
for (u32 port = 0; port < sPadHoldMasks.size(); ++port) {
if (sMenuChordConsumed[port] && has_menu_chord_part_held(port)) {
return true;
}
const char* controller_change_type(Uint32 eventType) noexcept {
switch (eventType) {
case SDL_EVENT_GAMEPAD_ADDED:
return "added";
case SDL_EVENT_GAMEPAD_REMOVED:
return "removed";
case SDL_EVENT_GAMEPAD_REMAPPED:
return "remapped";
default:
return nullptr;
}
return false;
}
void dispatch_controller_change_event(const SDL_Event& event) noexcept {
const char* type = controller_change_type(event.type);
if (type == nullptr) {
return;
}
auto* context = aurora::rmlui::get_context();
if (context == nullptr) {
return;
}
auto* root = context->GetRootElement();
if (root == nullptr) {
return;
}
Rml::Dictionary parameters;
parameters["type"] = Rml::String(type);
parameters["which"] = static_cast<int>(event.gdevice.which);
root->DispatchEvent("controllerchange", parameters);
}
PADButton pad_button_from_axis(PADAxis axis) noexcept {
@@ -355,6 +400,133 @@ void clear_gamepad_repeats() noexcept {
sMenuChordConsumed.fill(false);
}
void reset_touch_menu_tap() noexcept {
sTouchMenuTap = {};
}
Rml::Vector2f touch_position(const SDL_TouchFingerEvent& event, Rml::Context& context) noexcept {
const auto dimensions = context.GetDimensions();
return {
event.x * static_cast<float>(dimensions.x),
event.y * static_cast<float>(dimensions.y),
};
}
TouchTapFinger* find_touch_finger(SDL_FingerID id) noexcept {
for (auto& finger : sTouchMenuTap.fingers) {
if (finger.active && finger.id == id) {
return &finger;
}
}
return nullptr;
}
TouchTapFinger* find_free_touch_finger() noexcept {
for (auto& finger : sTouchMenuTap.fingers) {
if (!finger.active) {
return &finger;
}
}
return nullptr;
}
bool touch_moved_too_far(
const TouchTapFinger& finger, Rml::Vector2f position, Rml::Context& context) noexcept {
const Rml::Vector2f delta = position - finger.startPosition;
const float threshold =
kMenuTapMoveThreshold * std::max(context.GetDensityIndependentPixelRatio(), 1.0f);
return delta.SquaredMagnitude() > threshold * threshold;
}
void dispatch_menu_key(Rml::Context& context) noexcept {
context.ProcessMouseLeave();
context.ProcessKeyDown(Rml::Input::KI_F1, 0);
context.ProcessKeyUp(Rml::Input::KI_F1, 0);
}
bool handle_touch_menu_tap(Rml::Context& context, const SDL_Event& event) noexcept {
switch (event.type) {
case SDL_EVENT_FINGER_DOWN: {
const double now = now_seconds();
if (sTouchMenuTap.activeCount == 0) {
reset_touch_menu_tap();
sTouchMenuTap.firstDownAt = now;
}
if (sTouchMenuTap.candidate || sTouchMenuTap.activeCount >= kMenuTapFingerCount ||
find_touch_finger(event.tfinger.fingerID) != nullptr)
{
sTouchMenuTap.failed = true;
return false;
}
auto* finger = find_free_touch_finger();
if (finger == nullptr) {
sTouchMenuTap.failed = true;
return false;
}
*finger = TouchTapFinger{
.id = event.tfinger.fingerID,
.startPosition = touch_position(event.tfinger, context),
.active = true,
};
sTouchMenuTap.activeCount++;
if (now - sTouchMenuTap.firstDownAt > kMenuTapMaxDownSpan) {
sTouchMenuTap.failed = true;
}
if (sTouchMenuTap.activeCount == kMenuTapFingerCount) {
sTouchMenuTap.candidate = true;
}
return false;
}
case SDL_EVENT_FINGER_MOTION: {
auto* finger = find_touch_finger(event.tfinger.fingerID);
if (finger == nullptr) {
return false;
}
if (touch_moved_too_far(*finger, touch_position(event.tfinger, context), context)) {
sTouchMenuTap.failed = true;
}
return false;
}
case SDL_EVENT_FINGER_UP: {
auto* finger = find_touch_finger(event.tfinger.fingerID);
if (finger == nullptr) {
return false;
}
const double now = now_seconds();
if (!sTouchMenuTap.candidate ||
touch_moved_too_far(*finger, touch_position(event.tfinger, context), context))
{
sTouchMenuTap.failed = true;
}
*finger = {};
sTouchMenuTap.activeCount--;
if (sTouchMenuTap.activeCount > 0) {
return false;
}
const bool shouldDispatch = sTouchMenuTap.candidate && !sTouchMenuTap.failed &&
now - sTouchMenuTap.firstDownAt <= kMenuTapMaxDuration;
reset_touch_menu_tap();
if (shouldDispatch) {
dispatch_menu_key(context);
return true;
}
return false;
}
case SDL_EVENT_FINGER_CANCELED:
reset_touch_menu_tap();
return false;
default:
return false;
}
}
void begin_gamepad_key(GamepadRepeatState& repeat, Rml::Input::KeyIdentifier key) noexcept {
if (repeat.held) {
return;
@@ -454,7 +626,8 @@ void process_axis_direction(
}
set_pad_button_held(port, heldPadButton, true);
const bool chorded = heldPadButton == PAD_TRIGGER_R && is_menu_chord(port);
const bool chorded = heldPadButton == PAD_TRIGGER_R && is_menu_chord(port) &&
(port >= sMenuChordConsumed.size() || !sMenuChordConsumed[port]);
if (chorded) {
consume_menu_chord(port, context);
}
@@ -476,7 +649,7 @@ void process_axis_direction(
} // namespace
void sync_input_block() noexcept {
const bool shouldBlock = any_document_visible() || should_block_pad_for_menu_chord();
const bool shouldBlock = any_document_visible();
if (sPadInputBlocked == shouldBlock) {
return;
}
@@ -496,25 +669,39 @@ void release_input_block() noexcept {
void reset_input_state() noexcept {
clear_gamepad_repeats();
reset_touch_menu_tap();
}
void handle_event(const SDL_Event& event) noexcept {
if (event.type == SDL_EVENT_GAMEPAD_REMOVED || event.type == SDL_EVENT_WINDOW_FOCUS_LOST) {
reset_input_state();
sync_input_block();
return;
}
if (event.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN && event.type != SDL_EVENT_GAMEPAD_BUTTON_UP &&
event.type != SDL_EVENT_GAMEPAD_AXIS_MOTION)
{
return;
if (event.type != SDL_EVENT_GAMEPAD_REMOVED) {
return;
}
}
dispatch_controller_change_event(event);
auto* context = aurora::rmlui::get_context();
if (context == nullptr) {
return;
}
if (event.type == SDL_EVENT_FINGER_DOWN || event.type == SDL_EVENT_FINGER_MOTION ||
event.type == SDL_EVENT_FINGER_UP || event.type == SDL_EVENT_FINGER_CANCELED)
{
if (handle_touch_menu_tap(*context, event)) {
sync_input_block();
}
return;
}
if (event.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN && event.type != SDL_EVENT_GAMEPAD_BUTTON_UP &&
event.type != SDL_EVENT_GAMEPAD_AXIS_MOTION)
{
return;
}
if (event.type == SDL_EVENT_GAMEPAD_AXIS_MOTION) {
process_axis_direction(*context, event.gaxis, AXIS_SIGN_POSITIVE);
process_axis_direction(*context, event.gaxis, AXIS_SIGN_NEGATIVE);
+20 -7
View File
@@ -1,5 +1,8 @@
#include "number_button.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
#include <charconv>
#include <fmt/format.h>
@@ -8,8 +11,16 @@ namespace dusk::ui {
NumberButton::NumberButton(Rml::Element* parent, Props props)
: BaseStringButton(parent, {.key = std::move(props.key), .type = "number"}),
mGetValue(std::move(props.getValue)), mSetValue(std::move(props.setValue)),
mIsDisabled(std::move(props.isDisabled)), mMin(props.min), mMax(props.max), mStep(props.step),
mPrefix(std::move(props.prefix)), mSuffix(std::move(props.suffix)) {}
mIsDisabled(std::move(props.isDisabled)), mIsModified(std::move(props.isModified)),
mMin(props.min), mMax(props.max), mStep(props.step), mPrefix(std::move(props.prefix)),
mSuffix(std::move(props.suffix)) {}
bool NumberButton::modified() const {
if (mIsModified) {
return mIsModified();
}
return BaseStringButton::modified();
}
bool NumberButton::disabled() const {
if (mIsDisabled) {
@@ -43,11 +54,13 @@ void NumberButton::set_value(Rml::String value) {
}
bool NumberButton::handle_nav_command(NavCommand cmd) {
if (cmd == NavCommand::Left) {
mSetValue(std::clamp(mGetValue() - mStep, mMin, mMax));
return true;
} else if (cmd == NavCommand::Right) {
mSetValue(std::clamp(mGetValue() + mStep, mMin, mMax));
if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
const int newValue = std::clamp(
mGetValue() + (cmd == NavCommand::Right ? mStep : -mStep), mMin, mMax);
if (newValue != mGetValue()) {
mSetValue(newValue);
mDoAud_seStartMenu(Z2SE_SY_NAME_CURSOR);
}
return true;
}
return BaseStringButton::handle_nav_command(cmd);
+3
View File
@@ -11,6 +11,7 @@ public:
std::function<int()> getValue;
std::function<void(int)> setValue;
std::function<bool()> isDisabled;
std::function<bool()> isModified;
int min = 0;
int max = INT_MAX;
int step = 1;
@@ -20,6 +21,7 @@ public:
NumberButton(Rml::Element* parent, Props props);
bool modified() const override;
bool disabled() const override;
protected:
@@ -32,6 +34,7 @@ private:
std::function<int()> mGetValue;
std::function<void(int)> mSetValue;
std::function<bool()> mIsDisabled;
std::function<bool()> mIsModified;
int mMin;
int mMax;
int mStep;
+10 -5
View File
@@ -1,5 +1,8 @@
#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>
@@ -84,7 +87,7 @@ Rml::Element* create_stepped_carousel_arrow(
auto button = doc->CreateElement("button");
button->SetClass("stepped-carousel-arrow", true);
button->SetClass(className, true);
button->SetInnerRML(escape(label));
button->SetInnerRML(label);
return parent->AppendChild(std::move(button));
}
@@ -92,10 +95,10 @@ Rml::Element* create_stepped_carousel_arrow(
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", "<");
Rml::Element* prevElem = create_stepped_carousel_arrow(mRoot, "prev", "&#xe5cb;");
mValueElem = append(mRoot, "div");
mValueElem->SetClass("stepped-carousel-value", true);
Rml::Element* nextElem = create_stepped_carousel_arrow(mRoot, "next", ">");
Rml::Element* nextElem = create_stepped_carousel_arrow(mRoot, "next", "&#xe5cc;");
listen(prevElem, Rml::EventId::Click,
[this](Rml::Event&) { handle_nav_command(NavCommand::Left); });
@@ -146,6 +149,7 @@ void SteppedCarousel::apply(int value) {
if (nextValue == currentValue) {
return;
}
mDoAud_seStartMenu(Z2SE_SY_NAME_CURSOR);
if (mProps.onChange) {
mProps.onChange(nextValue);
}
@@ -160,10 +164,10 @@ Rml::String format_graphics_setting_value(GraphicsOption option, int value) {
u32 width = 0;
u32 height = 0;
AuroraGetRenderSize(&width, &height);
return fmt::format("{}x ({}x{})", value, width, height);
return fmt::format("{}× ({}×{})", value, width, height);
}
case GraphicsOption::ShadowResolution:
return fmt::format("{}x", value);
return fmt::format("{}×", value);
case GraphicsOption::BloomMode:
switch (static_cast<BloomMode>(value)) {
case BloomMode::Off:
@@ -229,6 +233,7 @@ Overlay::Overlay(OverlayProps props)
}
void Overlay::show() {
mDoAud_seStartMenu(Z2SE_SY_CURSOR_OK);
Document::show();
mRoot->SetAttribute("open", "");
}
+43
View File
@@ -1,5 +1,7 @@
#include "pane.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
#include "ui.hpp"
namespace dusk::ui {
@@ -56,6 +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);
event.StopPropagation();
break;
}
@@ -72,6 +75,12 @@ Pane::Pane(Rml::Element* parent, Type type) : FluentComponent(createRoot(parent)
childIndex = i;
}
}
// If item already selected, deselect
if (childIndex >= 0 && childIndex < mChildren.size() &&
mChildren[childIndex]->selected())
{
childIndex = -1;
}
set_selected_item(childIndex);
// If the selection was handled locally, don't allow it to bubble up to window
if (event.GetParameter("handled", false)) {
@@ -95,6 +104,40 @@ void Pane::set_selected_item(int index) {
}
}
Component& Pane::register_control(
Component& component, Pane& nextPane, std::function<void(Pane&)> callback) {
component.listen(component.root(), Rml::EventId::Mouseover,
[this, &component, &nextPane, callback](Rml::Event&) {
if (component.disabled()) {
return;
}
bool anySelected = false;
for (const auto& child : mChildren) {
if (child->selected()) {
anySelected = true;
break;
}
}
if (!anySelected) {
nextPane.clear();
if (callback) {
callback(nextPane);
}
}
});
component.listen(component.root(), Rml::EventId::Focus,
[&component, &nextPane, callback = std::move(callback)](Rml::Event&) {
if (component.disabled()) {
return;
}
nextPane.clear();
if (callback) {
callback(nextPane);
}
});
return component;
}
bool Pane::focus() {
// Focus the first selected child
for (const auto& child : mChildren) {
+2
View File
@@ -19,6 +19,8 @@ public:
void update() override;
void set_selected_item(int index);
Component& register_control(
Component& component, Pane& nextPane, std::function<void(Pane&)> callback);
Rml::Element* add_section(const Rml::String& text);
ControlledButton& add_button(ControlledButton::Props props) {
+37 -13
View File
@@ -2,9 +2,16 @@
#include <RmlUi/Core.h>
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
#include "achievements.hpp"
#include "aurora/rmlui.hpp"
#include "dusk/main.h"
#include "dusk/settings.h"
#include "editor.hpp"
#include "f_pc/f_pc_manager.h"
#include "f_pc/f_pc_name.h"
#include "imgui.h"
#include "settings.hpp"
#include "ui.hpp"
@@ -23,7 +30,7 @@ const Rml::String kDocumentSource = R"RML(
<link type="text/rcss" href="res/rml/popup.rcss" />
</head>
<body>
<popup id="popup"></div>
<popup id="popup"></popup>
</body>
</rml>
)RML";
@@ -31,18 +38,25 @@ const Rml::String kDocumentSource = R"RML(
}
Popup::Popup() : Document(kDocumentSource), mRoot(mDocument->GetElementById("popup")) {
mTabBar = std::make_unique<TabBar>(mRoot, TabBar::Props{.autoSelect = false});
mTabBar->add_tab("Settings", [] { push_document(std::make_unique<SettingsWindow>()); });
mTabBar->add_tab("Warp", [] {
// TODO
});
mTabBar->add_tab("Editor", [] { push_document(std::make_unique<EditorWindow>()); });
mTabBar = std::make_unique<TabBar>(mRoot, TabBar::Props{
.onClose = [this] { hide(false); },
.autoSelect = false,
});
mTabBar->add_tab("Settings", [this] { push(std::make_unique<SettingsWindow>()); });
// mTabBar->add_tab("Warp", [] {
// // TODO
// });
mTabBar->add_tab("Editor", [this] { push(std::make_unique<EditorWindow>()); });
mTabBar->add_tab("Achievements", [this] { push(std::make_unique<AchievementsWindow>()); });
mTabBar->add_tab("Reset", [this] {
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
mTabBar->set_active_tab(-1);
if (fpcM_SearchByName(fpcNm_LOGO_SCENE_e)) {
return;
}
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
hide(false);
});
mTabBar->add_tab("Exit", [] { IsRunning = false; });
mTabBar->add_tab("Quit", [] { IsRunning = false; });
// Hide document after transition completion
listen(mRoot, Rml::EventId::Transitionend, [this](Rml::Event& event) {
@@ -52,18 +66,19 @@ Popup::Popup() : Document(kDocumentSource), mRoot(mDocument->GetElementById("pop
Document::hide(mPendingClose);
}
});
// We start hidden, but want focus for an open nav event
mDocument->Focus();
}
void Popup::show() {
Document::show();
mRoot->SetAttribute("open", "");
mTabBar->set_active_tab(-1);
if (!mTabBar->focus_tab(mFocusedTabIndex)) {
mTabBar->focus();
}
}
void Popup::hide(bool close) {
mFocusedTabIndex = mTabBar->focused_tab_index();
mRoot->RemoveAttribute("open");
if (close) {
mPendingClose = true;
@@ -106,6 +121,11 @@ void Popup::update_safe_area() noexcept {
Rml::PropertyId::PaddingRight, Rml::Property(safeInsets.right, Rml::Unit::PX));
tabBar->SetProperty(
Rml::PropertyId::PaddingLeft, Rml::Property(safeInsets.left, Rml::Unit::PX));
if (auto* close = tabBar->QuerySelector("close")) {
close->SetProperty(Rml::PropertyId::Right,
Rml::Property(safeInsets.right + 8.0f * context->GetDensityIndependentPixelRatio(),
Rml::Unit::PX));
}
}
bool Popup::visible() const {
@@ -113,7 +133,11 @@ bool Popup::visible() const {
}
bool Popup::handle_nav_command(Rml::Event& event, NavCommand cmd) {
if (cmd == NavCommand::Cancel) {
if (!getSettings().backend.wasPresetChosen) {
return true;
}
if (cmd == NavCommand::Cancel && visible()) {
mDoAud_seStartMenu(Z2SE_SY_MENU_OUT);
hide(false);
return true;
}
+1
View File
@@ -32,6 +32,7 @@ private:
std::unique_ptr<Button> mCloseButton;
Insets mTabBarPadding;
float mTopMargin = 0.f;
int mFocusedTabIndex = -1;
};
} // namespace dusk::ui
+28 -20
View File
@@ -28,10 +28,13 @@ const Rml::String kDocumentSource = R"RML(
</hero>
<div id="menu-list" />
</menu>
<disk-status class="intro-item delay-4">
<span id="status" class="status" />
<span id="detail" class="detail" />
</disk-status>
<disc-info class="intro-item delay-4">
<div id="disc-status">
<icon />
<span id="disc-status-label" />
</div>
<span id="disc-version" class="detail" />
</disc-info>
<version-info class="intro-item delay-5">
<div class="version">Version <span id="version-text"></span></div>
<div class="update"><span>Update available!</span> Download</div>
@@ -113,7 +116,7 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
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 Disk Image"));
std::make_unique<Button>(menuList, hasValidPath ? "Start Game" : "Select Disc Image"));
mMenuButtons.back()->on_pressed([this] {
if (!is_selected_path_valid()) {
open_iso_picker();
@@ -125,8 +128,7 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
apply_intro_animation(mMenuButtons.back()->root(), "delay-1");
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Options"));
mMenuButtons.back()->on_pressed(
[] { push_document(std::make_unique<PrelaunchOptions>()); });
mMenuButtons.back()->on_pressed([this] { push(std::make_unique<PrelaunchOptions>()); });
apply_intro_animation(mMenuButtons.back()->root(), "delay-2");
mMenuButtons.push_back(std::make_unique<Button>(menuList, "Quit To Desktop"));
@@ -134,8 +136,8 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
apply_intro_animation(mMenuButtons.back()->root(), "delay-3");
}
mDiscStatus = mDocument->GetElementById("status");
mDiscDetail = mDocument->GetElementById("detail");
mDiscStatus = mDocument->GetElementById("disc-status");
mDiscDetail = mDocument->GetElementById("disc-version");
mVersion = mDocument->GetElementById("version-text");
listen(mDocument, Rml::EventId::Transitionend, [this](Rml::Event& event) {
@@ -186,27 +188,34 @@ void Prelaunch::update() {
}
if (!mMenuButtons.empty()) {
mMenuButtons[0]->set_text(hasValidPath ? "Start Game" : "Select Disk Image");
mMenuButtons[0]->set_text(hasValidPath ? "Start Game" : "Select Disc Image");
}
if (mDiscStatus != nullptr) {
const auto discStatusLabel = mDiscStatus->GetElementById("disc-status-label");
if (mDiscStatus != nullptr && discStatusLabel != nullptr) {
if (hasValidPath) {
mDiscStatus->RemoveAttribute("bad");
mDiscStatus->SetInnerRML("Disc Ready");
mDiscStatus->SetAttribute("status", "good");
discStatusLabel->SetInnerRML("Disc ready.");
} else {
mDiscStatus->SetAttribute("bad", "");
mDiscStatus->SetInnerRML("Disk Not Found");
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 • PAL" : "GameCube • USA");
mDiscDetail->SetInnerRML(state.isPal ? "GameCube • EUR" : "GameCube • USA");
} else {
mDiscDetail->SetProperty(Rml::PropertyId::Display, Rml::Style::Display::None);
}
}
if (mVersion != nullptr) {
mVersion->SetInnerRML(escape(DUSK_WC_DESCRIBE));
std::string_view versionStr(DUSK_WC_DESCRIBE);
if (versionStr[0] == 'v') {
versionStr = versionStr.substr(1);
}
mVersion->SetInnerRML(escape(versionStr));
}
Document::update();
@@ -240,9 +249,8 @@ bool Prelaunch::handle_nav_command(Rml::Event& event, NavCommand cmd) {
break;
}
}
const auto buttonCount = static_cast<int>(mMenuButtons.size());
int i = (focusedButton + direction) % buttonCount;
if (i < 0) i += buttonCount;
const auto n = static_cast<int>(mMenuButtons.size());
int i = ((focusedButton + direction) % n + n) % n;
while (i >= 0 && i < mMenuButtons.size()) {
if (mMenuButtons[i]->focus()) {
event.StopPropagation();
+187
View File
@@ -0,0 +1,187 @@
#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>
namespace dusk::ui {
namespace {
void applyPresetClassic() {
auto& s = getSettings();
s.video.lockAspectRatio.setValue(true);
s.game.bloomMode.setValue(BloomMode::Classic);
s.game.enableAchievementNotifications.setValue(false);
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);
s.game.fastClimbing.setValue(true);
s.game.noMissClimbing.setValue(true);
s.game.fastTears.setValue(true);
s.game.biggerWallets.setValue(true);
s.game.invertCameraXAxis.setValue(true);
s.game.no2ndFishForCat.setValue(true);
s.game.enableAchievementNotifications.setValue(true);
s.game.enableQuickTransform.setValue(true);
s.game.instantSaves.setValue(true);
s.game.midnasLamentNonStop.setValue(true);
s.game.enableFrameInterpolation.setValue(true);
s.game.sunsSong.setValue(true);
s.game.bloomMode.setValue(BloomMode::Dusk);
s.game.internalResolutionScale.setValue(0);
s.game.shadowResolutionMultiplier.setValue(4);
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");
title->SetClass("preset-title", true);
title->SetInnerRML("Welcome to Dusk!");
auto* intro = createElement(dialog, "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");
grid->SetClass("preset-grid", true);
struct PresetInfo {
const char* name;
const char* desc;
void (*apply)();
};
static constexpr PresetInfo kPresets[] = {
{"Classic",
"Enhancements disabled to match the GameCube version. "
"Good for speedrunning or simple nostalgia!",
applyPresetClassic},
{"Dusk",
"Graphics & quality of life tweaks, including some from the Wii U version. "
"Our recommended way to play!",
applyPresetDusk},
};
for (const auto& preset : kPresets) {
auto* col = createElement(grid, "div");
col->SetClass("preset-col", true);
auto btn = std::make_unique<Button>(col, Rml::String(preset.name));
btn->root()->SetClass("preset-btn", true);
btn->on_nav_command([this, apply = preset.apply](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm) {
apply();
getSettings().backend.wasPresetChosen.setValue(true);
config::Save();
hide(true);
return true;
}
return false;
});
mButtons.push_back(std::move(btn));
auto* desc = createElement(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();
}
return false;
}
bool PresetWindow::handle_nav_command(Rml::Event& event, NavCommand cmd) {
if (cmd == NavCommand::Cancel || cmd == NavCommand::Menu) {
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())) {
if (mButtons[next]->focus()) {
mDoAud_seStartMenu(Z2SE_SY_NAME_CURSOR);
return true;
}
}
return false;
}
}
return false;
}
} // namespace dusk::ui
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include "component.hpp"
#include "document.hpp"
#include <memory>
#include <vector>
namespace dusk::ui {
class PresetWindow : public Document {
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;
};
} // namespace dusk::ui
+55 -2
View File
@@ -2,6 +2,7 @@
#include "ui.hpp"
#include <fmt/format.h>
#include <utility>
namespace dusk::ui {
@@ -18,28 +19,72 @@ Rml::Element* createRoot(Rml::Element* parent) {
SelectButton::SelectButton(Rml::Element* parent, Props props)
: FluentComponent(createRoot(parent)) {
mKeyElem = append(mRoot, "key");
mIconElem = append(mRoot, "icon");
mValueElem = append(mRoot, "value");
update_props(std::move(props));
on_nav_command([this](Rml::Event&, NavCommand cmd) { return handle_nav_command(cmd); });
}
bool SelectButton::modified() const {
return mProps.modified;
}
void SelectButton::set_modified(bool value) {
if (mProps.modified != value) {
mValueElem->SetClass("modified", value);
if (value) {
mValueElem->SetInnerRML(fmt::format("•&nbsp;{}", escape(mProps.value)));
} else {
mValueElem->SetInnerRML(escape(mProps.value));
}
mProps.modified = value;
}
}
void SelectButton::set_value_label(const Rml::String& value) {
if (mProps.value != value) {
mValueElem->SetInnerRML(escape(value));
if (mProps.modified) {
mValueElem->SetInnerRML(fmt::format("•&nbsp;{}", escape(value)));
} else {
mValueElem->SetInnerRML(escape(value));
}
mProps.value = value;
}
}
SelectButton& SelectButton::on_pressed(SelectButtonCallback callback) {
if (!callback) {
return *this;
}
listen(Rml::EventId::Submit, [this, callback = std::move(callback)](Rml::Event& event) {
if (!disabled() && event.GetTargetElement() == mRoot) {
callback();
}
});
return *this;
}
void SelectButton::update_props(Props props) {
if (mProps.key != props.key) {
mKeyElem->SetInnerRML(escape(props.key));
}
if (mProps.icon != props.icon) {
Rml::StringList iconClasses;
Rml::StringUtilities::ExpandString(iconClasses, mIconElem->GetClassNames(), ' ', true);
for (const auto& className : iconClasses) {
mIconElem->SetClass(className, false);
}
if (!props.icon.empty()) {
mIconElem->SetClass(props.icon, true);
}
}
set_value_label(props.value);
set_modified(props.modified);
mProps = std::move(props);
}
bool SelectButton::handle_nav_command(NavCommand cmd) {
if (cmd == NavCommand::Confirm) {
if (cmd == NavCommand::Confirm && mProps.submit) {
mRoot->DispatchEvent(Rml::EventId::Submit, {});
return true;
}
@@ -49,9 +94,17 @@ bool SelectButton::handle_nav_command(NavCommand cmd) {
void BaseControlledSelectButton::update() {
set_disabled(disabled());
set_value_label(format_value());
set_modified(modified());
SelectButton::update();
}
bool ControlledSelectButton::modified() const {
if (mIsModified) {
return mIsModified();
}
return BaseControlledSelectButton::modified();
}
bool ControlledSelectButton::disabled() const {
if (mIsDisabled) {
return mIsDisabled();
+23 -3
View File
@@ -3,18 +3,29 @@
#include "component.hpp"
#include "ui.hpp"
#include <functional>
#include <utility>
namespace dusk::ui {
using SelectButtonCallback = std::function<void()>;
class SelectButton : public FluentComponent<SelectButton> {
public:
struct Props {
Rml::String key;
Rml::String value;
Rml::String icon;
bool modified = false;
bool submit = true;
};
SelectButton(Rml::Element* parent, Props props);
virtual bool modified() const;
void set_modified(bool value);
void set_value_label(const Rml::String& value);
SelectButton& on_pressed(SelectButtonCallback callback);
protected:
void update_props(Props props);
@@ -22,8 +33,8 @@ protected:
Props mProps;
Rml::Element* mKeyElem = nullptr;
Rml::Element* mIconElem = nullptr;
Rml::Element* mValueElem = nullptr;
std::function<void()> mOnHover;
};
class BaseControlledSelectButton : public SelectButton {
@@ -43,12 +54,20 @@ public:
Rml::String key;
std::function<Rml::String()> getValue;
std::function<bool()> isDisabled;
std::function<bool()> isModified;
bool submit = true;
};
ControlledSelectButton(Rml::Element* parent, Props props)
: BaseControlledSelectButton(parent, {std::move(props.key)}),
mGetValue(std::move(props.getValue)), mIsDisabled(std::move(props.isDisabled)) {}
: BaseControlledSelectButton(parent,
{
.key = std::move(props.key),
.submit = props.submit,
}),
mGetValue(std::move(props.getValue)), mIsDisabled(std::move(props.isDisabled)),
mIsModified(std::move(props.isModified)) {}
bool modified() const override;
bool disabled() const override;
protected:
@@ -56,6 +75,7 @@ protected:
std::function<Rml::String()> mGetValue;
std::function<bool()> mIsDisabled;
std::function<bool()> mIsModified;
};
} // namespace dusk::ui
+412 -434
View File
@@ -1,9 +1,8 @@
#include "settings.hpp"
#include <fmt/format.h>
#include "aurora/gfx.h"
#include "bool_button.hpp"
#include "controller_config.hpp"
#include "dusk/audio/DuskAudioSystem.h"
#include "dusk/audio/DuskDsp.hpp"
#include "dusk/config.hpp"
@@ -57,11 +56,9 @@ const Rml::String kBloomHelpText =
"a higher-quality bloom pass.";
const Rml::String kBloomBrightnessHelpText =
"Configure bloom intensity. Higher values make bright areas glow more strongly.";
int bloom_multiplier_percent() {
return std::clamp(
static_cast<int>(getSettings().game.bloomMultiplier.getValue() * 100.0f + 0.5f), 0, 100);
}
const Rml::String kUnlockFramerateHelpText =
"Uses inter-frame interpolation to enable higher frame rates.<br/><br/>May introduce minor "
"visual artifacts or animation glitches.";
int float_setting_percent(ConfigVar<float>& var) {
return static_cast<int>(var.getValue() * 100.0f + 0.5f);
@@ -73,6 +70,7 @@ bool gyro_enabled() {
struct ConfigBoolProps {
Rml::String key;
Rml::String icon;
Rml::String helpText;
std::function<void(bool)> onChange;
std::function<bool()> isDisabled;
@@ -80,278 +78,192 @@ struct ConfigBoolProps {
SelectButton& config_bool_select(
Pane& leftPane, Pane& rightPane, ConfigVar<bool>& var, ConfigBoolProps props) {
return leftPane
.add_child<BoolButton>(BoolButton::Props{
.key = std::move(props.key),
.getValue = [&var] { return var.getValue(); },
.setValue =
[&var, callback = std::move(props.onChange)](bool value) {
if (value == var.getValue()) {
return;
}
var.setValue(value);
config::Save();
if (callback) {
callback(value);
}
},
.isDisabled = std::move(props.isDisabled),
})
.on_focus([&rightPane, helpText = std::move(props.helpText)](Rml::Event&) {
rightPane.clear();
rightPane.add_rml(helpText);
auto& button = leftPane.add_child<BoolButton>(BoolButton::Props{
.key = std::move(props.key),
.icon = std::move(props.icon),
.getValue = [&var] { return var.getValue(); },
.setValue =
[&var, callback = std::move(props.onChange)](bool value) {
if (value == var.getValue()) {
return;
}
var.setValue(value);
config::Save();
if (callback) {
callback(value);
}
},
.isDisabled = std::move(props.isDisabled),
.isModified = [&var] { return var.getValue() != var.getDefaultValue(); },
});
leftPane.register_control(
button, rightPane, [helpText = std::move(props.helpText)](Pane& pane) {
pane.clear();
pane.add_rml(helpText);
});
return button;
}
SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar<float>& var,
Rml::String key, Rml::String helpText, int min, int max, int step = 5,
std::function<bool()> isDisabled = {}) {
return leftPane
.add_child<NumberButton>(NumberButton::Props{
.key = std::move(key),
.getValue = [&var] { return float_setting_percent(var); },
.setValue =
[&var, min, max](int value) {
var.setValue(std::clamp(value, min, max) / 100.0f);
config::Save();
},
.isDisabled = std::move(isDisabled),
.min = min,
.max = max,
.step = step,
.suffix = "%",
})
.on_focus([&rightPane, helpText = std::move(helpText)](Rml::Event&) {
rightPane.clear();
rightPane.add_text(helpText);
});
auto& button = leftPane.add_child<NumberButton>(NumberButton::Props{
.key = std::move(key),
.getValue = [&var] { return float_setting_percent(var); },
.setValue =
[&var, min, max](int value) {
var.setValue(std::clamp(value, min, max) / 100.0f);
config::Save();
},
.isDisabled = std::move(isDisabled),
.isModified = [&var] { return var.getValue() != var.getDefaultValue(); },
.min = min,
.max = max,
.step = step,
.suffix = "%",
});
leftPane.register_control(button, rightPane, [helpText = std::move(helpText)](Pane& pane) {
pane.clear();
pane.add_text(helpText);
});
return button;
}
class ControllerConfigWindow : public Window {
public:
ControllerConfigWindow() {
for (int i = 0; i < 4; ++i) {
add_tab(fmt::format("Port {}", i + 1), [this](Rml::Element* content) {
auto& pane = add_child<Pane>(content, Pane::Type::Controlled);
pane.add_section("Coming soon");
});
}
}
};
template <typename T>
void overlay_control(
Window& window, Pane& leftPane, Pane& rightPane, ConfigVar<T>& var, const OverlayProps& props) {
leftPane.register_control(
leftPane
.add_select_button({
.key = props.title,
.getValue =
[&var, option = props.option] {
if constexpr (std::is_same_v<T, float>) {
return format_graphics_setting_value(
option, float_setting_percent(var));
} else {
return format_graphics_setting_value(
option, static_cast<int>(var.getValue()));
}
},
.isModified = [&var] { return var.getValue() != var.getDefaultValue(); },
.submit = false,
})
.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));
return true;
}
return false;
}),
rightPane, [helpText = props.helpText](Pane& pane) {
pane.clear();
pane.add_text(helpText);
});
}
} // namespace
SettingsWindow::SettingsWindow() {
add_tab("Audio", [this](Rml::Element* content) {
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);
// TODO: Individual sliders for Main Music, Sub Music, Sound Effects, and Fanfare.
leftPane.add_section("Volume");
leftPane
.add_child<NumberButton>(NumberButton::Props{
.key = "Master Volume",
.getValue = [] { return getSettings().audio.masterVolume.getValue(); },
.setValue =
[](int value) {
getSettings().audio.masterVolume.setValue(value);
config::Save();
audio::SetMasterVolume(value / 100.f);
},
.max = 100,
.suffix = "%",
})
.on_focus([&rightPane](Rml::Event&) {
rightPane.clear();
rightPane.add_text("Adjusts the volume of all sounds in the game.");
});
leftPane.add_section("Display");
leftPane.add_section("Effects");
config_bool_select(leftPane, rightPane, getSettings().audio.enableReverb,
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(); }
);
config_bool_select(leftPane, rightPane, getSettings().video.enableVsync,
{
.key = "Enable Reverb",
.helpText = "Enables the reverb effect in game audio.",
.onChange = [](bool value) { audio::SetEnableReverb(value); },
.key = "Enable VSync",
.helpText = "Synchronizes the frame rate to your monitor's refresh rate.",
.onChange = [](bool value) { aurora_enable_vsync(value); },
});
config_bool_select(leftPane, rightPane, getSettings().audio.enableHrtf,
config_bool_select(leftPane, rightPane, getSettings().video.lockAspectRatio,
{
.key = "Enable Spatial Sound",
.helpText = "Emulate surround sound via HRTF. Recommended only for use with headphones!",
.onChange = [](bool value) { audio::EnableHrtf = value; },
});
leftPane.add_section("Tweaks");
config_bool_select(leftPane, rightPane, getSettings().game.noLowHpSound,
{
.key = "No Low HP Sound",
.helpText = "Disable the beeping sound when having low health.",
});
config_bool_select(leftPane, rightPane, getSettings().game.midnasLamentNonStop,
{
.key = "Non-Stop Midna's Lament",
.helpText = "Prevents enemy music while Midna's Lament is playing.",
});
});
add_tab("Cheats", [this](Rml::Element* content) {
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
auto addCheat = [&](const Rml::String& key, ConfigVar<bool>& value,
const Rml::String& helpText) {
config_bool_select(leftPane, rightPane, value,
{
.key = key,
.helpText = helpText,
.isDisabled = [] { return getSettings().game.speedrunMode; },
});
};
leftPane.add_section("Resources");
addCheat("Infinite Hearts", getSettings().game.infiniteHearts,
"Keeps your health full.");
addCheat("Infinite Arrows", getSettings().game.infiniteArrows,
"Keeps your arrow count full.");
addCheat("Infinite Bombs", getSettings().game.infiniteBombs,
"Keeps all bomb bags full.");
addCheat("Infinite Oil", getSettings().game.infiniteOil,
"Keeps your lantern oil full.");
addCheat("Infinite Oxygen", getSettings().game.infiniteOxygen,
"Keeps your underwater oxygen meter full.");
addCheat("Infinite Rupees", getSettings().game.infiniteRupees,
"Keeps your rupee count full.");
addCheat("No Item Timer", getSettings().game.enableIndefiniteItemDrops,
"Item drops such as rupees and hearts will never disappear after they drop.");
leftPane.add_section("Abilities");
addCheat("Moon Jump (R+A)", getSettings().game.moonJump,
"Hold R and A to rise into the air.");
addCheat("Super Clawshot", getSettings().game.superClawshot,
"Extends clawshot behavior beyond the normal game rules.");
addCheat("Always Greatspin", getSettings().game.alwaysGreatspin,
"Allows the Great Spin attack without requiring full health.");
addCheat("Fast Iron Boots", getSettings().game.enableFastIronBoots,
"Speeds up movement while wearing the Iron Boots.");
addCheat("Can Transform Anywhere", getSettings().game.canTransformAnywhere,
"Allows transforming even if NPCs are looking.");
addCheat("Fast Spinner", getSettings().game.fastSpinner,
"Speeds up Spinner movement while holding R.");
addCheat("Free Magic Armor", getSettings().game.freeMagicArmor,
"Lets the magic armor work without consuming rupees.");
});
add_tab("Gameplay", [this](Rml::Element* content) {
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
auto addOption = [&](const Rml::String& key, ConfigVar<bool>& value,
const Rml::String& helpText) {
config_bool_select(leftPane, rightPane, value,
{
.key = key,
.helpText = helpText,
});
};
auto addSpeedrunDisabledOption = [&](const Rml::String& key, ConfigVar<bool>& value,
const Rml::String& helpText) {
config_bool_select(leftPane, rightPane, value,
{
.key = key,
.helpText = helpText,
.isDisabled = [] { return getSettings().game.speedrunMode; },
});
};
leftPane.add_section("General");
addOption("Mirror Mode", getSettings().game.enableMirrorMode,
"Mirrors the world horizontally, matching the Wii version of the game.");
addOption("Disable Main HUD", getSettings().game.disableMainHUD,
"Disables the main HUD of the game.<br/>Useful for recording or a more immersive "
"experience.");
addOption("Restore Wii 1.0 Glitches", getSettings().game.restoreWiiGlitches,
"Restores patched glitches from Wii USA 1.0, the first released version.");
addOption("Enable Rotating Link Doll", getSettings().game.enableLinkDollRotation,
"Enables rotating Link in the collection menu with the C-Stick.");
leftPane.add_section("Difficulty");
leftPane
.add_child<NumberButton>(NumberButton::Props{
.key = "Damage Multiplier",
.getValue = [] { return getSettings().game.damageMultiplier.getValue(); },
.setValue =
[](int value) {
getSettings().game.damageMultiplier.setValue(value);
config::Save();
},
.isDisabled = [] { return getSettings().game.speedrunMode; },
.min = 1,
.max = 8,
.prefix = "x",
})
.on_focus([&rightPane](Rml::Event&) {
rightPane.clear();
rightPane.add_text("Multiplies incoming damage.");
});
addSpeedrunDisabledOption("Instant Death", getSettings().game.instantDeath,
"Any hit will instantly kill you.");
addSpeedrunDisabledOption("No Heart Drops", getSettings().game.noHeartDrops,
"Hearts will never drop from enemies, pots, and various other places.");
leftPane.add_section("Quality of Life");
addOption("Bigger Wallets", getSettings().game.biggerWallets,
"Wallet sizes are like in the HD version. (500, 1000, 2000)");
addOption("Disable Rupee Cutscenes", getSettings().game.disableRupeeCutscenes,
"Rupees will not play cutscenes after you have collected them the first time.");
addOption("Faster Climbing", getSettings().game.fastClimbing,
"Quicker climbing on ladders and vines like the HD version.");
addOption("Faster Tears of Light", getSettings().game.fastTears,
"Tears of Light dropped by Shadow Insects pop out faster like the HD version.");
addOption("Autosave", getSettings().game.autoSave,
"Autosaves the game when going to a new area, opening a dungeon door, or getting "
"a new item.<br/><br/>This feature is currently experimental, use at your own risk.");
addOption("Instant Saves", getSettings().game.instantSaves,
"Skips the delay when writing to the Memory Card.");
addOption("Hold B for Instant Text", getSettings().game.instantText,
"Makes text scroll immediately by holding B.");
addOption("No Climbing Miss Animation", getSettings().game.noMissClimbing,
"Prevents Link from playing a struggle animation when grabbing ledges or "
"climbing on vines.");
addOption("No Rupee Returns", getSettings().game.noReturnRupees,
"Always collect Rupees even if your Wallet is too full.");
addOption("No Sword Recoil", getSettings().game.noSwordRecoil,
"Link will not recoil when his sword hits walls.");
addOption("No 2nd Fish for Cat", getSettings().game.no2ndFishForCat,
"Skip needing to catch a second fish for Sera's cat.");
addOption("Skip TV Settings Screen", getSettings().game.hideTvSettingsScreen,
"Skips the TV calibration screen shown when loading a save.");
addOption("Skip Warning Screen", getSettings().game.skipWarningScreen,
"Skips the warning screen shown when starting the game.");
addOption("Sun's Song (R+X)", getSettings().game.sunsSong,
"Allows Wolf Link to howl and change the time of day.");
addOption("Quick Transform (R+Y)", getSettings().game.enableQuickTransform,
"Transform instantly by pressing R and Y simultaneously.");
leftPane.add_section("Speedrunning");
config_bool_select(leftPane, rightPane, getSettings().game.speedrunMode,
{
.key = "Speedrun Mode",
.helpText =
"Enables speedrunning options while restricting certain gameplay modifiers.",
.onChange = [](bool) { reset_for_speedrun_mode(); },
});
config_bool_select(leftPane, rightPane, getSettings().game.liveSplitEnabled,
{
.key = "LiveSplit Connection",
.helpText = "Connect to LiveSplit server on localhost:16834.",
.key = "Lock 4:3 Aspect Ratio",
.helpText = "Lock the game's aspect ratio to the original.",
.onChange =
[](bool enabled) {
if (enabled) {
speedrun::connectLiveSplit();
} else {
speedrun::disconnectLiveSplit();
}
[](bool value) {
AuroraSetViewportPolicy(
value ? AURORA_VIEWPORT_FIT : AURORA_VIEWPORT_STRETCH);
},
.isDisabled = [] { return !getSettings().game.speedrunMode; },
});
config_bool_select(leftPane, rightPane, getSettings().game.pauseOnFocusLost,
{
.key = "Pause on Focus Lost",
.isDisabled = [] { return IsMobile; },
});
leftPane.add_section("Resolution");
overlay_control(*this, leftPane, rightPane, getSettings().game.internalResolutionScale,
OverlayProps{
.option = GraphicsOption::InternalResolution,
.title = "Internal Resolution",
.helpText = kInternalResolutionHelpText,
.valueMin = 0,
.valueMax = 12,
.defaultValue = 0,
});
overlay_control(*this, leftPane, rightPane, getSettings().game.shadowResolutionMultiplier,
OverlayProps{
.option = GraphicsOption::ShadowResolution,
.title = "Shadow Resolution",
.helpText = kShadowResolutionHelpText,
.valueMin = 1,
.valueMax = 8,
.defaultValue = 1,
});
leftPane.add_section("Post-Processing");
overlay_control(*this, leftPane, rightPane, getSettings().game.bloomMode,
OverlayProps{
.option = GraphicsOption::BloomMode,
.title = "Bloom",
.helpText = kBloomHelpText,
.valueMin = static_cast<int>(BloomMode::Off),
.valueMax = static_cast<int>(BloomMode::Dusk),
.defaultValue = static_cast<int>(BloomMode::Classic),
});
overlay_control(*this, leftPane, rightPane, getSettings().game.bloomMultiplier,
OverlayProps{
.option = GraphicsOption::BloomMultiplier,
.title = "Bloom Brightness",
.helpText = kBloomBrightnessHelpText,
.valueMin = 0,
.valueMax = 100,
.defaultValue = 100,
});
leftPane.add_section("Rendering");
config_bool_select(leftPane, rightPane, getSettings().game.enableFrameInterpolation,
{
.key = "Unlock Framerate",
.helpText = kUnlockFramerateHelpText,
});
config_bool_select(leftPane, rightPane, getSettings().game.enableDepthOfField,
{
.key = "Enable Depth of Field",
});
config_bool_select(leftPane, rightPane, getSettings().game.enableMapBackground,
{
.key = "Enable Mini-Map Shadows",
});
});
@@ -370,11 +282,12 @@ SettingsWindow::SettingsWindow() {
};
leftPane.add_section("Controller");
leftPane.add_button("Configure Controller")
.on_pressed([] { push_document(std::make_unique<ControllerConfigWindow>()); })
.on_focus([&rightPane](Rml::Event&) {
rightPane.clear();
rightPane.add_text("Open controller binding configuration.");
leftPane.register_control(leftPane.add_button("Configure Controller").on_pressed([this] {
push(std::make_unique<ControllerConfigWindow>());
}),
rightPane, [](Pane& pane) {
pane.clear();
pane.add_text("Open controller binding configuration.");
});
leftPane.add_section("Camera");
@@ -423,179 +336,232 @@ SettingsWindow::SettingsWindow() {
[] { return getSettings().game.speedrunMode; });
});
add_tab("Graphics", [this](Rml::Element* content) {
add_tab("Audio", [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.add_button("Toggle Fullscreen").on_pressed([] {
getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
VISetWindowFullscreen(getSettings().video.enableFullscreen);
config::Save();
});
leftPane.add_button("Restore Default Window Size").on_pressed([] {
getSettings().video.enableFullscreen.setValue(false);
VISetWindowFullscreen(false);
VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2);
VICenterWindow();
});
config_bool_select(leftPane, rightPane, getSettings().video.enableVsync,
{
.key = "Enable VSync",
.helpText = "Synchronizes the frame rate to your monitor's refresh rate.",
.onChange = [](bool value) { aurora_enable_vsync(value); },
});
config_bool_select(leftPane, rightPane, getSettings().video.lockAspectRatio,
{
.key = "Lock 4:3 Aspect Ratio",
.helpText = "Lock the game's aspect ratio to the original.",
.onChange =
[](bool value) {
AuroraSetViewportPolicy(
value ? AURORA_VIEWPORT_FIT : AURORA_VIEWPORT_STRETCH);
// TODO: Individual sliders for Main Music, Sub Music, Sound Effects, and Fanfare.
leftPane.add_section("Volume");
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Master Volume",
.getValue = [] { return getSettings().audio.masterVolume.getValue(); },
.setValue =
[](int value) {
getSettings().audio.masterVolume.setValue(value);
config::Save();
audio::SetMasterVolume(value / 100.f);
},
});
config_bool_select(leftPane, rightPane, getSettings().game.pauseOnFocusLost,
{
.key = "Pause on Focus Lost",
.isDisabled = [] { return IsMobile; },
.isModified =
[] {
return getSettings().audio.masterVolume.getValue() !=
getSettings().audio.masterVolume.getDefaultValue();
},
.max = 100,
.suffix = "%",
}),
rightPane, [](Pane& pane) {
pane.clear();
pane.add_text("Adjusts the volume of all sounds in the game.");
});
leftPane.add_section("Resolution");
leftPane
.add_select_button({
.key = "Internal Resolution",
.getValue =
[] {
return format_graphics_setting_value(GraphicsOption::InternalResolution,
getSettings().game.internalResolutionScale.getValue());
},
})
.on_nav_command([](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left ||
cmd == NavCommand::Right) {
push_document(std::make_unique<Overlay>(OverlayProps{
.option = GraphicsOption::InternalResolution,
.title = "Internal Resolution",
.helpText = kInternalResolutionHelpText,
.valueMin = 0,
.valueMax = 12,
.defaultValue = 0,
}));
return true;
}
return false;
})
.on_focus([&rightPane](Rml::Event&) {
rightPane.clear();
rightPane.add_text(kInternalResolutionHelpText);
});
leftPane
.add_select_button({
.key = "Shadow Resolution",
.getValue =
[] {
return format_graphics_setting_value(GraphicsOption::ShadowResolution,
getSettings().game.shadowResolutionMultiplier.getValue());
},
})
.on_nav_command([](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left ||
cmd == NavCommand::Right) {
push_document(std::make_unique<Overlay>(OverlayProps{
.option = GraphicsOption::ShadowResolution,
.title = "Shadow Resolution",
.helpText = kShadowResolutionHelpText,
.valueMin = 1,
.valueMax = 8,
.defaultValue = 1,
}));
return true;
}
return false;
})
.on_focus([&rightPane](Rml::Event&) {
rightPane.clear();
rightPane.add_text(kShadowResolutionHelpText);
});
leftPane.add_section("Post-Processing");
leftPane
.add_select_button({
.key = "Bloom",
.getValue =
[] {
return format_graphics_setting_value(GraphicsOption::BloomMode,
static_cast<int>(getSettings().game.bloomMode.getValue()));
},
})
.on_nav_command([](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left ||
cmd == NavCommand::Right) {
push_document(std::make_unique<Overlay>(OverlayProps{
.option = GraphicsOption::BloomMode,
.title = "Bloom",
.helpText = kBloomHelpText,
.valueMin = static_cast<int>(BloomMode::Off),
.valueMax = static_cast<int>(BloomMode::Dusk),
.defaultValue = static_cast<int>(BloomMode::Classic),
}));
return true;
}
return false;
})
.on_focus([&rightPane](Rml::Event&) {
rightPane.clear();
rightPane.add_text(kBloomHelpText);
});
leftPane
.add_select_button({
.key = "Bloom Brightness",
.getValue =
[] {
return format_graphics_setting_value(
GraphicsOption::BloomMultiplier, bloom_multiplier_percent());
},
.isDisabled =
[] { return getSettings().game.bloomMode.getValue() == BloomMode::Off; },
})
.on_nav_command([](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left ||
cmd == NavCommand::Right) {
push_document(std::make_unique<Overlay>(OverlayProps{
.option = GraphicsOption::BloomMultiplier,
.title = "Bloom Brightness",
.helpText = kBloomBrightnessHelpText,
.valueMin = 0,
.valueMax = 100,
.defaultValue = 100,
}));
return true;
}
return false;
})
.on_focus([&rightPane](Rml::Event&) {
rightPane.clear();
rightPane.add_text(kBloomBrightnessHelpText);
});
leftPane.add_section("Rendering");
config_bool_select(leftPane, rightPane, getSettings().game.enableFrameInterpolation,
leftPane.add_section("Effects");
config_bool_select(leftPane, rightPane, getSettings().audio.enableReverb,
{
.key = "Unlock Framerate",
.key = "Enable Reverb",
.helpText = "Enables the reverb effect in game audio.",
.onChange = [](bool value) { audio::SetEnableReverb(value); },
});
config_bool_select(leftPane, rightPane, getSettings().audio.enableHrtf,
{
.key = "Enable Spatial Sound",
.helpText =
"Uses inter-frame interpolation to enable higher frame rates.<br/><br/>Visual "
"artifacts, animation glitches, or instability may occur.",
"Emulate surround sound via HRTF. Recommended only for use with headphones!",
.onChange = [](bool value) { audio::EnableHrtf = value; },
});
config_bool_select(leftPane, rightPane, getSettings().game.enableDepthOfField,
config_bool_select(leftPane, rightPane, getSettings().audio.menuSounds,
{
.key = "Enable Depth of Field",
.key = "Dusk Menu Sounds",
.helpText = "Play sound effects when navigating the Dusk menu.",
});
config_bool_select(leftPane, rightPane, getSettings().game.enableMapBackground,
leftPane.add_section("Tweaks");
config_bool_select(leftPane, rightPane, getSettings().game.noLowHpSound,
{
.key = "Enable Mini-Map Shadows",
.key = "No Low HP Sound",
.helpText = "Disable the beeping sound when having low health.",
});
config_bool_select(leftPane, rightPane, getSettings().game.midnasLamentNonStop,
{
.key = "Non-Stop Midna's Lament",
.helpText = "Prevents enemy music while Midna's Lament is playing.",
});
});
add_tab("Gameplay", [this](Rml::Element* content) {
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
auto addOption = [&](const Rml::String& key, ConfigVar<bool>& value,
const Rml::String& helpText) {
config_bool_select(leftPane, rightPane, value,
{
.key = key,
.helpText = helpText,
});
};
auto addSpeedrunDisabledOption = [&](const Rml::String& key, ConfigVar<bool>& value,
const Rml::String& helpText) {
config_bool_select(leftPane, rightPane, value,
{
.key = key,
.helpText = helpText,
.isDisabled = [] { return getSettings().game.speedrunMode; },
});
};
leftPane.add_section("General");
addOption("Mirror Mode", getSettings().game.enableMirrorMode,
"Mirrors the world horizontally, matching the Wii version of the game.");
addOption("Disable Main HUD", getSettings().game.disableMainHUD,
"Disables the main HUD of the game.<br/>Useful for recording or a more immersive "
"experience.");
addOption("Restore Wii 1.0 Glitches", getSettings().game.restoreWiiGlitches,
"Restores patched glitches from Wii USA 1.0, the first released version.");
addOption("Enable Rotating Link Doll", getSettings().game.enableLinkDollRotation,
"Enables rotating Link in the collection menu with the C-Stick.");
leftPane.add_section("Difficulty");
leftPane.register_control(
leftPane.add_child<NumberButton>(NumberButton::Props{
.key = "Damage Multiplier",
.getValue = [] { return getSettings().game.damageMultiplier.getValue(); },
.setValue =
[](int value) {
getSettings().game.damageMultiplier.setValue(value);
config::Save();
},
.isDisabled = [] { return getSettings().game.speedrunMode; },
.isModified =
[] {
return getSettings().game.damageMultiplier.getValue() !=
getSettings().game.damageMultiplier.getDefaultValue();
},
.min = 1,
.max = 8,
.suffix = "×",
}),
rightPane, [](Pane& pane) {
pane.clear();
pane.add_text("Multiplies incoming damage.");
});
addSpeedrunDisabledOption(
"Instant Death", getSettings().game.instantDeath, "Any hit will instantly kill you.");
addSpeedrunDisabledOption("No Heart Drops", getSettings().game.noHeartDrops,
"Hearts will never drop from enemies, pots, and various other places.");
leftPane.add_section("Quality of Life");
addOption("Bigger Wallets", getSettings().game.biggerWallets,
"Wallet sizes are like in the HD version. (500, 1000, 2000)");
addOption("Disable Rupee Cutscenes", getSettings().game.disableRupeeCutscenes,
"Rupees will not play cutscenes after you have collected them the first time.");
addOption("Faster Climbing", getSettings().game.fastClimbing,
"Quicker climbing on ladders and vines like the HD version.");
addOption("Faster Tears of Light", getSettings().game.fastTears,
"Tears of Light dropped by Shadow Insects pop out faster like the HD version.");
config_bool_select(leftPane, rightPane, getSettings().game.autoSave,
{
.key = "Autosave",
.icon = "warning",
.helpText =
"Autosaves the game when going to a new area, opening a dungeon door, "
"or getting a new item.<br/><br/><icon class=\"warning\"/> Experimental "
"feature: Use at your own risk.",
});
addOption("Instant Saves", getSettings().game.instantSaves,
"Skips the delay when writing to the Memory Card.");
addOption("Hold B for Instant Text", getSettings().game.instantText,
"Makes text scroll immediately by holding B.");
addOption("No Climbing Miss Animation", getSettings().game.noMissClimbing,
"Prevents Link from playing a struggle animation when grabbing ledges or "
"climbing on vines.");
addOption("No Rupee Returns", getSettings().game.noReturnRupees,
"Always collect Rupees even if your Wallet is too full.");
addOption("No Sword Recoil", getSettings().game.noSwordRecoil,
"Link will not recoil when his sword hits walls.");
addOption("No 2nd Fish for Cat", getSettings().game.no2ndFishForCat,
"Skip needing to catch a second fish for Sera's cat.");
addOption("Sun's Song (R+X)", getSettings().game.sunsSong,
"Allows Wolf Link to howl and change the time of day.");
addOption("Quick Transform (R+Y)", getSettings().game.enableQuickTransform,
"Transform instantly by pressing R and Y simultaneously.");
leftPane.add_section("Speedrunning");
config_bool_select(leftPane, rightPane, getSettings().game.speedrunMode,
{
.key = "Speedrun Mode",
.helpText =
"Enables speedrunning options while restricting certain gameplay modifiers.",
.onChange = [](bool) { reset_for_speedrun_mode(); },
});
config_bool_select(leftPane, rightPane, getSettings().game.liveSplitEnabled,
{
.key = "LiveSplit Connection",
.helpText = "Connect to LiveSplit server on localhost:16834.",
.onChange =
[](bool enabled) {
if (enabled) {
speedrun::connectLiveSplit();
} else {
speedrun::disconnectLiveSplit();
}
},
.isDisabled = [] { return !getSettings().game.speedrunMode; },
});
});
add_tab("Cheats", [this](Rml::Element* content) {
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
auto addCheat = [&](const Rml::String& key, ConfigVar<bool>& value,
const Rml::String& helpText) {
config_bool_select(leftPane, rightPane, value,
{
.key = key,
.helpText = helpText,
.isDisabled = [] { return getSettings().game.speedrunMode; },
});
};
leftPane.add_section("Resources");
addCheat("Infinite Hearts", getSettings().game.infiniteHearts, "Keeps your health full.");
addCheat(
"Infinite Arrows", getSettings().game.infiniteArrows, "Keeps your arrow count full.");
addCheat("Infinite Bombs", getSettings().game.infiniteBombs, "Keeps all bomb bags full.");
addCheat("Infinite Oil", getSettings().game.infiniteOil, "Keeps your lantern oil full.");
addCheat("Infinite Oxygen", getSettings().game.infiniteOxygen,
"Keeps your underwater oxygen meter full.");
addCheat(
"Infinite Rupees", getSettings().game.infiniteRupees, "Keeps your rupee count full.");
addCheat("No Item Timer", getSettings().game.enableIndefiniteItemDrops,
"Item drops such as rupees and hearts will never disappear after they drop.");
leftPane.add_section("Abilities");
addCheat(
"Moon Jump (R+A)", getSettings().game.moonJump, "Hold R and A to rise into the air.");
addCheat("Super Clawshot", getSettings().game.superClawshot,
"Extends clawshot behavior beyond the normal game rules.");
addCheat("Always Greatspin", getSettings().game.alwaysGreatspin,
"Allows the Great Spin attack without requiring full health.");
addCheat("Fast Iron Boots", getSettings().game.enableFastIronBoots,
"Speeds up movement while wearing the Iron Boots.");
addCheat("Can Transform Anywhere", getSettings().game.canTransformAnywhere,
"Allows transforming even if NPCs are looking.");
addCheat("Fast Spinner", getSettings().game.fastSpinner,
"Speeds up Spinner movement while holding R.");
addCheat("Free Magic Armor", getSettings().game.freeMagicArmor,
"Lets the magic armor work without consuming rupees.");
});
// TODO: Reorganize all of this?
@@ -605,29 +571,41 @@ SettingsWindow::SettingsWindow() {
config_bool_select(leftPane, rightPane, getSettings().game.enableAchievementNotifications,
{
.key = "Enable Achievement Notifications",
.key = "Achievement Notifications",
.helpText = "Display a toast when an achievement is unlocked.",
});
#if DUSK_ENABLE_SENTRY_NATIVE
config_bool_select(leftPane, rightPane, getSettings().backend.enableCrashReporting,
{
.key = "Enable Crash Reporting",
{.key = "Crash Reporting",
.helpText = "Enable automatic reporting of crashes to the developers.<br/><br/>"
"Submissions include logs which may contain sensitive information. Refrain from "
"enabling reporting if you do not agree with the following inclusions:<br/><br/> "
"- Operating System<br/>- CPU Architecture<br/>- GPU Model & Driver Version<br/>"
"- Account Username"
});
"Submissions include logs which may contain sensitive information. "
"Refrain from "
"enabling reporting if you do not agree with the following "
"inclusions:<br/><br/> "
"- Operating System<br/>- CPU Architecture<br/>- GPU Model & Driver "
"Version<br/>"
"- Account Username"});
#endif
leftPane.add_section("Advanced");
config_bool_select(leftPane, rightPane, getSettings().backend.skipPreLaunchUI,
{
.key = "Skip Pre-Launch UI",
.key = "Skip Dusk Main Menu",
.helpText = "When starting Dusk, skips the main menu and boots straight into the "
"game if a disc image is available.",
});
config_bool_select(leftPane, rightPane, getSettings().game.hideTvSettingsScreen,
{
.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",
.helpText = "Show an overlay when shaders are being compiled for your hardware."
.helpText = "Show an overlay when shaders are being compiled for your hardware.",
});
});
}
+126 -13
View File
@@ -1,5 +1,8 @@
#include "tab_bar.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
namespace dusk::ui {
namespace {
@@ -9,28 +12,98 @@ Rml::Element* createRoot(Rml::Element* parent) {
return parent->AppendChild(std::move(elem));
}
int key_modifiers_from_event(const Rml::Event& event) {
int modifiers = 0;
if (event.GetParameter("ctrl_key", 0)) {
modifiers |= Rml::Input::KM_CTRL;
}
if (event.GetParameter("shift_key", 0)) {
modifiers |= Rml::Input::KM_SHIFT;
}
if (event.GetParameter("alt_key", 0)) {
modifiers |= Rml::Input::KM_ALT;
}
if (event.GetParameter("meta_key", 0)) {
modifiers |= Rml::Input::KM_META;
}
if (event.GetParameter("caps_lock_key", 0)) {
modifiers |= Rml::Input::KM_CAPSLOCK;
}
if (event.GetParameter("num_lock_key", 0)) {
modifiers |= Rml::Input::KM_NUMLOCK;
}
if (event.GetParameter("scroll_lock_key", 0)) {
modifiers |= Rml::Input::KM_SCROLLLOCK;
}
return modifiers;
}
} // namespace
TabBar::TabBar(Rml::Element* parent, Props props)
: FluentComponent(createRoot(parent)), mProps(std::move(props)) {
if (mProps.onClose) {
mRoot->SetAttribute("closable", "");
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;
}
return false;
});
mEndSpacer = append(mRoot, "tab-end-spacer");
}
listen(Rml::EventId::Keydown, [this](Rml::Event& event) {
const auto cmd = map_nav_event(event);
if (cmd != NavCommand::None && handle_nav_command(event, cmd)) {
event.StopPropagation();
}
});
// Remap vertical scroll events into horizontal scroll events
listen(Rml::EventId::Mousescroll, [this](Rml::Event& event) {
if (mRedirectingScroll) {
return;
}
if (mRoot->GetScrollWidth() <= mRoot->GetClientWidth() + 0.5f) {
return;
}
const float wheelDeltaX = event.GetParameter("wheel_delta_x", 0.0f);
const float wheelDeltaY = event.GetParameter("wheel_delta_y", 0.0f);
const float absWheelDeltaX = wheelDeltaX < 0.0f ? -wheelDeltaX : wheelDeltaX;
const float absWheelDeltaY = wheelDeltaY < 0.0f ? -wheelDeltaY : wheelDeltaY;
if (absWheelDeltaY == 0.0f || absWheelDeltaX >= absWheelDeltaY) {
return;
}
auto* context = mRoot->GetContext();
if (context == nullptr) {
return;
}
mRedirectingScroll = true;
context->ProcessMouseWheel({wheelDeltaY, 0.0f}, key_modifiers_from_event(event));
mRedirectingScroll = false;
event.StopPropagation();
});
}
bool TabBar::focus() {
if (mProps.selectedTabIndex >= 0 && mProps.selectedTabIndex < mTabs.size()) {
// Try to focus the currently selected tab
if (mTabs[mProps.selectedTabIndex].button.focus()) {
mLastFocusedTabIndex = mProps.selectedTabIndex;
return true;
}
}
// Otherwise, focus the first enabled tab
for (const auto& tab : mTabs) {
if (tab.button.focus()) {
for (int i = 0; i < static_cast<int>(mTabs.size()); ++i) {
if (mTabs[i].button.focus()) {
mLastFocusedTabIndex = i;
return true;
}
}
@@ -44,10 +117,21 @@ void TabBar::add_tab(const Rml::String& title, TabCallback callback) {
callback();
}
auto& button = add_child<Button>(Button::Props{title}, "tab");
button.on_pressed([this, index] { set_active_tab(index); });
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);
set_active_tab(index);
return true;
}
return false;
});
if (selected) {
button.set_selected(true);
}
if (mEndSpacer != nullptr) {
auto spacer = mRoot->RemoveChild(mEndSpacer);
mEndSpacer = mRoot->AppendChild(std::move(spacer));
}
mTabs.emplace_back(Tab{
.title = title,
.button = button,
@@ -58,8 +142,8 @@ void TabBar::add_tab(const Rml::String& title, TabCallback callback) {
bool TabBar::set_active_tab(int index) {
if (index == -1) {
// Clear currently selected tab
for (int i = 0; i < static_cast<int>(mTabs.size()); ++i) {
mTabs[i].button.set_selected(false);
for (auto& tab : mTabs) {
tab.button.set_selected(false);
}
mProps.selectedTabIndex = -1;
return true;
@@ -70,6 +154,7 @@ bool TabBar::set_active_tab(int index) {
}
const auto& tab = mTabs[index];
if (tab.button.focus()) {
mLastFocusedTabIndex = index;
for (int i = 0; i < static_cast<int>(mTabs.size()); ++i) {
mTabs[i].button.set_selected(i == index);
}
@@ -82,11 +167,28 @@ bool TabBar::set_active_tab(int index) {
return false;
}
void TabBar::refresh_active_tab() {
if (mProps.selectedTabIndex >= 0 && mProps.selectedTabIndex < static_cast<int>(mTabs.size())) {
const auto& tab = mTabs[mProps.selectedTabIndex];
if (tab.callback) {
tab.callback();
}
}
}
int TabBar::focused_tab_index() const {
return mLastFocusedTabIndex;
}
bool TabBar::focus_tab(int index) {
if (index < 0 || index >= mTabs.size() || index == mProps.selectedTabIndex) {
return false;
}
return mTabs[index].button.focus();
if (mTabs[index].button.focus()) {
mLastFocusedTabIndex = index;
return true;
}
return false;
}
int TabBar::tab_containing(Rml::Element* element) const {
@@ -103,20 +205,31 @@ bool TabBar::handle_nav_command(Rml::Event& event, NavCommand cmd) {
cmd == NavCommand::Previous)
{
bool isNext = cmd == NavCommand::Right || cmd == NavCommand::Next;
int currentComponent = tab_containing(event.GetTargetElement());
int currentComponent = mProps.selectedTabIndex;
if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
int activeTab = tab_containing(event.GetTargetElement());
if (activeTab != -1) {
currentComponent = activeTab;
}
}
int direction = isNext ? 1 : -1;
int i = currentComponent + direction;
if (currentComponent == -1) {
// If the container itself is focused and right is pressed, focus the first element
if (isNext) {
i = 0;
if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
// If the container itself is focused and right is pressed, focus the first element
if (!isNext) {
return false;
}
currentComponent = -1;
} else {
// Otherwise, allow event to bubble
// Next/Previous require a currently selected tab to navigate from
return false;
}
}
int i = currentComponent + direction;
while (i >= 0 && i < mTabs.size()) {
if (mProps.autoSelect ? set_active_tab(i) : focus_tab(i)) {
const bool changed = mProps.autoSelect ? set_active_tab(i) : focus_tab(i);
if (changed) {
mDoAud_seStartMenu(Z2SE_SY_NAME_CURSOR);
return true;
}
i += direction;
+5
View File
@@ -28,7 +28,9 @@ public:
void add_tab(const Rml::String& title, TabCallback callback);
bool set_active_tab(int index);
void refresh_active_tab();
bool focus_tab(int index);
int focused_tab_index() const;
bool handle_nav_command(Rml::Event& event, NavCommand cmd);
private:
@@ -36,6 +38,9 @@ private:
Props mProps;
std::vector<Tab> mTabs;
Rml::Element* mEndSpacer = nullptr;
bool mRedirectingScroll = false;
int mLastFocusedTabIndex = -1;
};
} // namespace dusk::ui
+16 -3
View File
@@ -33,10 +33,12 @@ bool initialize() noexcept {
}
load_font("FiraSans-Regular.ttf", true);
load_font("FiraSans-Bold.ttf");
load_font("FiraSansCondensed-Regular.ttf");
load_font("FiraSansCondensed-Bold.ttf");
load_font("AlegreyaSC-Regular.ttf");
load_font("AlegreyaSC-Bold.ttf");
load_font("MaterialSymbolsRounded-Regular.ttf");
sInitialized = true;
return true;
@@ -50,9 +52,6 @@ void shutdown() noexcept {
}
Document& push_document(std::unique_ptr<Document> doc, bool show) noexcept {
if (auto* top = top_document()) {
top->hide(false);
}
Document& ret = *doc;
sDocuments.push_back({std::move(doc)});
if (show) {
@@ -88,9 +87,23 @@ void update() noexcept {
for (const auto& doc : sDocuments) {
doc->update();
}
// Remove closed documents
const auto [first, last] =
std::ranges::remove_if(sDocuments, [](const auto& doc) { return doc->closed(); });
sDocuments.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)
{
for (auto& doc : std::views::reverse(sDocuments)) {
if (!doc->closed() && !doc->pending_close() && doc->focus()) {
break;
}
}
}
sync_input_block();
}
+20 -3
View File
@@ -6,6 +6,9 @@
#include "pane.hpp"
#include "ui.hpp"
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
#include <algorithm>
#include <cmath>
@@ -40,6 +43,7 @@ const Rml::String kDocumentSource = R"RML(
Window::Window() : Document(kDocumentSource), mRoot(mDocument->GetElementById("window")) {
mTabBar = std::make_unique<TabBar>(mRoot, TabBar::Props{
.onClose = [this] { pop(); },
.selectedTabIndex = 0,
.autoSelect = true,
});
@@ -135,6 +139,10 @@ bool Window::set_active_tab(int index) {
return mTabBar->set_active_tab(index);
}
void Window::refresh_active_tab() {
mTabBar->refresh_active_tab();
}
void Window::add_tab(const Rml::String& title, TabBuilder builder) {
mTabBar->add_tab(title, [this, builder = std::move(builder)] {
clear_content();
@@ -167,11 +175,13 @@ bool Window::handle_nav_command(Rml::Event& event, NavCommand cmd) {
}
}
if (cmd == NavCommand::Confirm || cmd == NavCommand::Down) {
if (!mContentComponents.empty()) {
return mContentComponents.front()->focus();
if (!mContentComponents.empty() && mContentComponents.front()->focus()) {
mDoAud_seStartMenu(Z2SE_SY_NAME_CURSOR);
return true;
}
}
if (cmd == NavCommand::Cancel) {
mDoAud_seStartMenu(Z2SE_SY_CURSOR_CANCEL);
pop();
return true;
}
@@ -183,7 +193,14 @@ bool Window::handle_nav_command(Rml::Event& event, NavCommand cmd) {
bool Window::handle_content_nav(Rml::Event& event, NavCommand cmd) noexcept {
if (cmd == NavCommand::Up) {
return focus();
if (focus()) {
mDoAud_seStartMenu(Z2SE_SY_NAME_CURSOR);
return true;
}
return false;
} else if (cmd == NavCommand::Down) {
// End of content, avoid looping
return true;
} else if (cmd == NavCommand::Cancel) {
int currentComponent = -1;
for (int i = 0; i < mContentComponents.size(); ++i) {
+1
View File
@@ -32,6 +32,7 @@ public:
protected:
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;
+6 -4
View File
@@ -52,15 +52,14 @@
#include "dusk/frame_interpolation.h"
#include "dusk/game_clock.h"
#include "dusk/gyro.h"
#include "dusk/imgui/ImGuiConsole.hpp"
#include "dusk/imgui/ImGuiEngine.hpp"
#include "dusk/logging.h"
#include "dusk/main.h"
#include "dusk/imgui/ImGuiConsole.hpp"
#include "dusk/ui/ui.hpp"
#include "dusk/ui/editor.hpp"
#include "dusk/ui/popup.hpp"
#include "dusk/ui/prelaunch.hpp"
#include "dusk/ui/settings.hpp"
#include "dusk/ui/preset.hpp"
#include "dusk/ui/ui.hpp"
#include "version.h"
#include <aurora/aurora.h>
@@ -634,6 +633,9 @@ int game_main(int argc, char* argv[]) {
}
dusk::ui::push_document(std::make_unique<dusk::ui::Popup>(), false);
if (!dusk::getSettings().backend.wasPresetChosen) {
dusk::ui::push_document(std::make_unique<dusk::ui::PresetWindow>());
}
dusk::version::init();
LanguageInit();