Merge branch 'main' of https://github.com/TwilitRealm/dusk into randomizer

This commit is contained in:
gymnast86
2026-05-04 22:26:07 -07:00
53 changed files with 2115 additions and 1510 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
+4 -4
View File
@@ -1451,8 +1451,6 @@ set(DUSK_FILES
src/dusk/imgui/ImGuiMenuRandomizer.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
@@ -1463,8 +1461,6 @@ 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
@@ -1475,6 +1471,10 @@ set(DUSK_FILES
src/dusk/ui/controller_config.hpp
src/dusk/ui/document.cpp
src/dusk/ui/document.hpp
src/dusk/ui/achievements.cpp
src/dusk/ui/achievements.hpp
src/dusk/ui/preset.cpp
src/dusk/ui/preset.hpp
src/dusk/ui/editor.cpp
src/dusk/ui/editor.hpp
src/dusk/ui/event.cpp
+18
View File
@@ -118,6 +118,18 @@ class camera_class;
class dCamera_c;
typedef bool (dCamera_c::*engine_fn)(s32);
#if TARGET_PC
struct DebugFlyCam {
bool initialized;
f32 pitch;
f32 yaw;
cXyz savedCenter;
cXyz savedEye;
f32 savedFovy;
cSAngle savedBank;
};
#endif
class dCamera_c {
public:
class dCamInfo_c {
@@ -1028,6 +1040,8 @@ public:
bool test2Camera(s32);
#if TARGET_PC
bool freeCamera();
bool executeDebugFlyCam();
void deactivateDebugFlyCam();
#endif
bool towerCamera(s32);
bool hookshotCamera(s32);
@@ -1376,6 +1390,10 @@ public:
/* 0x970 */ dCamSetup_c mCamSetup;
/* 0xAEC */ dCamParam_c mCamParam;
/* 0xB0C */ u8 field_0xb0c;
#if TARGET_PC
DebugFlyCam mDebugFlyCam;
#endif
}; // Size: 0xB10
dCamera_c* dCam_getBody();
+2 -1
View File
@@ -56,6 +56,7 @@ struct UserSettings {
ConfigVar<int> fanfareVolume;
ConfigVar<bool> enableReverb;
ConfigVar<bool> enableHrtf;
ConfigVar<bool> menuSounds;
} audio;
// Game settings
@@ -119,6 +120,7 @@ struct UserSettings {
ConfigVar<bool> invertCameraXAxis;
ConfigVar<bool> invertCameraYAxis;
ConfigVar<float> freeCameraSensitivity;
ConfigVar<bool> debugFlyCam;
// Cheats
ConfigVar<bool> infiniteHearts;
@@ -154,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,
+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,
+162 -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,11 +235,12 @@ 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;
}
@@ -241,3 +252,150 @@ 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;
+102
View File
@@ -1040,6 +1040,11 @@ void dCamera_c::debugDrawInit() {
bool dCamera_c::Run() {
#if TARGET_PC
ResetView();
if (executeDebugFlyCam()) {
mFrameCounter++;
mTicks++;
return true;
}
#endif
daAlink_c* link = daAlink_getAlinkActorClass();
@@ -7474,7 +7479,104 @@ bool dCamera_c::test2Camera(s32 param_0) {
return false;
}
static constexpr f32 FLYCAM_SPEED = 0.5f;
static constexpr f32 FLYCAM_FAST_SPEED = 4.0f;
static constexpr f32 FLYCAM_ROTATION_SPEED = 0.002f;
static constexpr f32 FLYCAM_TRIGGER_DEADZONE = 20.0f;
#if TARGET_PC
bool dCamera_c::executeDebugFlyCam() {
if (!dusk::getSettings().game.debugFlyCam) {
if (mDebugFlyCam.initialized) {
deactivateDebugFlyCam();
}
return false;
}
dEvt_control_c* event = dComIfGp_getEvent();
if (event == nullptr) {
return false;
}
if (!mDebugFlyCam.initialized && (event->mEventStatus != 0 || dComIfGp_isPauseFlag())) {
dusk::getSettings().game.debugFlyCam.setValue(false);
return false;
}
if (!mDebugFlyCam.initialized) {
mDebugFlyCam.savedCenter = mCenter;
mDebugFlyCam.savedEye = mEye;
mDebugFlyCam.savedFovy = mFovy;
mDebugFlyCam.savedBank = mBank;
f32 dx = mCenter.x - mEye.x;
f32 dy = mCenter.y - mEye.y;
f32 dz = mCenter.z - mEye.z;
mDebugFlyCam.yaw = atan2f(dz, dx);
f32 horizontal = sqrtf(dx * dx + dz * dz);
mDebugFlyCam.pitch = atan2f(dy, horizontal);
mDebugFlyCam.initialized = true;
}
event->mEventStatus = 1;
dComIfGp_getEventManager().setCameraPlay(1);
interface_of_controller_pad& pad = mDoCPd_c::getCpadInfo(0);
f32 stickY = pad.mMainStickPosY * 72.0f;
f32 stickX = pad.mMainStickPosX * 72.0f;
f32 cStickY = pad.mCStickPosY * 59.0f;
f32 cStickX = pad.mCStickPosX * 59.0f;
f32 trigL = pad.mTriggerLeft * 150.0f;
f32 trigR = pad.mTriggerRight * 150.0f;
f32 verticalDisp = 0.0f;
if (trigR >= FLYCAM_TRIGGER_DEADZONE) {
verticalDisp += trigR;
}
if (trigL >= FLYCAM_TRIGGER_DEADZONE) {
verticalDisp -= trigL;
}
f32 moveDy = stickY * sinf(mDebugFlyCam.pitch) + verticalDisp;
f32 moveDx = stickY * cosf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) - stickX * sinf(mDebugFlyCam.yaw);
f32 moveDz = stickY * sinf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) + stickX * cosf(mDebugFlyCam.yaw);
f32 speed = mDoCPd_c::getHoldZ(PAD_1) ? FLYCAM_FAST_SPEED : FLYCAM_SPEED;
mEye.x += speed * moveDx;
mEye.y += speed * moveDy;
mEye.z += speed * moveDz;
static constexpr f32 FLYCAM_TARGET_DIST = 100.0f;
mCenter.x = mEye.x + cosf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST;
mCenter.z = mEye.z + sinf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST;
mCenter.y = mEye.y + sinf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST;
Reset(mCenter, mEye);
f32 yawInput = dusk::getSettings().game.invertCameraXAxis ? cStickX : -cStickX;
mDebugFlyCam.yaw += yawInput * FLYCAM_ROTATION_SPEED;
mDebugFlyCam.yaw = fmodf(mDebugFlyCam.yaw + 2.0f * (f32)M_PI, 2.0f * (f32)M_PI);
f32 maxPitch = (f32)M_PI / 2.0f - 0.1f;
f32 minPitch = -(f32)M_PI / 2.0f + 0.1f;
mDebugFlyCam.pitch = std::clamp(mDebugFlyCam.pitch + cStickY * FLYCAM_ROTATION_SPEED, minPitch, maxPitch);
return true;
}
void dCamera_c::deactivateDebugFlyCam() {
Reset(mDebugFlyCam.savedCenter, mDebugFlyCam.savedEye, mDebugFlyCam.savedFovy, mDebugFlyCam.savedBank.Val());
dEvt_control_c* event = dComIfGp_getEvent();
if (event != nullptr) {
event->mEventStatus = 0;
}
dComIfGp_getEventManager().setCameraPlay(0);
mDebugFlyCam.initialized = false;
}
bool dCamera_c::freeCamera() {
if (dusk::getSettings().game.freeCamera && mGear == 1) {
mGear = 0;
-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
+18 -60
View File
@@ -1,9 +1,12 @@
#include "f_op/f_op_camera_mng.h"
#include "SSystem/SComponent/c_xyz.h"
#include "d/d_com_inf_game.h"
#include "imgui.h"
#include "ImGuiConfig.hpp"
#include "ImGuiConsole.hpp"
#include "ImGuiMenuTools.hpp"
#include "dusk/settings.h"
namespace dusk {
void ImGuiMenuTools::ShowCameraOverlay() {
@@ -46,70 +49,25 @@ namespace dusk {
ImGui::InputFloat("Camera FOV", &dCam->mFovy);
ImGui::SeparatorText("Free-look Data");
ImGui::SeparatorText("Options");
static float eyeYawDeg = 0.0f;
static float moveSpeed = 5000.0f;
static float rotSpeed = 5.0f;
static cXyz freeLookPos = cXyz::Zero;
static bool freeLookActive = false;
bool changed = false;
if (ImGui::IsKeyDown(ImGuiKey_LeftArrow)) {
eyeYawDeg += rotSpeed;
if (eyeYawDeg >= 360.0f)
eyeYawDeg -= 360.0f;
changed = true;
bool eventRunning = (dComIfGp_event_runCheck() || dComIfGp_isPauseFlag()) && !getSettings().game.debugFlyCam;
if (eventRunning) {
ImGui::BeginDisabled();
}
else if (ImGui::IsKeyDown(ImGuiKey_RightArrow)) {
eyeYawDeg -= rotSpeed;
if (eyeYawDeg < 0.0f)
eyeYawDeg += 360.0f;
changed = true;
config::ImGuiCheckbox("Fly Mode", getSettings().game.debugFlyCam);
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
if (eventRunning) {
ImGui::SetTooltip("Cannot enable while paused or during an active event.");
} else {
ImGui::SetTooltip("Detach camera and fly freely.\n"
"Left stick: move, C-stick: look\n"
"L/R triggers: up/down, Z: fast");
}
}
cSAngle yawAngle = cSAngle(eyeYawDeg);
cXyz frontDir = cXyz(yawAngle.Sin(), 0.0f, yawAngle.Cos());
if (ImGui::IsKeyDown(ImGuiKey_UpArrow)) {
freeLookPos -= frontDir * moveSpeed * ImGui::GetIO().DeltaTime;
changed = true;
if (eventRunning) {
ImGui::EndDisabled();
}
else if (ImGui::IsKeyDown(ImGuiKey_DownArrow)) {
freeLookPos += frontDir * moveSpeed * ImGui::GetIO().DeltaTime;
changed = true;
}
if (ImGui::IsKeyDown(ImGuiKey_LeftShift)) {
freeLookPos += cXyz::BaseY * moveSpeed * ImGui::GetIO().DeltaTime;
changed = true;
}
if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl)) {
freeLookPos -= cXyz::BaseY * moveSpeed * ImGui::GetIO().DeltaTime;
changed = true;
}
if (!freeLookActive && changed) {
freeLookPos += dCam->Center();
freeLookActive = true;
}
if (ImGui::IsKeyDown(ImGuiKey_R)) {
freeLookPos = cXyz::Zero;
freeLookActive = false;
}
if (freeLookActive) {
dCam->Reset(freeLookPos, freeLookPos + (frontDir * 100.0f));
}
ImGui::InputFloat("Free-look Yaw", &eyeYawDeg);
ImGui::InputFloat3("Free-look Position", &freeLookPos.x);
ImGui::InputFloat("Free-look Move Speed", &moveSpeed);
ImGui::InputFloat("Free-look Rotation Speed", &rotSpeed);
ShowCornerContextMenu(m_cameraOverlayCorner, 0);
+9 -66
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;
@@ -324,15 +278,10 @@ namespace dusk {
// 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
@@ -356,14 +305,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;
@@ -399,10 +343,9 @@ namespace dusk {
m_menuTools.ShowSaveEditor();
m_menuTools.ShowStateShare();
}
m_menuTools.ShowStateShare();
m_menuRandomizer.windowRandoStats();
m_menuRandomizer.windowRandoGeneration();
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 "ImGuiMenuRandomizer.hpp"
@@ -43,15 +41,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;
ImGuiMenuRandomizer m_menuRandomizer;
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
+1
View File
@@ -601,6 +601,7 @@ namespace dusk {
getSettings().game.freeMagicArmor.setValue(false);
getSettings().game.enableTurboKeybind.setValue(false);
getSettings().game.debugFlyCam.setValue(false);
}
SpeedrunInfo m_speedrunInfo;
+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;
};
}
+9 -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},
@@ -78,6 +79,7 @@ UserSettings g_userSettings = {
.invertCameraXAxis {"game.invertCameraXAxis", false},
.invertCameraYAxis {"game.invertCameraYAxis", false},
.freeCameraSensitivity {"game.freeCameraSensitivity", 1.0f},
.debugFlyCam {"game.debugFlyCam", false},
// Cheats
.infiniteHearts {"game.infiniteHearts", false},
@@ -113,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)}
}
};
@@ -136,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);
@@ -203,6 +205,7 @@ void registerSettings() {
Register(g_userSettings.game.gyroInvertPitch);
Register(g_userSettings.game.gyroInvertYaw);
Register(g_userSettings.game.freeCamera);
Register(g_userSettings.game.debugFlyCam);
Register(g_userSettings.backend.isoPath);
Register(g_userSettings.backend.graphicsBackend);
@@ -210,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
+11 -2
View File
@@ -1,9 +1,16 @@
#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)), mIsModified(std::move(props.isModified)) {}
@@ -27,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;
+1
View File
@@ -7,6 +7,7 @@ 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;
+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)) {
+46 -52
View File
@@ -246,18 +246,16 @@ void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) {
mRightPane = &rightPane;
mActivePort = port;
auto showPage = [this, &rightPane, port](Page page) {
mPage = page;
render_page(rightPane, port, page);
};
auto addPageButton = [&leftPane, showPage](Page page, Rml::String key, auto getValue) {
leftPane
.add_select_button({
.key = std::move(key),
.getValue = std::move(getValue),
})
.on_focus([showPage, page](Rml::Event&) { showPage(page); })
.on_pressed([showPage, page] { showPage(page); });
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); });
@@ -266,47 +264,43 @@ void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) {
addPageButton(Page::Sticks, "Sticks", [] { return Rml::String(">"); });
leftPane.add_section("Options");
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; },
})
.on_focus([&rightPane](Rml::Event&) {
rightPane.clear();
rightPane.add_text("Apply configured dead zones to the sticks and analog triggers.");
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
.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; },
})
.on_focus([&rightPane](Rml::Event&) {
rightPane.clear();
rightPane.add_text("Treat analog trigger movement as digital L and R button input.");
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);
+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); });
});
}
+166 -16
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,15 +75,6 @@ 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;
}
}
return false;
}
const char* controller_change_type(Uint32 eventType) noexcept {
switch (eventType) {
case SDL_EVENT_GAMEPAD_ADDED:
@@ -389,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;
@@ -488,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);
}
@@ -510,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;
}
@@ -530,6 +669,7 @@ 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 {
@@ -541,17 +681,27 @@ void handle_event(const SDL_Event& event) noexcept {
}
}
dispatch_controller_change_event(event);
if (event.type != SDL_EVENT_GAMEPAD_BUTTON_DOWN && event.type != SDL_EVENT_GAMEPAD_BUTTON_UP &&
event.type != SDL_EVENT_GAMEPAD_AXIS_MOTION)
{
return;
}
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);
+10 -5
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>
@@ -51,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);
+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) {
+23 -7
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"
@@ -35,14 +42,18 @@ Popup::Popup() : Document(kDocumentSource), mRoot(mDocument->GetElementById("pop
.onClose = [this] { hide(false); },
.autoSelect = false,
});
mTabBar->add_tab("Settings", [] { push_document(std::make_unique<SettingsWindow>()); });
mTabBar->add_tab("Settings", [this] { push(std::make_unique<SettingsWindow>()); });
// mTabBar->add_tab("Warp", [] {
// // TODO
// });
mTabBar->add_tab("Editor", [] { push_document(std::make_unique<EditorWindow>()); });
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("Quit", [] { IsRunning = false; });
@@ -55,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;
@@ -121,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
+14 -3
View File
@@ -19,6 +19,7 @@ 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); });
@@ -32,7 +33,7 @@ void SelectButton::set_modified(bool value) {
if (mProps.modified != value) {
mValueElem->SetClass("modified", value);
if (value) {
mValueElem->SetInnerRML(fmt::format(" {}", escape(mProps.value)));
mValueElem->SetInnerRML(fmt::format("&nbsp;{}", escape(mProps.value)));
} else {
mValueElem->SetInnerRML(escape(mProps.value));
}
@@ -43,7 +44,7 @@ void SelectButton::set_modified(bool value) {
void SelectButton::set_value_label(const Rml::String& value) {
if (mProps.value != value) {
if (mProps.modified) {
mValueElem->SetInnerRML(fmt::format(" {}", escape(value)));
mValueElem->SetInnerRML(fmt::format("&nbsp;{}", escape(value)));
} else {
mValueElem->SetInnerRML(escape(value));
}
@@ -67,13 +68,23 @@ 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;
}
+9 -1
View File
@@ -15,7 +15,9 @@ public:
struct Props {
Rml::String key;
Rml::String value;
Rml::String icon;
bool modified = false;
bool submit = true;
};
SelectButton(Rml::Element* parent, Props props);
@@ -31,6 +33,7 @@ protected:
Props mProps;
Rml::Element* mKeyElem = nullptr;
Rml::Element* mIconElem = nullptr;
Rml::Element* mValueElem = nullptr;
};
@@ -52,10 +55,15 @@ public:
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)}),
: 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)) {}
+408 -446
View File
@@ -56,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);
@@ -72,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;
@@ -79,276 +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),
.isModified = [&var] { return var.getValue() != var.getDefaultValue(); }})
.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),
.isModified = [&var] { return var.getValue() != var.getDefaultValue(); },
.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;
}
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);
},
.isModified =
[] {
return getSettings().audio.masterVolume.getValue() !=
getSettings().audio.masterVolume.getDefaultValue();
},
.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; },
.isModified =
[] {
return getSettings().game.damageMultiplier.getValue() !=
getSettings().game.damageMultiplier.getDefaultValue();
},
.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",
});
});
@@ -367,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");
@@ -420,199 +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);
},
});
config_bool_select(leftPane, rightPane, getSettings().game.pauseOnFocusLost,
{
.key = "Pause on Focus Lost",
.isDisabled = [] { return IsMobile; },
});
leftPane.add_section("Resolution");
leftPane
.add_select_button({
.key = "Internal Resolution",
.getValue =
[] {
return format_graphics_setting_value(GraphicsOption::InternalResolution,
getSettings().game.internalResolutionScale.getValue());
// 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);
},
.isModified =
[] {
return getSettings().game.internalResolutionScale.getValue() !=
getSettings().game.internalResolutionScale.getDefaultValue();
return getSettings().audio.masterVolume.getValue() !=
getSettings().audio.masterVolume.getDefaultValue();
},
})
.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());
},
.isModified =
[] {
return getSettings().game.shadowResolutionMultiplier.getValue() !=
getSettings().game.shadowResolutionMultiplier.getDefaultValue();
},
})
.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);
.max = 100,
.suffix = "%",
}),
rightPane, [](Pane& pane) {
pane.clear();
pane.add_text("Adjusts the volume of all sounds in the game.");
});
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()));
},
.isModified =
[] {
return getSettings().game.bloomMode.getValue() !=
getSettings().game.bloomMode.getDefaultValue();
},
})
.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; },
.isModified =
[] {
return getSettings().game.bloomMultiplier.getValue() !=
getSettings().game.bloomMultiplier.getDefaultValue();
},
})
.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?
@@ -622,28 +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
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.",
});
});
}
+58 -12
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 {
@@ -41,7 +44,15 @@ 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_pressed([this] { mProps.onClose(); });
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");
}
@@ -85,12 +96,14 @@ 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;
}
}
@@ -104,7 +117,14 @@ 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);
}
@@ -134,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);
}
@@ -146,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 {
@@ -169,21 +207,29 @@ bool TabBar::handle_nav_command(Rml::Event& event, NavCommand cmd) {
bool isNext = cmd == NavCommand::Right || cmd == NavCommand::Next;
int currentComponent = mProps.selectedTabIndex;
if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
currentComponent = tab_containing(event.GetTargetElement());
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;
+3
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:
@@ -38,6 +40,7 @@ private:
std::vector<Tab> mTabs;
Rml::Element* mEndSpacer = nullptr;
bool mRedirectingScroll = false;
int mLastFocusedTabIndex = -1;
};
} // namespace dusk::ui
+14 -3
View File
@@ -52,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) {
@@ -90,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();
}
+19 -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>
@@ -136,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();
@@ -168,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;
}
@@ -184,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>
@@ -650,6 +649,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();