Files
dusklight/src/dusk/ui/settings.cpp
T
SuperDude88 1b4a842eec Invert First Person Aiming (#870)
- Inverts first person aiming on either axis, for both stick and gyro controls

Could be separated from gyro if desirable, but I think it makes the most sense to have it apply to both
2026-05-10 19:00:20 -06:00

1119 lines
49 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "settings.hpp"
#include "aurora/gfx.h"
#include "bool_button.hpp"
#include "controller_config.hpp"
#include "dusk/audio/DuskAudioSystem.h"
#include "dusk/audio/DuskDsp.hpp"
#include "dusk/config.hpp"
#include "dusk/file_select.hpp"
#include "dusk/imgui/ImGuiEngine.hpp"
#include "dusk/livesplit.h"
#include "dusk/main.h"
#include "graphics_tuner.hpp"
#include "m_Do/m_Do_main.h"
#include "menu_bar.hpp"
#include "number_button.hpp"
#include "menu_bar.hpp"
#include "pane.hpp"
#include "prelaunch.hpp"
#include "ui.hpp"
#if DUSK_ENABLE_SENTRY_NATIVE
#include "dusk/crash_reporting.h"
#endif
#include <algorithm>
namespace dusk::ui {
namespace {
constexpr std::array kLanguageNames = {
"English",
"German",
"French",
"Spanish",
"Italian",
};
constexpr std::array kCardFileTypes = {
"Card Image",
"GCI Folder",
};
constexpr std::array kFpsOverlayCornerNames = {
"Top Left",
"Top Right",
"Bottom Left",
"Bottom Right",
};
constexpr std::array kGyroInputModeLabels = {
"Sensor",
"Mouse",
};
bool try_parse_backend(std::string_view backend, AuroraBackend& outBackend) {
if (backend == "auto") {
outBackend = BACKEND_AUTO;
return true;
}
if (backend == "d3d11") {
outBackend = BACKEND_D3D11;
return true;
}
if (backend == "d3d12") {
outBackend = BACKEND_D3D12;
return true;
}
if (backend == "metal") {
outBackend = BACKEND_METAL;
return true;
}
if (backend == "vulkan") {
outBackend = BACKEND_VULKAN;
return true;
}
if (backend == "opengl") {
outBackend = BACKEND_OPENGL;
return true;
}
if (backend == "opengles") {
outBackend = BACKEND_OPENGLES;
return true;
}
if (backend == "webgpu") {
outBackend = BACKEND_WEBGPU;
return true;
}
if (backend == "null") {
outBackend = BACKEND_NULL;
return true;
}
return false;
}
std::string_view backend_name(AuroraBackend backend) {
switch (backend) {
default:
return "Auto";
case BACKEND_D3D12:
return "D3D12";
case BACKEND_D3D11:
return "D3D11";
case BACKEND_METAL:
return "Metal";
case BACKEND_VULKAN:
return "Vulkan";
case BACKEND_OPENGL:
return "OpenGL";
case BACKEND_OPENGLES:
return "OpenGL ES";
case BACKEND_WEBGPU:
return "WebGPU";
case BACKEND_NULL:
return "Null";
}
}
std::string_view backend_id(AuroraBackend backend) {
switch (backend) {
default:
return "auto";
case BACKEND_D3D12:
return "d3d12";
case BACKEND_D3D11:
return "d3d11";
case BACKEND_METAL:
return "metal";
case BACKEND_VULKAN:
return "vulkan";
case BACKEND_OPENGL:
return "opengl";
case BACKEND_OPENGLES:
return "opengles";
case BACKEND_WEBGPU:
return "webgpu";
case BACKEND_NULL:
return "null";
}
}
std::vector<AuroraBackend> available_backends() {
std::vector<AuroraBackend> backends;
backends.emplace_back(BACKEND_AUTO);
size_t backendCount = 0;
const AuroraBackend* raw = aurora_get_available_backends(&backendCount);
for (size_t i = 0; i < backendCount; ++i) {
// Do not expose NULL or D3D11
if (raw[i] != BACKEND_NULL && raw[i] != BACKEND_D3D11) {
backends.emplace_back(raw[i]);
}
}
return backends;
}
AuroraBackend configured_backend() {
AuroraBackend configuredBackend = BACKEND_AUTO;
const auto configuredId = getSettings().backend.graphicsBackend.getValue();
if (!try_parse_backend(configuredId, configuredBackend)) {
configuredBackend = BACKEND_AUTO;
}
return configuredBackend;
}
void reset_for_speedrun_mode() {
mDoMain::developmentMode = -1;
getSettings().game.damageMultiplier.setValue(1);
getSettings().game.instantDeath.setValue(false);
getSettings().game.noHeartDrops.setValue(false);
getSettings().game.infiniteHearts.setValue(false);
getSettings().game.infiniteArrows.setValue(false);
getSettings().game.infiniteBombs.setValue(false);
getSettings().game.infiniteOil.setValue(false);
getSettings().game.infiniteOxygen.setValue(false);
getSettings().game.infiniteRupees.setValue(false);
getSettings().game.enableIndefiniteItemDrops.setValue(false);
getSettings().game.moonJump.setValue(false);
getSettings().game.superClawshot.setValue(false);
getSettings().game.alwaysGreatspin.setValue(false);
getSettings().game.enableFastIronBoots.setValue(false);
getSettings().game.canTransformAnywhere.setValue(false);
getSettings().game.fastSpinner.setValue(false);
getSettings().game.freeMagicArmor.setValue(false);
getSettings().game.enableTurboKeybind.setValue(false);
getSettings().game.debugFlyCam.setValue(false);
getSettings().game.autoSave.setValue(false);
}
const Rml::String kInternalResolutionHelpText =
"Configure the resolution used for rendering the game. Higher values are more demanding on "
"your graphics hardware.";
const Rml::String kShadowResolutionHelpText =
"Configure the shadow-map resolution. Higher values improve shadow quality but increase GPU "
"and memory usage.";
const Rml::String kBloomHelpText =
"Configure the post-processing bloom effect. Classic uses the original bloom pass; Dusk uses "
"a higher-quality bloom pass.";
const Rml::String kBloomBrightnessHelpText =
"Configure bloom intensity. Higher values make bright areas glow more strongly.";
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);
}
bool gyro_enabled() {
return getSettings().game.enableGyroAim ||
(getSettings().game.enableGyroRollgoal &&
getSettings().game.gyroMode.getValue() != GyroMode::Mouse);
}
struct ConfigBoolProps {
Rml::String key;
Rml::String icon;
Rml::String helpText;
std::function<void(bool)> onChange;
std::function<bool()> isDisabled;
};
SelectButton& config_bool_select(
Pane& leftPane, Pane& rightPane, ConfigVar<bool>& var, ConfigBoolProps props) {
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 = {}) {
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 graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, ConfigVar<T>& var,
const GraphicsTunerProps& props, bool prelaunch) {
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, prelaunch](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left ||
cmd == NavCommand::Right) {
window.push(std::make_unique<GraphicsTuner>(props, prelaunch));
return true;
}
return false;
}),
rightPane, [helpText = props.helpText](Pane& pane) {
pane.clear();
pane.add_text(helpText);
});
}
} // namespace
SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
if (prelaunch) {
mSuppressNavFallback = true;
add_tab("Prelaunch", [this](Rml::Element* content) {
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
leftPane.register_control(
leftPane
.add_select_button({
.key = "Disc Image",
.getValue =
[] {
const auto& path = prelaunch_state().configuredDiscPath;
std::string display;
if (path.empty()) {
display = "(none)";
} else {
display = display_name_for_path(path);
if (display.empty()) {
display = path;
}
}
return display;
},
.isModified =
[] {
const auto& state = prelaunch_state();
const auto& active = state.activeDiscPath;
return !active.empty() && state.configuredDiscPath != active;
},
})
.on_pressed([] { open_iso_picker(); }),
rightPane, [](Pane& pane) {
pane.add_rml("Set the disc image that Dusk uses to launch the game.<br/><br/>"
"Changes require a restart.");
});
leftPane.register_control(
leftPane.add_select_button({
.key = "Language",
.getValue =
[] {
const auto& state = prelaunch_state();
if (!state.configuredDiscCanLaunch || !state.configuredDiscInfo.isPal) {
return kLanguageNames[0];
}
const u8 idx = static_cast<u8>(getSettings().game.language.getValue());
return kLanguageNames[idx];
},
.isDisabled =
[] {
const auto& state = prelaunch_state();
return !state.configuredDiscCanLaunch ||
!state.configuredDiscInfo.isPal;
},
.isModified =
[] {
return getSettings().game.language.getValue() !=
prelaunch_state().initialLanguage;
},
}),
rightPane, [](Pane& pane) {
for (int i = 0; i < kLanguageNames.size(); i++) {
pane.add_button({
.text = kLanguageNames[i],
.isSelected =
[i] {
return getSettings().game.language.getValue() ==
static_cast<GameLanguage>(i);
},
})
.on_pressed([i] {
mDoAud_seStartMenu(kSoundItemChange);
getSettings().game.language.setValue(static_cast<GameLanguage>(i));
config::Save();
});
}
pane.add_rml("<br/>Changes require a restart.");
});
leftPane.register_control(
leftPane.add_select_button({
.key = "Graphics Backend",
.getValue = [] { return Rml::String{backend_name(configured_backend())}; },
.isModified =
[] {
return getSettings().backend.graphicsBackend.getValue() !=
prelaunch_state().initialGraphicsBackend;
},
}),
rightPane, [](Pane& pane) {
const auto availableBackends = available_backends();
for (const auto backend : availableBackends) {
pane
.add_button({
.text = Rml::String{backend_name(backend)},
.isSelected = [backend] { return configured_backend() == backend; },
})
.on_pressed([backend] {
mDoAud_seStartMenu(kSoundItemChange);
getSettings().backend.graphicsBackend.setValue(
std::string{backend_id(backend)});
config::Save();
});
}
pane.add_rml("<br/>Changes require a restart.");
});
leftPane.register_control(
leftPane.add_select_button({
.key = "Save File Type",
.getValue =
[] {
return kCardFileTypes[getSettings().backend.cardFileType.getValue()];
},
.isModified =
[] {
return getSettings().backend.cardFileType.getValue() !=
prelaunch_state().initialCardFileType;
},
}),
rightPane, [](Pane& pane) {
for (int i = 0; i < kCardFileTypes.size(); i++) {
pane
.add_button({
.text = kCardFileTypes[i],
.isSelected =
[i] {
return getSettings().backend.cardFileType.getValue() == i;
},
})
.on_pressed([i] {
mDoAud_seStartMenu(kSoundItemChange);
getSettings().backend.cardFileType.setValue(i);
config::Save();
});
}
});
});
}
add_tab("Video", [this](Rml::Element* content) {
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
auto& rightPane = add_child<Pane>(content, Pane::Type::Uncontrolled);
leftPane.add_section("Display");
leftPane.register_control(leftPane.add_button("Toggle Fullscreen").on_pressed([] {
mDoAud_seStartMenu(kSoundItemChange);
getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
VISetWindowFullscreen(getSettings().video.enableFullscreen);
config::Save();
}),
rightPane, [](Pane& pane) { pane.clear(); });
leftPane.register_control(leftPane.add_button("Restore Default Window Size").on_pressed([] {
mDoAud_seStartMenu(kSoundItemChange);
getSettings().video.enableFullscreen.setValue(false);
VISetWindowFullscreen(false);
VISetWindowSize(FB_WIDTH * 2, FB_HEIGHT * 2);
VICenterWindow();
}),
rightPane, [](Pane& pane) { pane.clear(); });
config_bool_select(leftPane, rightPane, getSettings().video.enableVsync,
{
.key = "Enable VSync",
.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.register_control(
leftPane.add_select_button({
.key = "Show FPS Counter",
.getValue =
[] {
if (!getSettings().video.enableFpsOverlay.getValue()) {
return Rml::String{"Off"};
}
const int idx = getSettings().video.fpsOverlayCorner.getValue();
return Rml::String{kFpsOverlayCornerNames[idx]};
},
.isModified =
[] {
const auto& enable = getSettings().video.enableFpsOverlay;
const auto& corner = getSettings().video.fpsOverlayCorner;
return enable.getValue() != enable.getDefaultValue() ||
(enable.getValue() && corner.getValue() != corner.getDefaultValue());
},
}),
rightPane, [](Pane& pane) {
pane.add_button(
{
.text = "Off",
.isSelected =
[] { return !getSettings().video.enableFpsOverlay.getValue(); },
})
.on_pressed([] {
mDoAud_seStartMenu(kSoundItemChange);
getSettings().video.enableFpsOverlay.setValue(false);
config::Save();
});
for (int i = 0; i < static_cast<int>(kFpsOverlayCornerNames.size()); ++i) {
pane.add_button(
{
.text = kFpsOverlayCornerNames[i],
.isSelected =
[i] {
return getSettings().video.enableFpsOverlay.getValue() &&
getSettings().video.fpsOverlayCorner.getValue() == i;
},
})
.on_pressed([i] {
mDoAud_seStartMenu(kSoundItemChange);
getSettings().video.enableFpsOverlay.setValue(true);
getSettings().video.fpsOverlayCorner.setValue(i);
config::Save();
});
}
pane.add_rml(
"<br/>Display the current framerate in a corner of the screen while playing.");
});
leftPane.add_section("Resolution");
graphics_tuner_control(*this, leftPane, rightPane,
getSettings().game.internalResolutionScale,
GraphicsTunerProps{
.option = GraphicsOption::InternalResolution,
.title = "Internal Resolution",
.helpText = kInternalResolutionHelpText,
.valueMin = 0,
.valueMax = 12,
.defaultValue = 0,
}, mPrelaunch);
graphics_tuner_control(*this, leftPane, rightPane,
getSettings().game.shadowResolutionMultiplier,
GraphicsTunerProps{
.option = GraphicsOption::ShadowResolution,
.title = "Shadow Resolution",
.helpText = kShadowResolutionHelpText,
.valueMin = 1,
.valueMax = 8,
.defaultValue = 1,
}, mPrelaunch);
leftPane.add_section("Post-Processing");
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMode,
GraphicsTunerProps{
.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),
}, mPrelaunch);
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMultiplier,
GraphicsTunerProps{
.option = GraphicsOption::BloomMultiplier,
.title = "Bloom Brightness",
.helpText = kBloomBrightnessHelpText,
.valueMin = 0,
.valueMax = 100,
.defaultValue = 100,
}, mPrelaunch);
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",
});
});
add_tab("Input", [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, std::function<bool()> isDisabled = {}) {
config_bool_select(leftPane, rightPane, value,
{
.key = key,
.helpText = helpText,
.isDisabled = std::move(isDisabled),
});
};
leftPane.add_section("Controller");
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.");
});
config_bool_select(leftPane, rightPane, getSettings().game.allowBackgroundInput,
{
.key = "Allow Background Input",
.helpText = "Allow controller input even when the game window is not focused.",
.onChange = [](bool value) { aurora_set_background_input(value); },
});
leftPane.add_section("Camera");
addOption("Free Camera", getSettings().game.freeCamera,
"Enables twin-stick camera control, letting the C-Stick move the camera vertically as "
"well as horizontally.");
addOption("Invert Camera X Axis", getSettings().game.invertCameraXAxis,
"Invert horizontal camera movement.");
addOption("Invert Camera Y Axis", getSettings().game.invertCameraYAxis,
"Invert vertical camera movement when Free Camera is enabled.",
[] { return !getSettings().game.freeCamera; });
config_percent_select(leftPane, rightPane, getSettings().game.freeCameraSensitivity,
"Free Camera Sensitivity", "Adjusts twin-stick camera sensitivity.", 50, 200, 5,
[] { return !getSettings().game.freeCamera; });
addOption("Invert First Person X Axis", getSettings().game.invertFirstPersonXAxis,
"Invert horizontal movement while aiming with items or first person camera. Applies to both stick and gyro aiming.");
addOption("Invert First Person Y Axis", getSettings().game.invertFirstPersonYAxis,
"Invert vertical movement while aiming with items or first person camera. Applies to both stick and gyro aiming.");
leftPane.add_section("Gyro");
leftPane.register_control(
leftPane.add_select_button({
.key = "Gyro Input Method",
.getValue =
[] {
const auto mode = getSettings().game.gyroMode.getValue();
const auto idx = static_cast<size_t>(mode);
return Rml::String{kGyroInputModeLabels[idx]};
},
.isModified =
[] {
return getSettings().game.gyroMode.getValue() !=
getSettings().game.gyroMode.getDefaultValue();
},
}),
rightPane, [](Pane& pane) {
for (size_t i = 0; i < kGyroInputModeLabels.size(); i++) {
pane
.add_button({
.text = Rml::String{kGyroInputModeLabels[i]},
.isSelected =
[i] {
return getSettings().game.gyroMode.getValue() == static_cast<GyroMode>(i);
},
})
.on_pressed([i] {
mDoAud_seStartMenu(kSoundItemChange);
const GyroMode mode = static_cast<GyroMode>(i);
getSettings().game.gyroMode.setValue(mode);
config::Save();
});
}
pane.add_rml(
"<br/><b>Sensor</b> reads motion directly from a supported controller's gyro via SDL.<br/>"
"<br/><b>Mouse</b> treats mouse input as gyro, intended for use with the Steam Deck.<br/>"
"<br/>Mouse input cannot currently be used with Gyro Rollgoal.");
});
addOption("Gyro Aim", getSettings().game.enableGyroAim,
"Enables gyro controls while in look mode, aiming a hawk, and aiming "
"supported items.<br/><br/>Supported items include the Slingshot, Gale Boomerang, "
"Hero's Bow, Clawshot(s), Ball and Chain, and Dominion Rod.");
addOption("Gyro Rollgoal", getSettings().game.enableGyroRollgoal,
"Enables gyro controls for Rollgoal in Hena's Cabin.",
[] { return getSettings().game.gyroMode.getValue() == GyroMode::Mouse; });
config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityY,
"Gyro Pitch Sensitivity", "Controls vertical gyro aiming sensitivity.", 25, 400, 5,
[] { return !gyro_enabled(); });
config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityX,
"Gyro Yaw Sensitivity", "Controls horizontal gyro aiming sensitivity.", 25, 400, 5,
[] { return !gyro_enabled(); });
config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityRollgoal,
"Rollgoal Sensitivity", "Controls how strongly gyro input tilts the Rollgoal table.",
25, 400, 5,
[] {
return !getSettings().game.enableGyroRollgoal ||
getSettings().game.gyroMode.getValue() == GyroMode::Mouse;
});
config_percent_select(leftPane, rightPane, getSettings().game.gyroDeadband, "Gyro Deadband",
"Ignores small gyro movement to reduce drift and jitter.", 0, 50, 1,
[] { return !gyro_enabled(); });
config_percent_select(leftPane, rightPane, getSettings().game.gyroSmoothing,
"Gyro Smoothing", "Higher values smooth gyro input over time.", 0, 100, 1,
[] { return !gyro_enabled(); });
addOption("Invert Gyro Pitch", getSettings().game.gyroInvertPitch,
"Invert vertical gyro aiming.", [] { return !gyro_enabled(); });
addOption("Invert Gyro Yaw", getSettings().game.gyroInvertYaw,
"Invert horizontal gyro aiming.", [] { return !gyro_enabled(); });
leftPane.add_section("Tools");
addOption("Turbo Key", getSettings().game.enableTurboKeybind,
"Hold Tab to increase game speed by up to 4x.",
[] { return getSettings().game.speedrunMode; });
});
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);
// 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().audio.masterVolume.getValue() !=
getSettings().audio.masterVolume.getDefaultValue();
},
.max = 100,
.suffix = "%",
}),
rightPane, [](Pane& pane) {
pane.clear();
pane.add_text("Adjusts the volume of all sounds in the game.");
});
leftPane.add_section("Effects");
config_bool_select(leftPane, rightPane, getSettings().audio.enableReverb,
{
.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 =
"Emulate surround sound via HRTF. Recommended only for use with headphones!",
.onChange = [](bool value) { audio::EnableHrtf = value; },
});
config_bool_select(leftPane, rightPane, getSettings().audio.menuSounds,
{
.key = "Dusk Menu Sounds",
.helpText = "Play sound effects when navigating the Dusk menu.",
});
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("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("Minimal HUD", getSettings().game.minimalHUD,
"Disables the elements of the main HUD of the game.<br/>Useful for 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",
.helpText = "Autosaves the game when going to a new area, opening a dungeon door, "
"or getting a new item.",
});
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.");
});
add_tab("Interface", [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("Dusk");
#if DUSK_CAN_OPEN_DATA_FOLDER
leftPane.register_control(
leftPane.add_button("Open Data Folder").on_pressed([] {
mDoAud_seStartMenu(kSoundClick);
dusk::OpenDataFolder();
}),
rightPane, [](Pane& pane) {
pane.add_text(
"Open the folder where Dusk stores settings, saves, logs, texture "
"replacements, and other app data.");
});
#endif
leftPane.register_control(
leftPane.add_select_button({
.key = "Notifications",
.getValue = [] {
const bool ach = getSettings().game.enableAchievementToasts.getValue();
const bool ctl = getSettings().game.enableControllerToasts.getValue();
if (!ach && !ctl) {
return Rml::String{"Off"};
}
if (ach && ctl) {
return Rml::String{"All"};
}
return Rml::String{"Some"};
},
.isModified = [] {
const auto& ach = getSettings().game.enableAchievementToasts;
const auto& ctl = getSettings().game.enableControllerToasts;
return ach.getValue() != ach.getDefaultValue() || ctl.getValue() != ctl.getDefaultValue();
},
}),
rightPane, [](Pane& pane) {
pane.clear();
pane.add_button("Select All").on_pressed([] {
mDoAud_seStartMenu(kSoundItemChange);
getSettings().game.enableAchievementToasts.setValue(true);
getSettings().game.enableControllerToasts.setValue(true);
config::Save();
});
pane.add_button("Select None").on_pressed([] {
mDoAud_seStartMenu(kSoundItemChange);
getSettings().game.enableAchievementToasts.setValue(false);
getSettings().game.enableControllerToasts.setValue(false);
config::Save();
});
pane.add_section("Types");
pane.add_button(
{
.text = "Achievements",
.isSelected =
[] {
return getSettings().game.enableAchievementToasts.getValue();
},
})
.on_pressed([] {
mDoAud_seStartMenu(kSoundItemChange);
auto& v = getSettings().game.enableAchievementToasts;
v.setValue(!v.getValue());
config::Save();
});
pane.add_button(
{
.text = "Controller",
.isSelected =
[] { return getSettings().game.enableControllerToasts.getValue(); },
})
.on_pressed([] {
mDoAud_seStartMenu(kSoundItemChange);
auto& v = getSettings().game.enableControllerToasts;
v.setValue(!v.getValue());
config::Save();
});
pane.add_rml("<br/>Choose which notifications can be displayed.");
});
#if DUSK_ENABLE_SENTRY_NATIVE
auto& crashReporting = leftPane.add_child<BoolButton>(BoolButton::Props{
.key = "Crash Reporting",
.getValue =
[] { return crash_reporting::get_consent() == crash_reporting::Consent::Given; },
.setValue = [](bool enabled) { crash_reporting::set_consent(enabled); },
.isDisabled =
[] {
return crash_reporting::get_consent() == crash_reporting::Consent::Unavailable;
},
.isModified = [] { return false; },
});
leftPane.register_control(crashReporting, rightPane, [](Pane& pane) {
pane.clear();
pane.add_rml("Dusk can automatically send crash reports to the developers. Crash "
"reports contain the following:<br/>• Operating system version<br/>• CPU "
"architecture<br/>• GPU model & driver version<br/>• File paths (may "
"include account username)<br/>• Stack trace");
});
#endif
config_bool_select(leftPane, rightPane, getSettings().backend.skipPreLaunchUI,
{
.key = "Skip Dusk Main Menu",
.helpText = "When starting Dusk, skip the main menu and boot straight into the "
"game if a disc image is available.",
});
config_bool_select(leftPane, rightPane, getSettings().backend.showPipelineCompilation,
{
.key = "Show Pipeline Compilation",
.helpText = "Show an overlay when shaders are being compiled for your hardware.",
});
config_bool_select(leftPane, rightPane, getSettings().backend.checkForUpdates,
{
.key = "Check for Updates",
.helpText = "Checks GitHub releases for a new Dusk version on startup.<br/><br/>"
"No personal information is transmitted or collected.",
});
config_bool_select(leftPane, rightPane, getSettings().game.pauseOnFocusLost,
{
.key = "Pause On Focus Lost",
.helpText = "Pause the game when window focus is lost.",
.onChange = [](bool value) { aurora_set_pause_on_focus_lost(value); },
});
config_bool_select(leftPane, rightPane, getSettings().backend.enableAdvancedSettings,
{
.key = "Enable Advanced Settings",
.icon = "warning",
.helpText = "Show advanced settings and debugging tools with "
"Shift+F1.<br/><br/><icon class=\"warning\"/> WARNING: Debugging tools "
"can easily break your game. Do not use on a regular save!",
.onChange =
[](bool) {
for (auto& doc : get_document_stack()) {
if (dynamic_cast<MenuBar*>(doc.get())) {
doc = std::make_unique<MenuBar>();
break;
}
}
},
});
leftPane.add_section("Game");
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.recordingMode,
{
.key = "Recording Mode",
.helpText = "Disables the game HUD and all background music.<br/><br/>Useful for "
"recording footage.",
});
});
}
void SettingsWindow::update() {
if (mPrelaunch && top_document() == this) {
try_push_verification_modal(*this);
}
Window::update();
}
} // namespace dusk::ui