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
+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();