#include "settings.hpp" #include #include "aurora/gfx.h" #include "bool_button.hpp" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/config.hpp" #include "dusk/livesplit.h" #include "m_Do/m_Do_main.h" #include "number_button.hpp" #include "overlay.hpp" #include "pane.hpp" #include "ui.hpp" #include namespace dusk::ui { namespace { 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); } 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."; int bloom_multiplier_percent() { return std::clamp( static_cast(getSettings().game.bloomMultiplier.getValue() * 100.0f + 0.5f), 0, 100); } int float_setting_percent(ConfigVar& var) { return static_cast(var.getValue() * 100.0f + 0.5f); } bool gyro_enabled() { return getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal; } struct ConfigBoolProps { Rml::String key; Rml::String helpText; std::function onChange; std::function isDisabled; }; SelectButton& config_bool_select( Pane& leftPane, Pane& rightPane, ConfigVar& var, ConfigBoolProps props) { return leftPane .add_child(BoolButton::Props{ .key = std::move(props.key), .getValue = [&var] { return var.getValue(); }, .setValue = [&var, callback = std::move(props.onChange)](bool value) { if (value == var.getValue()) { return; } var.setValue(value); config::Save(); if (callback) { callback(value); } }, .isDisabled = std::move(props.isDisabled), }) .on_focus([&rightPane, helpText = std::move(props.helpText)](Rml::Event&) { rightPane.clear(); rightPane.add_rml(helpText); }); } SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar& var, Rml::String key, Rml::String helpText, int min, int max, int step = 5, std::function isDisabled = {}) { return leftPane .add_child(NumberButton::Props{ .key = std::move(key), .getValue = [&var] { return float_setting_percent(var); }, .setValue = [&var, min, max](int value) { var.setValue(std::clamp(value, min, max) / 100.0f); config::Save(); }, .isDisabled = std::move(isDisabled), .min = min, .max = max, .step = step, .suffix = "%", }) .on_focus([&rightPane, helpText = std::move(helpText)](Rml::Event&) { rightPane.clear(); rightPane.add_text(helpText); }); } class ControllerConfigWindow : public Window { public: ControllerConfigWindow() { for (int i = 0; i < 4; ++i) { add_tab(fmt::format("Port {}", i + 1), [this](Rml::Element* content) { auto& pane = add_child(content, Pane::Type::Controlled); pane.add_section("Coming soon"); }); } } }; } // namespace SettingsWindow::SettingsWindow() { add_tab("Audio", [this](Rml::Element* content) { auto& leftPane = add_child(content, Pane::Type::Controlled); auto& rightPane = add_child(content, Pane::Type::Uncontrolled); leftPane.add_section("Volume"); leftPane .add_child(NumberButton::Props{ .key = "Master Volume", .getValue = [] { return getSettings().audio.masterVolume.getValue(); }, .setValue = [](int value) { getSettings().audio.masterVolume.setValue(value); config::Save(); audio::SetMasterVolume(value / 100.f); }, .max = 100, .suffix = "%", }) .on_focus([&rightPane](Rml::Event&) { rightPane.clear(); rightPane.add_text("Adjusts the volume of all sounds in the game."); }); leftPane.add_section("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); }, }); 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(content, Pane::Type::Controlled); auto& rightPane = add_child(content, Pane::Type::Uncontrolled); auto addCheat = [&](const Rml::String& key, ConfigVar& 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(content, Pane::Type::Controlled); auto& rightPane = add_child(content, Pane::Type::Uncontrolled); auto addOption = [&](const Rml::String& key, ConfigVar& value, const Rml::String& helpText) { config_bool_select(leftPane, rightPane, value, { .key = key, .helpText = helpText, }); }; auto addSpeedrunDisabledOption = [&](const Rml::String& key, ConfigVar& 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.
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::Props{ .key = "Damage Multiplier", .getValue = [] { return getSettings().game.damageMultiplier.getValue(); }, .setValue = [](int value) { getSettings().game.damageMultiplier.setValue(value); config::Save(); }, .isDisabled = [] { return getSettings().game.speedrunMode; }, .min = 1, .max = 8, .prefix = "x", }) .on_focus([&rightPane](Rml::Event&) { rightPane.clear(); rightPane.add_text("Multiplies incoming damage."); }); addSpeedrunDisabledOption( "Instant Death", getSettings().game.instantDeath, "Any hit will instantly kill you."); addSpeedrunDisabledOption("No Heart Drops", getSettings().game.noHeartDrops, "Hearts will never drop from enemies, pots, and various other places."); leftPane.add_section("Quality of Life"); addOption("Bigger Wallets", getSettings().game.biggerWallets, "Wallet sizes are like in the HD version. (500, 1000, 2000)"); addOption("Disable Rupee Cutscenes", getSettings().game.disableRupeeCutscenes, "Rupees will not play cutscenes after you have collected them the first time."); addOption("Faster Climbing", getSettings().game.fastClimbing, "Quicker climbing on ladders and vines like the HD version."); addOption("Faster Tears of Light", getSettings().game.fastTears, "Tears of Light dropped by Shadow Insects pop out faster like the HD version."); addOption("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.", .onChange = [](bool enabled) { if (enabled) { speedrun::connectLiveSplit(); } else { speedrun::disconnectLiveSplit(); } }, .isDisabled = [] { return !getSettings().game.speedrunMode; }, }); }); add_tab("Input", [this](Rml::Element* content) { auto& leftPane = add_child(content, Pane::Type::Controlled); auto& rightPane = add_child(content, Pane::Type::Uncontrolled); auto addOption = [&](const Rml::String& key, ConfigVar& value, const Rml::String& helpText, std::function isDisabled = {}) { config_bool_select(leftPane, rightPane, value, { .key = key, .helpText = helpText, .isDisabled = std::move(isDisabled), }); }; leftPane.add_section("Controller"); leftPane.add_button("Configure Controller") .on_pressed([] { push_document(std::make_unique()); }) .on_focus([&rightPane](Rml::Event&) { rightPane.clear(); rightPane.add_text("Open controller binding configuration."); }); 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; }); leftPane.add_section("Gyro"); addOption("Gyro Aim", getSettings().game.enableGyroAim, "Enables gyro aiming on supported controllers while in look mode and while aiming " "items."); addOption("Gyro Rollgoal", getSettings().game.enableGyroRollgoal, "Enables gyro controls for Rollgoal in Hena's Cabin."); 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; }); 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("Graphics", [this](Rml::Element* content) { auto& leftPane = add_child(content, Pane::Type::Controlled); auto& rightPane = add_child(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); }, }); leftPane.add_section("Resolution"); leftPane .add_select_button({ .key = "Internal Resolution", .getValue = [] { return format_graphics_setting_value(GraphicsOption::InternalResolution, getSettings().game.internalResolutionScale.getValue()); }, }) .on_nav_command([](Rml::Event&, NavCommand cmd) { if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || cmd == NavCommand::Right) { push_document(std::make_unique(OverlayProps{ .option = GraphicsOption::InternalResolution, .title = "Internal Resolution", .helpText = kInternalResolutionHelpText, .valueMin = 0, .valueMax = 12, .defaultValue = 0, })); return true; } return false; }) .on_focus([&rightPane](Rml::Event&) { rightPane.clear(); rightPane.add_text(kInternalResolutionHelpText); }); leftPane .add_select_button({ .key = "Shadow Resolution", .getValue = [] { return format_graphics_setting_value(GraphicsOption::ShadowResolution, getSettings().game.shadowResolutionMultiplier.getValue()); }, }) .on_nav_command([](Rml::Event&, NavCommand cmd) { if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || cmd == NavCommand::Right) { push_document(std::make_unique(OverlayProps{ .option = GraphicsOption::ShadowResolution, .title = "Shadow Resolution", .helpText = kShadowResolutionHelpText, .valueMin = 1, .valueMax = 8, .defaultValue = 1, })); return true; } return false; }) .on_focus([&rightPane](Rml::Event&) { rightPane.clear(); rightPane.add_text(kShadowResolutionHelpText); }); leftPane.add_section("Post-Processing"); leftPane .add_select_button({ .key = "Bloom", .getValue = [] { return format_graphics_setting_value(GraphicsOption::BloomMode, static_cast(getSettings().game.bloomMode.getValue())); }, }) .on_nav_command([](Rml::Event&, NavCommand cmd) { if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || cmd == NavCommand::Right) { push_document(std::make_unique(OverlayProps{ .option = GraphicsOption::BloomMode, .title = "Bloom", .helpText = kBloomHelpText, .valueMin = static_cast(BloomMode::Off), .valueMax = static_cast(BloomMode::Dusk), .defaultValue = static_cast(BloomMode::Classic), })); return true; } return false; }) .on_focus([&rightPane](Rml::Event&) { rightPane.clear(); rightPane.add_text(kBloomHelpText); }); leftPane .add_select_button({ .key = "Bloom Brightness", .getValue = [] { return format_graphics_setting_value( GraphicsOption::BloomMultiplier, bloom_multiplier_percent()); }, .isDisabled = [] { return getSettings().game.bloomMode.getValue() == BloomMode::Off; }, }) .on_nav_command([](Rml::Event&, NavCommand cmd) { if (cmd == NavCommand::Confirm || cmd == NavCommand::Left || cmd == NavCommand::Right) { push_document(std::make_unique(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, { .key = "Unlock Framerate", .helpText = "Uses inter-frame interpolation to enable higher frame rates.

Visual " "artifacts, animation glitches, or instability may occur.", }); 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", }); }); } } // namespace dusk::ui