mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-05-23 22:45:05 -04:00
2da6590657
* Add interpolation frame rate cap * wip: reworked framelimiter Based on my testing this is a bit more stable in frametimes. * wip: efficiency improvement + windows build fix Significantly improve efficiency by using a hybrid approach. * wip: UI changes * wip: end frame AFTER limiting * wip: remove unused include * wip: minor ui code change Makes it easier to remove/add presets * Simplify Limiter UI - Change enableFrameInterpolation to an enum with off/capped/unlimited values - Simplify the UI to use 2 settings (unlock framerate + a max value entry) * wip: slight limiter simplification * wip: implement review suggestions * wip: fix syntax error * wip: revert enum order + replace old checks * Fix compile error --------- Co-authored-by: SailorSnoW <sailorsnow@pm.me> Co-authored-by: Loïs <49660929+SailorSnoW@users.noreply.github.com> Co-authored-by: SuperDude88 <82904174+SuperDude88@users.noreply.github.com> Co-authored-by: Luke Street <luke@street.dev>
1430 lines
61 KiB
C++
1430 lines
61 KiB
C++
#include "settings.hpp"
|
||
|
||
#include "aurora/gfx.h"
|
||
#include "bool_button.hpp"
|
||
#include "controller_config.hpp"
|
||
#include "dusk/app_info.hpp"
|
||
#include "dusk/audio/DuskAudioSystem.h"
|
||
#include "dusk/audio/DuskDsp.hpp"
|
||
#include "dusk/config.hpp"
|
||
#include "dusk/hotkeys.h"
|
||
#include "dusk/data.hpp"
|
||
#include "dusk/file_select.hpp"
|
||
#include "dusk/imgui/ImGuiEngine.hpp"
|
||
#include "dusk/io.hpp"
|
||
#include "dusk/livesplit.h"
|
||
#include "dusk/main.h"
|
||
#include "dusk/discord_presence.hpp"
|
||
#include "graphics_tuner.hpp"
|
||
#include "m_Do/m_Do_main.h"
|
||
#include "menu_bar.hpp"
|
||
#include "modal.hpp"
|
||
#include "number_button.hpp"
|
||
#include "menu_bar.hpp"
|
||
#include "pane.hpp"
|
||
#include "prelaunch.hpp"
|
||
#include "ui.hpp"
|
||
|
||
#include <aurora/lib/window.hpp>
|
||
#include <SDL3/SDL_filesystem.h>
|
||
#include <fmt/format.h>
|
||
|
||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||
#include "dusk/crash_reporting.h"
|
||
#endif
|
||
|
||
#include <algorithm>
|
||
#include <filesystem>
|
||
|
||
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 kInterpolationModes = {
|
||
"Off",
|
||
"Capped",
|
||
"Unlimited",
|
||
};
|
||
|
||
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
|
||
if (raw[i] != BACKEND_NULL) {
|
||
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.enableTurboKeybind.setSpeedrunValue(false);
|
||
|
||
getSettings().game.damageMultiplier.setSpeedrunValue(1);
|
||
getSettings().game.instantDeath.setSpeedrunValue(false);
|
||
getSettings().game.noHeartDrops.setSpeedrunValue(false);
|
||
getSettings().game.autoSave.setSpeedrunValue(false);
|
||
getSettings().game.sunsSong.setSpeedrunValue(false);
|
||
|
||
getSettings().game.infiniteHearts.setSpeedrunValue(false);
|
||
getSettings().game.infiniteArrows.setSpeedrunValue(false);
|
||
getSettings().game.infiniteSeeds.setSpeedrunValue(false);
|
||
getSettings().game.infiniteBombs.setSpeedrunValue(false);
|
||
getSettings().game.infiniteOil.setSpeedrunValue(false);
|
||
getSettings().game.infiniteOxygen.setSpeedrunValue(false);
|
||
getSettings().game.infiniteRupees.setSpeedrunValue(false);
|
||
getSettings().game.enableIndefiniteItemDrops.setSpeedrunValue(false);
|
||
getSettings().game.moonJump.setSpeedrunValue(false);
|
||
getSettings().game.superClawshot.setSpeedrunValue(false);
|
||
getSettings().game.alwaysGreatspin.setSpeedrunValue(false);
|
||
getSettings().game.enableFastIronBoots.setSpeedrunValue(false);
|
||
getSettings().game.canTransformAnywhere.setSpeedrunValue(false);
|
||
getSettings().game.fastRoll.setSpeedrunValue(false);
|
||
getSettings().game.fastSpinner.setSpeedrunValue(false);
|
||
getSettings().game.freeMagicArmor.setSpeedrunValue(false);
|
||
getSettings().game.invincibleEnemies.setSpeedrunValue(false);
|
||
|
||
getSettings().game.pauseOnFocusLost.setSpeedrunValue(false);
|
||
aurora_set_pause_on_focus_lost(false);
|
||
|
||
getSettings().backend.enableAdvancedSettings.setSpeedrunValue(false);
|
||
getSettings().game.recordingMode.setSpeedrunValue(false);
|
||
getSettings().game.debugFlyCam.setSpeedrunValue(false);
|
||
}
|
||
|
||
void clear_speedrun_overrides() {
|
||
config::EnumerateRegistered([](config::ConfigVarBase& cvar) {
|
||
cvar.clearSpeedrunOverride();
|
||
});
|
||
}
|
||
|
||
void restore_from_speedrun_mode() {
|
||
clear_speedrun_overrides();
|
||
aurora_set_pause_on_focus_lost(getSettings().game.pauseOnFocusLost.getValue());
|
||
}
|
||
|
||
std::filesystem::path normalized_display_path(const std::filesystem::path& path) {
|
||
std::error_code ec;
|
||
auto normalized = std::filesystem::weakly_canonical(path, ec);
|
||
if (!ec) {
|
||
return normalized;
|
||
}
|
||
|
||
normalized = std::filesystem::absolute(path, ec);
|
||
if (!ec) {
|
||
return normalized.lexically_normal();
|
||
}
|
||
|
||
return path.lexically_normal();
|
||
}
|
||
|
||
std::filesystem::path user_home_path() {
|
||
const char* homePath = SDL_GetUserFolder(SDL_FOLDER_HOME);
|
||
if (homePath == nullptr || homePath[0] == '\0') {
|
||
return {};
|
||
}
|
||
return std::filesystem::path{reinterpret_cast<const char8_t*>(homePath)};
|
||
}
|
||
|
||
Rml::String abbreviated_data_path_string() {
|
||
const auto path = data::configured_data_path();
|
||
const auto homePath = user_home_path();
|
||
if (path.empty() || homePath.empty()) {
|
||
return io::fs_path_to_string(path);
|
||
}
|
||
|
||
const auto normalizedPath = normalized_display_path(path);
|
||
const auto normalizedHome = normalized_display_path(homePath);
|
||
if (normalizedPath == normalizedHome) {
|
||
return "~";
|
||
}
|
||
|
||
const auto relativePath = normalizedPath.lexically_relative(normalizedHome);
|
||
if (!relativePath.empty() && !relativePath.is_absolute()) {
|
||
const auto it = relativePath.begin();
|
||
if (it == relativePath.end() || *it != "..") {
|
||
return io::fs_path_to_string(std::filesystem::path{"~"} / relativePath);
|
||
}
|
||
}
|
||
|
||
return io::fs_path_to_string(path);
|
||
}
|
||
|
||
Rml::String configured_data_path_display_name() {
|
||
const auto path = abbreviated_data_path_string();
|
||
if (path.empty()) {
|
||
return "(none)";
|
||
}
|
||
|
||
auto display = display_name_for_path(path);
|
||
if (display.empty()) {
|
||
return path;
|
||
}
|
||
return display;
|
||
}
|
||
|
||
class DataFolderPathText : public Component {
|
||
public:
|
||
explicit DataFolderPathText(Rml::Element* parent) : Component(append(parent, "div")) {}
|
||
|
||
void update() override {
|
||
const Rml::String rml = "<span class=\"data-folder-current\">Current data folder:<br/>" +
|
||
escape(abbreviated_data_path_string()) + "</span>";
|
||
if (rml != mCurrentRml) {
|
||
mRoot->SetInnerRML(rml);
|
||
mCurrentRml = rml;
|
||
}
|
||
Component::update();
|
||
}
|
||
|
||
private:
|
||
Rml::String mCurrentRml;
|
||
};
|
||
|
||
void show_data_folder_error_modal(std::string_view message) {
|
||
auto dismiss = [](Modal& modal) {
|
||
mDoAud_seStartMenu(kSoundWindowClose);
|
||
modal.pop();
|
||
};
|
||
push_document(std::make_unique<Modal>(Modal::Props{
|
||
.title = "Data Folder Not Changed",
|
||
.bodyRml = escape(message),
|
||
.actions =
|
||
{
|
||
ModalAction{
|
||
.label = "OK",
|
||
.onPressed = dismiss,
|
||
},
|
||
},
|
||
.onDismiss = dismiss,
|
||
.icon = "warning",
|
||
}));
|
||
if (auto* doc = top_document()) {
|
||
doc->focus();
|
||
}
|
||
}
|
||
|
||
void data_folder_dialog_callback(void*, const char* path, const char* error) {
|
||
if (error != nullptr) {
|
||
show_data_folder_error_modal(error);
|
||
return;
|
||
}
|
||
if (path == nullptr) {
|
||
return;
|
||
}
|
||
|
||
std::string dataPathError;
|
||
if (data::set_custom_data_path(path, &dataPathError)) {
|
||
mDoAud_seStartMenu(kSoundItemChange);
|
||
return;
|
||
}
|
||
|
||
if (dataPathError.empty()) {
|
||
dataPathError =
|
||
fmt::format("{} could not use the selected folder as its data folder.", AppName);
|
||
}
|
||
show_data_folder_error_modal(dataPathError);
|
||
}
|
||
|
||
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; Dusklight 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 =
|
||
"<br/>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;
|
||
}
|
||
|
||
void add_speedrun_disabled_option(Pane& leftPane, Pane& rightPane, ConfigVar<bool>& var,
|
||
const Rml::String& key, const Rml::String& helpText) {
|
||
config_bool_select(leftPane, rightPane, var, {
|
||
.key = key,
|
||
.helpText = helpText,
|
||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||
});
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
SelectButton& config_int_select(Pane& leftPane, Pane& rightPane, ConfigVar<int>& var,
|
||
Rml::String key, Rml::String helpText, int min, int max, int step = 5,
|
||
std::function<bool()> isDisabled = {}, std::string suffix = "") {
|
||
auto& button = leftPane.add_child<NumberButton>(NumberButton::Props{
|
||
.key = std::move(key),
|
||
.getValue = [&var] { return var; },
|
||
.setValue =
|
||
[&var, min, max](int value) {
|
||
var.setValue(std::clamp(value, min, max));
|
||
config::Save();
|
||
},
|
||
.isDisabled = std::move(isDisabled),
|
||
.isModified = [&var] { return var.getValue() != var.getDefaultValue(); },
|
||
.min = min,
|
||
.max = max,
|
||
.step = step,
|
||
.suffix = 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 Dusklight uses to launch the game.<br/><br/>"
|
||
"Changes require a restart.");
|
||
});
|
||
#if DUSK_CAN_CHANGE_DATA_FOLDER
|
||
leftPane.register_control(
|
||
leftPane.add_select_button({
|
||
.key = "Data Folder",
|
||
.getValue = [] { return configured_data_path_display_name(); },
|
||
.isModified = [] { return data::is_data_path_restart_pending(); },
|
||
}),
|
||
rightPane, [](Pane& pane) {
|
||
pane.add_text("The data folder is where Dusklight stores settings, saves, "
|
||
"logs, texture replacements, and other app data.");
|
||
pane.add_child<DataFolderPathText>();
|
||
#if DUSK_CAN_OPEN_DATA_FOLDER
|
||
pane.add_button("Open Data Folder").on_pressed([] {
|
||
if (data::open_data_path()) {
|
||
mDoAud_seStartMenu(kSoundClick);
|
||
}
|
||
});
|
||
#endif
|
||
pane.add_button("Change Data Folder").on_pressed([] {
|
||
const auto defaultLocation =
|
||
io::fs_path_to_string(data::configured_data_path());
|
||
ShowFolderSelect(&data_folder_dialog_callback, nullptr,
|
||
aurora::window::get_sdl_window(),
|
||
defaultLocation.empty() ? nullptr : defaultLocation.c_str());
|
||
});
|
||
#if defined(_WIN32)
|
||
pane.add_button("Portable Mode").on_pressed([] {
|
||
if (data::set_portable_data_path()) {
|
||
mDoAud_seStartMenu(kSoundItemChange);
|
||
}
|
||
});
|
||
#endif
|
||
pane.add_button({
|
||
.text = "Reset to Default",
|
||
.isDisabled = [] { return data::is_default_data_path(); },
|
||
}).on_pressed([] {
|
||
if (data::reset_data_path()) {
|
||
mDoAud_seStartMenu(kSoundItemChange);
|
||
}
|
||
});
|
||
pane.add_rml("Data will be migrated automatically on restart.");
|
||
});
|
||
#endif
|
||
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",
|
||
.helpText = "Pause the game when window focus is lost.",
|
||
.onChange = [](bool value) { aurora_set_pause_on_focus_lost(value); },
|
||
.isDisabled = [] { return IsMobile || getSettings().game.speedrunMode; },
|
||
});
|
||
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,
|
||
.step = 10,
|
||
}, mPrelaunch);
|
||
|
||
leftPane.add_section("Rendering");
|
||
config_bool_select(leftPane, rightPane, getSettings().game.enableTextureReplacements,
|
||
{
|
||
.key = "Use Texture Pack",
|
||
.helpText = "Enable installed texture replacements.",
|
||
.onChange = [](bool value) { aurora_set_texture_replacements_enabled(value); },
|
||
});
|
||
leftPane.register_control(
|
||
leftPane.add_select_button({
|
||
.key = "Unlock Framerate",
|
||
.getValue =
|
||
[] {
|
||
return kInterpolationModes[static_cast<u8>(getSettings().game.enableFrameInterpolation.getValue())];
|
||
},
|
||
.isModified =
|
||
[] {
|
||
return getSettings().game.enableFrameInterpolation.getValue() !=
|
||
getSettings().game.enableFrameInterpolation.getDefaultValue();
|
||
},
|
||
}),
|
||
rightPane, [](Pane& pane) {
|
||
for (int i = 0; i < kInterpolationModes.size(); i++) {
|
||
pane.add_button({
|
||
.text = kInterpolationModes[i],
|
||
.isSelected =
|
||
[i] {
|
||
return getSettings().game.enableFrameInterpolation.getValue() == static_cast<FrameInterpMode>(i);
|
||
},
|
||
})
|
||
.on_pressed([i] {
|
||
mDoAud_seStartMenu(kSoundItemChange);
|
||
getSettings().game.enableFrameInterpolation.setValue(static_cast<FrameInterpMode>(i));
|
||
config::Save();
|
||
});
|
||
}
|
||
pane.add_rml(kUnlockFramerateHelpText);
|
||
});
|
||
config_int_select(leftPane, rightPane, getSettings().video.maxFrameRate,
|
||
"Framerate Cap", "Limit the framerate to the specified value.", 30, 540, 1,
|
||
[] { return getSettings().game.enableFrameInterpolation.getValue() != FrameInterpMode::Capped; });
|
||
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",
|
||
});
|
||
config_bool_select(leftPane, rightPane, getSettings().game.disableCutscenePillarboxing,
|
||
{
|
||
.key = "Disable Cutscene Pillarboxing",
|
||
});
|
||
});
|
||
|
||
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("Inputs");
|
||
leftPane.register_control(leftPane.add_button("Configure Inputs").on_pressed([this] {
|
||
push(std::make_unique<ControllerConfigWindow>(mPrelaunch));
|
||
}),
|
||
rightPane, [](Pane& pane) {
|
||
pane.clear();
|
||
pane.add_text("Open input binding configuration.");
|
||
});
|
||
config_bool_select(leftPane, rightPane, getSettings().game.allowBackgroundInput,
|
||
{
|
||
.key = "Allow Background Inputs",
|
||
.helpText = "Allow inputs 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 only to the control stick (the gyroscope can be inverted in Input settings).");
|
||
addOption("Invert First Person Y Axis", getSettings().game.invertFirstPersonYAxis,
|
||
"Invert vertical movement while aiming with items or first person camera. Applies only to the control stick (the gyroscope can be inverted in Input settings).");
|
||
|
||
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; });
|
||
addOption("Reset Key (" + Rml::String{hotkeys::DO_RESET} + ")",
|
||
getSettings().game.enableResetKeybind,
|
||
"Press " + Rml::String{hotkeys::DO_RESET} + " to reset the game.");
|
||
});
|
||
|
||
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(audio::MasterVolumeToLinear(value / 100.0f));
|
||
},
|
||
.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 = "Dusklight Menu Sounds",
|
||
.helpText = "Play sound effects when navigating the Dusklight 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) {
|
||
add_speedrun_disabled_option(leftPane, rightPane, value, key, helpText);
|
||
};
|
||
|
||
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.");
|
||
addSpeedrunDisabledOption("Autosave", getSettings().game.autoSave,
|
||
"Autosaves the game when going to a new area or opening a dungeon door.");
|
||
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.");
|
||
addSpeedrunDisabledOption("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 enabled) {
|
||
if (enabled) {
|
||
reset_for_speedrun_mode();
|
||
} else {
|
||
restore_from_speedrun_mode();
|
||
if (getSettings().game.liveSplitEnabled) {
|
||
speedrun::disconnectLiveSplit();
|
||
}
|
||
}
|
||
for (auto& doc : get_document_stack()) {
|
||
if (dynamic_cast<MenuBar*>(doc.get())) {
|
||
doc = std::make_unique<MenuBar>();
|
||
break;
|
||
}
|
||
}
|
||
},
|
||
});
|
||
config_bool_select(leftPane, rightPane, getSettings().game.liveSplitEnabled,
|
||
{
|
||
.key = "LiveSplit Connection",
|
||
.helpText = "Connect to LiveSplit server on localhost:16834. For this to work you must right click LiveSplit, and turn on Control -> Start TCP Server."
|
||
" To see IGT in LiveSplit you must change your comparison to Game Time.",
|
||
.onChange =
|
||
[](bool enabled) {
|
||
if (enabled) {
|
||
speedrun::connectLiveSplit();
|
||
} else {
|
||
speedrun::disconnectLiveSplit();
|
||
}
|
||
},
|
||
.isDisabled = [] { return IsMobile || !getSettings().game.speedrunMode; },
|
||
});
|
||
config_bool_select(leftPane, rightPane, getSettings().game.showSpeedrunRTATimer,
|
||
{
|
||
.key = "Show RTA",
|
||
.helpText = "Display the RTA timer. IGT is always visible.",
|
||
.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) {
|
||
add_speedrun_disabled_option(leftPane, rightPane, value, key, helpText);
|
||
};
|
||
|
||
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 Seeds", getSettings().game.infiniteSeeds, "Keeps your slingshot pellets (seeds) 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 heavy, including wearing the Iron Boots, holding the Ball and Chain, wearing Magic Armor without rupees, etc.");
|
||
addCheat("Can Transform Anywhere", getSettings().game.canTransformAnywhere,
|
||
"Allows transforming even if NPCs are looking.");
|
||
addCheat("Fast Roll", getSettings().game.fastRoll,
|
||
"Makes Link's roll animation and movement twice as fast.");
|
||
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.");
|
||
addCheat("Invincible Enemies", getSettings().game.invincibleEnemies,
|
||
"Prevents enemies from taking damage.");
|
||
});
|
||
|
||
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("Dusklight");
|
||
#if DUSK_CAN_OPEN_DATA_FOLDER
|
||
leftPane.register_control(
|
||
leftPane.add_button("Open Data Folder").on_pressed([] {
|
||
mDoAud_seStartMenu(kSoundClick);
|
||
data::open_data_path();
|
||
}),
|
||
rightPane, [](Pane& pane) {
|
||
pane.add_text(
|
||
"Open the folder where Dusklight 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 = "Missing Device",
|
||
.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("Dusklight 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 Dusklight Main Menu",
|
||
.helpText = "When starting Dusklight, 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 Dusklight version on startup.<br/><br/>"
|
||
"No personal information is transmitted or collected.",
|
||
});
|
||
#ifdef DUSK_DISCORD
|
||
config_bool_select(leftPane, rightPane, getSettings().game.enableDiscordPresence,
|
||
{
|
||
.key = "Enable Discord Rich Presence",
|
||
.helpText = "Enable Dusklight to integrate with Discord Rich Presence. This allows Discord to show your status in-game.",
|
||
.onChange = [](bool enabled) {
|
||
if (enabled) {
|
||
dusk::discord::initialize();
|
||
} else {
|
||
dusk::discord::shutdown();
|
||
}
|
||
},
|
||
});
|
||
#endif
|
||
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;
|
||
}
|
||
}
|
||
},
|
||
.isDisabled = [] { return getSettings().game.speedrunMode; },
|
||
});
|
||
config_bool_select(leftPane, rightPane, getSettings().game.showInputViewer,
|
||
{
|
||
.key = "Show Input Viewer",
|
||
.helpText = "Display a controller input overlay while playing.",
|
||
});
|
||
config_bool_select(leftPane, rightPane, getSettings().game.showInputViewerGyro,
|
||
{
|
||
.key = "Show Gyro Input Viewer",
|
||
.helpText = "Show gyro sensor values in the input viewer.",
|
||
.isDisabled = [] { return !getSettings().game.showInputViewer; },
|
||
});
|
||
|
||
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.",
|
||
});
|
||
add_speedrun_disabled_option(leftPane, rightPane, getSettings().game.recordingMode,
|
||
"Recording Mode",
|
||
"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
|