From 5eddcb9653798d6cc036972146c0c0f765707356 Mon Sep 17 00:00:00 2001 From: SuperDude88 <82904174+SuperDude88@users.noreply.github.com> Date: Tue, 12 May 2026 21:14:44 -0400 Subject: [PATCH 01/12] Discord RPC Toggle (#1120) * Discord RPC Toggle * I learned my lesson (Formatting) Took me long enough * Fix Mobile Platforms - ifdef the setting so it builds properly on platforms that don't have rpc - More formatting I missed --- include/dusk/settings.h | 1 + src/dusk/settings.cpp | 2 ++ src/dusk/ui/settings.cpp | 15 +++++++++++++++ src/m_Do/m_Do_main.cpp | 4 +++- 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/include/dusk/settings.h b/include/dusk/settings.h index de78432c98..12fe11d089 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -115,6 +115,7 @@ struct UserSettings { ConfigVar enableLinkDollRotation; ConfigVar enableAchievementToasts; ConfigVar enableControllerToasts; + ConfigVar enableDiscordPresence; // Graphics ConfigVar bloomMode; diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 9a13d0ecc8..4379587b38 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -52,6 +52,7 @@ UserSettings g_userSettings = { .enableLinkDollRotation {"game.enableLinkDollRotation", false}, .enableAchievementToasts {"game.enableAchievementToasts", true}, .enableControllerToasts {"game.enableControllerToasts", true}, + .enableDiscordPresence {"game.enableDiscordPresence", true}, // Graphics .bloomMode {"game.bloomMode", BloomMode::Dusk}, @@ -180,6 +181,7 @@ void registerSettings() { Register(g_userSettings.game.freeCameraSensitivity); Register(g_userSettings.game.minimalHUD); Register(g_userSettings.game.pauseOnFocusLost); + Register(g_userSettings.game.enableDiscordPresence); Register(g_userSettings.game.bloomMode); Register(g_userSettings.game.bloomMultiplier); Register(g_userSettings.game.disableWaterRefraction); diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 305f05f3b2..5bbe75bde0 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -12,6 +12,7 @@ #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" @@ -1249,6 +1250,20 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .helpText = "Checks GitHub releases for a new Dusklight version on startup.

" "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 Dusk 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", diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 8f0a02ad1d..da5ba8f55f 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -549,7 +549,9 @@ int game_main(int argc, char* argv[]) { } #ifdef DUSK_DISCORD - dusk::discord::initialize(); + if (dusk::getSettings().game.enableDiscordPresence) { + dusk::discord::initialize(); + } #endif VISetWindowTitle( From 4c5e3b933ecc900a30d6945431d9597e42fffba5 Mon Sep 17 00:00:00 2001 From: Sulfrix Date: Tue, 12 May 2026 20:15:33 -0500 Subject: [PATCH 02/12] set sdl app metadata when initializing audio (#1137) * set sdl app metadata when initializing audio * move metadata to main --- src/m_Do/m_Do_main.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index da5ba8f55f..723210706f 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -71,6 +71,7 @@ #include #include +#include "SDL3/SDL_init.h" #include "SDL3/SDL_filesystem.h" #include "SDL3/SDL_iostream.h" #include "SDL3/SDL_misc.h" @@ -524,6 +525,9 @@ int game_main(int argc, char* argv[]) { } } + // Set SDL metadata for audio mixers and macOS "About" menu + SDL_SetAppMetadata("Dusklight", DUSK_VERSION_STRING, "dev.twilitrealm.dusk"); + { const auto configPathString = dusk::ConfigPath.u8string(); AuroraConfig config{}; From 80af15c95b20e64073f94725d516a49044c44871 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Wed, 13 May 2026 03:16:37 +0200 Subject: [PATCH 03/12] Log build info on startup (#1117) Co-authored-by: Luke Street --- src/m_Do/m_Do_main.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 723210706f..ca681b9257 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -462,6 +462,11 @@ static std::string asset_path(const char* assetName) { return std::string("res/") + assetName; } +static void log_build_info() { + DuskLog.info("Build: {} (rev {}, built {}, type {})", DUSK_WC_DESCRIBE, DUSK_WC_REVISION, DUSK_WC_DATE, DUSK_BUILD_TYPE); + DuskLog.info("Platform: {}", DUSK_PLATFORM_NAME); +} + // ========================================================================= // PC ENTRY POINT // ========================================================================= @@ -511,6 +516,8 @@ int game_main(int argc, char* argv[]) { dusk::ConfigPath = dusk::data::initialize_data(); dusk::InitializeFileLogging(dusk::ConfigPath, startupLogLevel); + log_build_info(); + dusk::config::LoadFromUserPreferences(); ApplyCVarOverrides(parsed_arg_options["cvar"]); dusk::crash_reporting::initialize(); From 45196886b0afd034bcf18afbfed134a924e72242 Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Wed, 13 May 2026 03:16:49 +0200 Subject: [PATCH 04/12] Trim trailing newlines off OSReport (#1127) --- src/dusk/OSReport.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/dusk/OSReport.cpp b/src/dusk/OSReport.cpp index aee1e17bc6..422b88f03c 100644 --- a/src/dusk/OSReport.cpp +++ b/src/dusk/OSReport.cpp @@ -53,6 +53,11 @@ static std::string FormatToString(const char* msg, va_list list) { size *= 2; } } + + while (!str.empty() && str[str.size()-1] == '\n') { + str.pop_back(); + } + return str; } From 1b76b2650c26aac684c809e0d68dafc090838509 Mon Sep 17 00:00:00 2001 From: qwertyquerty Date: Tue, 12 May 2026 18:18:00 -0700 Subject: [PATCH 05/12] input viewer options in rmlui and reset key option (#1136) * input viewer options in rmlui * reset key --- extern/aurora | 2 +- include/dusk/settings.h | 3 +++ src/dusk/imgui/ImGuiConsole.cpp | 6 ++++++ src/dusk/imgui/ImGuiControllerOverlay.cpp | 9 +++++---- src/dusk/imgui/ImGuiMenuTools.cpp | 3 --- src/dusk/imgui/ImGuiMenuTools.hpp | 2 -- src/dusk/settings.cpp | 8 +++++++- src/dusk/ui/settings.cpp | 16 +++++++++++++++- 8 files changed, 37 insertions(+), 12 deletions(-) diff --git a/extern/aurora b/extern/aurora index ff9ca7170c..974d11dfe7 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit ff9ca7170cc840b1fbb5c7015b2fa1966055a04d +Subproject commit 974d11dfe727f213e34243f7c61fbcf0ef437968 diff --git a/include/dusk/settings.h b/include/dusk/settings.h index 12fe11d089..d57b15b9c1 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -175,12 +175,15 @@ struct UserSettings { // Controls ConfigVar enableTurboKeybind; + ConfigVar enableResetKeybind; // Tools ConfigVar speedrunMode; ConfigVar liveSplitEnabled; ConfigVar showSpeedrunRTATimer; ConfigVar recordingMode; + ConfigVar showInputViewer; + ConfigVar showInputViewerGyro; } game; struct { diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 4ac21d81df..7d1cb98524 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -260,6 +260,12 @@ namespace dusk { config::Save(); } + if (getSettings().game.enableResetKeybind && ImGui::GetIO().KeyCtrl && + ImGui::IsKeyPressed(ImGuiKey_R) && !fpcM_SearchByName(fpcNm_LOGO_SCENE_e)) + { + JUTGamePad::C3ButtonReset::sResetSwitchPushing = true; + } + if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_F1)) { if (getSettings().backend.enableAdvancedSettings) { m_isHidden = !m_isHidden; diff --git a/src/dusk/imgui/ImGuiControllerOverlay.cpp b/src/dusk/imgui/ImGuiControllerOverlay.cpp index 75637e5e61..ad07978a8b 100644 --- a/src/dusk/imgui/ImGuiControllerOverlay.cpp +++ b/src/dusk/imgui/ImGuiControllerOverlay.cpp @@ -3,12 +3,13 @@ #include "imgui.h" #include #include "ImGuiConsole.hpp" +#include "dusk/settings.h" #include namespace dusk { void ImGuiMenuTools::ShowInputViewer() { - if (!m_showInputViewer) { + if (!getSettings().game.showInputViewer) { return; } @@ -259,10 +260,10 @@ namespace dusk { size.y = 130 * scale; ImGui::Dummy(size); - if (PADHasSensor(PAD_1, PAD_SENSOR_GYRO) == TRUE) { + if (getSettings().game.showInputViewerGyro) + { ImGui::Separator(); - ImGui::Checkbox("Gyro Values", &m_showInputViewerGyro); - if (m_showInputViewerGyro) { + { ImGui::TextUnformatted("Gyro"); constexpr float kBarScale = 4.0f; diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 2206e0e3c6..c7afea3202 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -49,9 +49,6 @@ namespace dusk { ImGui::EndDisabled(); } - ImGui::Separator(); - ImGui::Checkbox("Show Input Viewer", &m_showInputViewer); - #if DUSK_CAN_OPEN_DATA_FOLDER ImGui::Separator(); if (ImGui::MenuItem("Open Data Folder")) { diff --git a/src/dusk/imgui/ImGuiMenuTools.hpp b/src/dusk/imgui/ImGuiMenuTools.hpp index 1c17ddbbe9..50cc697d09 100644 --- a/src/dusk/imgui/ImGuiMenuTools.hpp +++ b/src/dusk/imgui/ImGuiMenuTools.hpp @@ -69,8 +69,6 @@ namespace dusk { bool m_showStateShare = false; ImGuiStateShare m_stateShare; - bool m_showInputViewer = false; - bool m_showInputViewerGyro = false; bool m_showActorSpawner = false; int m_inputOverlayCorner = 3; std::string m_controllerName; diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 4379587b38..7475e5d48a 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -112,12 +112,15 @@ UserSettings g_userSettings = { // Controls .enableTurboKeybind {"game.enableTurboKeybind", false}, + .enableResetKeybind {"game.enableResetKeybind", false}, // Tools .speedrunMode {"game.speedrunMode", false}, .liveSplitEnabled {"game.liveSplitEnabled", false}, .showSpeedrunRTATimer {"game.showSpeedrunRTATimer", true}, - .recordingMode {"game.recordingMode", false} + .recordingMode {"game.recordingMode", false}, + .showInputViewer {"game.showInputViewer", false}, + .showInputViewerGyro {"game.showInputViewerGyro", false} }, .backend = { @@ -202,10 +205,13 @@ void registerSettings() { Register(g_userSettings.game.noLowHpSound); Register(g_userSettings.game.midnasLamentNonStop); Register(g_userSettings.game.enableTurboKeybind); + Register(g_userSettings.game.enableResetKeybind); Register(g_userSettings.game.speedrunMode); Register(g_userSettings.game.liveSplitEnabled); Register(g_userSettings.game.showSpeedrunRTATimer); Register(g_userSettings.game.recordingMode); + Register(g_userSettings.game.showInputViewer); + Register(g_userSettings.game.showInputViewerGyro); Register(g_userSettings.game.fastSpinner); Register(g_userSettings.game.infiniteHearts); Register(g_userSettings.game.infiniteArrows); diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 5bbe75bde0..b1769a7431 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -6,6 +6,7 @@ #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" @@ -711,7 +712,6 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { pane.add_rml( "
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, @@ -893,6 +893,9 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { 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) { @@ -1282,6 +1285,17 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { }, .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, From 93c7d0d64d34e2bd07ae9fd7798cbd3cd03289b2 Mon Sep 17 00:00:00 2001 From: SuperDude88 <82904174+SuperDude88@users.noreply.github.com> Date: Tue, 12 May 2026 21:21:28 -0400 Subject: [PATCH 06/12] Only Flip Stick Axes (#1132) Some people asked about this, I think I've come around to their position (follow-up to #870 ). I checked what TPHD does and its invert setting only changes the sticks --- src/d/actor/d_a_alink_link.inc | 4 ++-- src/dusk/ui/settings.cpp | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/d/actor/d_a_alink_link.inc b/src/d/actor/d_a_alink_link.inc index abbda0c5e2..a834535426 100644 --- a/src/d/actor/d_a_alink_link.inc +++ b/src/d/actor/d_a_alink_link.inc @@ -144,8 +144,8 @@ BOOL daAlink_c::setBodyAngleToCamera() { f32 gy_pitch = 0.f; dusk::gyro::getAimDeltas(gy_yaw, gy_pitch); - shape_angle.y = shape_angle.y + cM_rad2s(gy_yaw * gyro_scale * (dusk::getSettings().game.invertFirstPersonXAxis ? -1.0f : 1.0f)); - sp8 = sp8 + cM_rad2s(gy_pitch * gyro_scale * (dusk::getSettings().game.invertFirstPersonYAxis ? -1.0f : 1.0f)); + shape_angle.y = shape_angle.y + cM_rad2s(gy_yaw * gyro_scale); + sp8 = sp8 + cM_rad2s(gy_pitch * gyro_scale); if (checkNotItemSinkLimit() && sp8 > 0 && sp8 > mBodyAngle.x) { sp8 = mBodyAngle.x; diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index b1769a7431..b8d05f29cd 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -816,9 +816,9 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { "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."); + "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 to both stick and gyro aiming."); + "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( From aeeb1ccdd287855938870be116cb1c565fb5203e Mon Sep 17 00:00:00 2001 From: MelonSpeedruns Date: Tue, 12 May 2026 21:23:28 -0400 Subject: [PATCH 07/12] Auto Save Protection (#1102) * Auto Save Protection * added line behind target_pc define --------- Co-authored-by: MelonSpeedruns Co-authored-by: TakaRikka <38417346+TakaRikka@users.noreply.github.com> --- include/dusk/autosave.h | 1 + src/d/d_bright_check.cpp | 3 +++ src/d/d_s_name.cpp | 3 +++ src/d/d_s_play.cpp | 4 ++++ src/dusk/autosave.cpp | 7 ++++++- src/dusk/imgui/ImGuiStateShare.cpp | 3 +++ 6 files changed, 20 insertions(+), 1 deletion(-) diff --git a/include/dusk/autosave.h b/include/dusk/autosave.h index 248924fcb8..6670a66830 100644 --- a/include/dusk/autosave.h +++ b/include/dusk/autosave.h @@ -13,5 +13,6 @@ void enterAutoSave(); void autoSaving(); void waitingForWrite(); void endAutoSave(); +void toggleAutoSave(bool enabled); #endif \ No newline at end of file diff --git a/src/d/d_bright_check.cpp b/src/d/d_bright_check.cpp index 42ed7845cb..fc6924c9e7 100644 --- a/src/d/d_bright_check.cpp +++ b/src/d/d_bright_check.cpp @@ -13,6 +13,7 @@ #include "dusk/imgui/ImGuiConsole.hpp" #include "dusk/speedrun.h" #include "m_Do/m_Do_controller_pad.h" +#include dBrightCheck_c::dBrightCheck_c(JKRArchive* i_archive) { mArchive = i_archive; @@ -151,6 +152,8 @@ void dBrightCheck_c::modeMove() { dusk::m_speedrunInfo.startRun(); } } + + toggleAutoSave(true); #endif mCompleteCheck = true; mMode = MODE_WAIT_e; diff --git a/src/d/d_s_name.cpp b/src/d/d_s_name.cpp index cfeeb722d6..c3ae12e992 100644 --- a/src/d/d_s_name.cpp +++ b/src/d/d_s_name.cpp @@ -20,6 +20,7 @@ #include "m_Do/m_Do_machine.h" #include "m_Do/m_Do_main.h" #include "m_Do/m_Do_mtx.h" +#include #if TARGET_PC #define SHOW_TV_SETTINGS_SCREEN (this->mShowTvSettingsScreen) @@ -423,6 +424,8 @@ void dScnName_c::changeGameScene() { dusk::m_speedrunInfo.startRun(); } } + + toggleAutoSave(true); #endif } } diff --git a/src/d/d_s_play.cpp b/src/d/d_s_play.cpp index f7ebf6a20d..f2e7d5d926 100644 --- a/src/d/d_s_play.cpp +++ b/src/d/d_s_play.cpp @@ -1042,6 +1042,10 @@ static BOOL heapSizeCheck() { bool dScnPly_c::resetGame() { if (fpcM_GetName(this) == fpcNm_OPENING_SCENE_e) { + #if TARGET_PC + toggleAutoSave(false); + #endif + if (!dStage_roomControl_c::resetArchiveBank(0)) { return false; } diff --git a/src/dusk/autosave.cpp b/src/dusk/autosave.cpp index 779cb19915..6fce913910 100644 --- a/src/dusk/autosave.cpp +++ b/src/dusk/autosave.cpp @@ -2,6 +2,7 @@ #include "dusk/ui/ui.hpp" #include "imgui/ImGuiConsole.hpp" +bool shouldAutoSave = false; u8 mSaveBuffer[QUEST_LOG_SIZE * 3]; u8 mAutoSaveProc = 0; int autoSaveWriteState = 0; @@ -14,7 +15,7 @@ static AutoSaveFuncs AutoSaveFuncsProc[] = { void noAutoSave() {} void triggerAutoSave() { - if (dusk::getSettings().game.autoSave && mAutoSaveProc == 0 && + if (dusk::getSettings().game.autoSave && shouldAutoSave && mAutoSaveProc == 0 && strcmp(dComIfGp_getStartStageName(), "F_SP102") != 0) { mAutoSaveProc = 1; @@ -89,4 +90,8 @@ void endAutoSave() { .duration = std::chrono::milliseconds(1500), }); mAutoSaveProc = 0; +} + +void toggleAutoSave(bool enabled) { + shouldAutoSave = enabled; } \ No newline at end of file diff --git a/src/dusk/imgui/ImGuiStateShare.cpp b/src/dusk/imgui/ImGuiStateShare.cpp index 48849e2bc3..27c6c74517 100644 --- a/src/dusk/imgui/ImGuiStateShare.cpp +++ b/src/dusk/imgui/ImGuiStateShare.cpp @@ -18,6 +18,7 @@ #include #include +#include namespace dusk { @@ -135,6 +136,8 @@ bool ImGuiStateShare::applyEncodedState(const std::string& encoded, const std::s return false; } + toggleAutoSave(false); + StateSharePacket pkt; memcpy(&pkt, raw.data(), sizeof(pkt)); pkt.stageName[7] = '\0'; From 76efa02bebc18e3356b2430803d0c82405f46d9d Mon Sep 17 00:00:00 2001 From: Pieter-Jan Briers Date: Wed, 13 May 2026 03:23:49 +0200 Subject: [PATCH 08/12] Allow written files to be read by other applications (#1092) * Allow written files to be read by other applications Intended for the log file mainly. Hopefully fixes https://github.com/TwilitRealm/dusk/issues/966? * Consistency --- src/dusk/io.cpp | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/dusk/io.cpp b/src/dusk/io.cpp index 3c87a66acf..4f6ef16a9b 100644 --- a/src/dusk/io.cpp +++ b/src/dusk/io.cpp @@ -17,6 +17,8 @@ using namespace dusk::io; #else #define MODE(val) val #endif +#define _SH_DENYNO 0 +#define _SH_DENYWR 0 #endif static FILE* ThrowIfNotOpen(const FileStream& file) { @@ -31,19 +33,19 @@ static FILE* ThrowIfNotOpen(const FileStream& file) { throw std::system_error(std::make_error_code(static_cast(code))); } -static FILE* OpenCore(const std::filesystem::path& path, const MODE_TYPE* mode) { +static FILE* OpenCore(const std::filesystem::path& path, const MODE_TYPE* mode, int shareFlag) { FILE* file; int err; + errno = 0; #if _WIN32 static_assert(std::is_same_v); - err = _wfopen_s(&file, path.c_str(), mode); + file = _wfsopen(path.c_str(), mode, shareFlag); #else - errno = 0; static_assert(std::is_same_v); file = fopen(path.c_str(), mode); - err = errno; #endif + err = errno; if (!file) { ThrowForError(err); @@ -52,8 +54,8 @@ static FILE* OpenCore(const std::filesystem::path& path, const MODE_TYPE* mode) return file; } -static FILE* OpenCore(const char* path, const MODE_TYPE* mode) { - return OpenCore(reinterpret_cast(path), mode); +static FILE* OpenCore(const char* path, const MODE_TYPE* mode, int shareFlag) { + return OpenCore(reinterpret_cast(path), mode, shareFlag); } FileStream::FileStream() noexcept : file(nullptr) { @@ -76,19 +78,19 @@ FileStream::~FileStream() { } FileStream FileStream::OpenRead(const char* utf8Path) { - return FileStream(OpenCore(utf8Path, MODE("rb"))); + return FileStream(OpenCore(utf8Path, MODE("rb"), _SH_DENYWR)); } FileStream FileStream::OpenRead(const std::filesystem::path& path) { - return FileStream(OpenCore(path, MODE("rb"))); + return FileStream(OpenCore(path, MODE("rb"), _SH_DENYWR)); } FileStream FileStream::Create(const char* utf8Path) { - return FileStream(OpenCore(utf8Path, MODE("wb"))); + return FileStream(OpenCore(utf8Path, MODE("wb"), _SH_DENYWR)); } FileStream FileStream::Create(const std::filesystem::path& path) { - return FileStream(OpenCore(path, MODE("wb"))); + return FileStream(OpenCore(path, MODE("wb"), _SH_DENYWR)); } std::vector FileStream::ReadFull() { From ef43b943704ea0fce9cfc1e85c5d2840ca7b54b3 Mon Sep 17 00:00:00 2001 From: gymnast86 Date: Tue, 12 May 2026 21:36:07 -0400 Subject: [PATCH 09/12] Add options for binding custom buttons to specific actions (#1141) * custom action framework and first person custom action * add bind for midna call * custom binding for opening dusklight menu * turbo speed button action * text descriptions * fix not stopping default GC controller menu combo * more explanation text * block bind actions when in the dusklight menu --- files.cmake | 2 + include/dusk/action_bindings.h | 43 +++++ include/dusk/config.hpp | 7 + include/dusk/config_var.hpp | 2 + include/dusk/settings.h | 10 + libs/JSystem/src/JUtility/JUTGamePad.cpp | 7 + src/d/actor/d_a_alink.cpp | 9 + src/d/actor/d_a_alink_link.inc | 5 +- src/d/d_camera.cpp | 20 +- src/dusk/action_bindings.cpp | 96 ++++++++++ src/dusk/config.cpp | 8 + src/dusk/imgui/ImGuiConsole.cpp | 4 +- src/dusk/settings.cpp | 45 +++++ src/dusk/ui/controller_config.cpp | 231 ++++++++++++++++------- src/dusk/ui/controller_config.hpp | 5 + src/dusk/ui/input.cpp | 26 ++- src/dusk/ui/overlay.cpp | 14 +- 17 files changed, 456 insertions(+), 78 deletions(-) create mode 100644 include/dusk/action_bindings.h create mode 100644 src/dusk/action_bindings.cpp diff --git a/files.cmake b/files.cmake index 47527da735..450fae3b49 100644 --- a/files.cmake +++ b/files.cmake @@ -1411,6 +1411,7 @@ set(DOLPHIN_FILES ) set(DUSK_FILES + include/dusk/action_bindings.h include/dusk/endian_gx.hpp include/dusk/config.hpp include/dusk/dvd_asset.hpp @@ -1522,6 +1523,7 @@ set(DUSK_FILES src/dusk/discord.hpp src/dusk/discord_presence.cpp src/dusk/version.cpp + src/dusk/action_bindings.cpp ) set(DUSK_HTTP_BACKEND_FILES diff --git a/include/dusk/action_bindings.h b/include/dusk/action_bindings.h new file mode 100644 index 0000000000..7eba412fe8 --- /dev/null +++ b/include/dusk/action_bindings.h @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include "dusk/config_var.hpp" + +namespace dusk { + +enum class ActionBinds { + FIRST_PERSON_CAMERA, + CALL_MIDNA, + OPEN_DUSKLIGHT_MENU, + TURBO_SPEED_BUTTON, + COUNT, +}; + +struct ActionBindData { + std::array* configVars{}; + std::string actionName{}; +}; + +struct ActionBindPressData { + bool pressedCurFrame{false}; + bool pressedPrevFrame{false}; +}; + +using ActionBindsMap = std::unordered_map; + +ActionBindsMap& getActionBinds(); + +bool isActionBound(ActionBinds action, u32 port); + +void updateActionBindings(); + +bool getActionBindTrig(ActionBinds action, u32 port); + +bool getActionBindHold(ActionBinds action, u32 port); + +bool getActionBindHoldAnyPort(ActionBinds action); + +int getActionBindButton(ActionBinds action, u32 port); + +} diff --git a/include/dusk/config.hpp b/include/dusk/config.hpp index 72ec449b50..382c4c24c6 100644 --- a/include/dusk/config.hpp +++ b/include/dusk/config.hpp @@ -112,6 +112,13 @@ void Save(); */ ConfigVarBase* GetConfigVar(std::string_view name); +/** + * \brief Resets all custom action bindings for a specific port to nothing + * + * @param port The port to be cleared of action bindings + */ +void ClearAllActionBindings(int port); + /** * \brief Call a function on every registered CVar. */ diff --git a/include/dusk/config_var.hpp b/include/dusk/config_var.hpp index b16409c7f3..cc8c700ed4 100644 --- a/include/dusk/config_var.hpp +++ b/include/dusk/config_var.hpp @@ -287,6 +287,8 @@ public: } }; +using ActionBindConfigVar = ConfigVar; + } #endif // DUSK_CONFIG_VAR_HPP diff --git a/include/dusk/settings.h b/include/dusk/settings.h index d57b15b9c1..25fb08d246 100644 --- a/include/dusk/settings.h +++ b/include/dusk/settings.h @@ -1,6 +1,8 @@ #ifndef DUSK_CONFIG_H #define DUSK_CONFIG_H +#include + #include "dusk/config_var.hpp" namespace dusk { @@ -197,6 +199,14 @@ struct UserSettings { ConfigVar cardFileType; ConfigVar enableAdvancedSettings; } backend; + + // Arrays of size 4 for 4 ports + struct { + std::array firstPersonCamera; + std::array callMidna; + std::array openDusklightMenu; + std::array turboSpeedButton; + } actionBindings; }; UserSettings& getSettings(); diff --git a/libs/JSystem/src/JUtility/JUTGamePad.cpp b/libs/JSystem/src/JUtility/JUTGamePad.cpp index 2ddf4397dc..48ed5f130e 100644 --- a/libs/JSystem/src/JUtility/JUTGamePad.cpp +++ b/libs/JSystem/src/JUtility/JUTGamePad.cpp @@ -4,6 +4,10 @@ #include #include "os_report.h" +#if TARGET_PC +#include "dusk/action_bindings.h" +#endif + u32 JUTGamePad::CRumble::sChannelMask[4] = { PAD_CHAN0_BIT, PAD_CHAN1_BIT, @@ -85,6 +89,9 @@ u32 JUTGamePad::sRumbleSupported; u32 JUTGamePad::read() { sRumbleSupported = PADRead(mPadStatus); +#if TARGET_PC + dusk::updateActionBindings(); +#endif switch (sClampMode) { case EClampStick: diff --git a/src/d/actor/d_a_alink.cpp b/src/d/actor/d_a_alink.cpp index 0e0f0d06c7..498d3de173 100644 --- a/src/d/actor/d_a_alink.cpp +++ b/src/d/actor/d_a_alink.cpp @@ -51,10 +51,13 @@ #include "d/actor/d_a_ni.h" #include "d/d_s_play.h" +#if TARGET_PC +#include "dusk/action_bindings.h" #include "dusk/frame_interpolation.h" #include "dusk/settings.h" #include "res/Object/Alink.h" #include +#endif static int daAlink_Create(fopAc_ac_c* i_this); static int daAlink_Delete(daAlink_c* i_this); @@ -9363,6 +9366,12 @@ BOOL daAlink_c::spActionTrigger() { } BOOL daAlink_c::midnaTalkTrigger() const { +#if TARGET_PC + // If we have a custom bind for Midna, check that instead + if (dusk::isActionBound(dusk::ActionBinds::CALL_MIDNA, 0)) { + return dusk::getActionBindTrig(dusk::ActionBinds::CALL_MIDNA, 0); + } +#endif return mItemTrigger & BTN_Z; } diff --git a/src/d/actor/d_a_alink_link.inc b/src/d/actor/d_a_alink_link.inc index a834535426..636aabdffe 100644 --- a/src/d/actor/d_a_alink_link.inc +++ b/src/d/actor/d_a_alink_link.inc @@ -12,6 +12,7 @@ #if TARGET_PC #include "dusk/gyro.h" +#include "dusk/action_bindings.h" #endif bool daAlink_c::checkNoSubjectModeCamera() { @@ -192,7 +193,9 @@ BOOL daAlink_c::subjectCancelTrigger() { BOOL daAlink_c::checkSubjectEnd(BOOL i_isPlaySe) { setDoStatus(BUTTON_STATUS_BACK); - if (checkEventRun() || checkEquipAnime() || doTrigger() || checkSetItemTrigger(dItemNo_HAWK_EYE_e) || subjectCancelTrigger() || checkEndResetFlg0(ERFLG0_FORCE_SUBJECT_CANCEL) || dComIfGp_checkCameraAttentionStatus(field_0x317c, 0x2000)) { + // Allow pressing the first person binding to also leave first person + if (IF_DUSK(dusk::getActionBindTrig(dusk::ActionBinds::FIRST_PERSON_CAMERA, 0)) || + checkEventRun() || checkEquipAnime() || doTrigger() || checkSetItemTrigger(dItemNo_HAWK_EYE_e) || subjectCancelTrigger() || checkEndResetFlg0(ERFLG0_FORCE_SUBJECT_CANCEL) || dComIfGp_checkCameraAttentionStatus(field_0x317c, 0x2000)) { if (i_isPlaySe) { seStartSystem(Z2SE_SUBJ_VIEW_OUT); } diff --git a/src/d/d_camera.cpp b/src/d/d_camera.cpp index 99274f133d..bfc4c809c9 100644 --- a/src/d/d_camera.cpp +++ b/src/d/d_camera.cpp @@ -31,6 +31,7 @@ #if TARGET_PC #include "dusk/frame_interpolation.h" #include "dusk/logging.h" +#include "dusk/action_bindings.h" #include "imgui.h" #endif @@ -838,6 +839,12 @@ void dCamera_c::updatePad() { mTrigB = mDoCPd_c::getTrigB(mPadID) ? true : false; #if TARGET_PC + // If our custom action binding is triggered, and we're not already in first person, go into first person + if (dusk::getActionBindTrig(dusk::ActionBinds::FIRST_PERSON_CAMERA, mPadID) && mGear != -1) { + setComStat(0x1000); + mGear = 0; + } + if (mCamParam.mManualMode) { return; } @@ -877,7 +884,8 @@ void dCamera_c::updatePad() { if (mPadInfo.mCStick.mLastPosY < -mCamSetup.mCStick.SwTHH()) { if (mCStickYState != -1) { - if (mGear == -1 && mCurMode == 4) { + // Don't use regular first person trigger if custom mapping is set + if (mGear == -1 && mCurMode == 4 IF_DUSK(&& !dusk::isActionBound(dusk::ActionBinds::FIRST_PERSON_CAMERA, mPadID))) { mGear = 0; setComStat(0x2000); } else if (mGear == 0 && sp6C) { @@ -888,7 +896,8 @@ void dCamera_c::updatePad() { mCStickYState = -1; } else if (mPadInfo.mCStick.mLastPosY > mCamSetup.mCStick.SwTHH()) { if (mCStickYState != 1) { - if (mGear == 0 && sp6B) { + // Don't use regular first person trigger if custom mapping is set + if (mGear == 0 && sp6B IF_DUSK(&& !dusk::isActionBound(dusk::ActionBinds::FIRST_PERSON_CAMERA, mPadID))) { setComStat(0x1000); } else if (mGear == 1) { mGear = 0; @@ -7649,9 +7658,10 @@ bool dCamera_c::freeCamera() { f32 magnitude = sqrt(mPadInfo.mCStick.mLastPosX * mPadInfo.mCStick.mLastPosX + mPadInfo.mCStick.mLastPosY * mPadInfo.mCStick.mLastPosY); // If we aren't in manual cam mode, don't trigger it if the player tries to hit C-up - // for first person - if (mPadInfo.mCStick.mLastPosX != 0 || mPadInfo.mCStick.mLastPosY < 0 || - (mCamParam.mManualMode == 1 && mPadInfo.mCStick.mLastPosY != 0)) { + // for first person unless they have first person bound to a custom binding + if ((dusk::isActionBound(dusk::ActionBinds::FIRST_PERSON_CAMERA, mPadID) && mPadInfo.mCStick.mLastPosY != 0) || + mPadInfo.mCStick.mLastPosX != 0 || mPadInfo.mCStick.mLastPosY < 0 || (mCamParam.mManualMode == 1 && mPadInfo.mCStick.mLastPosY != 0)) + { mCamParam.mManualMode = 1; camMovement = camMovement.normalize(); camMovement.y *= dusk::getSettings().game.invertCameraYAxis ? 1.0f : -1.0f; diff --git a/src/dusk/action_bindings.cpp b/src/dusk/action_bindings.cpp new file mode 100644 index 0000000000..204f219558 --- /dev/null +++ b/src/dusk/action_bindings.cpp @@ -0,0 +1,96 @@ +#include "dusk/action_bindings.h" + +#include "aurora/lib/input.hpp" +#include "dusk/settings.h" +#include "dusk/ui/ui.hpp" + +namespace dusk { + +static std::array(ActionBinds::COUNT)>, PAD_CHANMAX> actionPressData{}; + +ActionBindsMap& getActionBinds() { + static ActionBindsMap actionBinds = { + {ActionBinds::FIRST_PERSON_CAMERA, {&getSettings().actionBindings.firstPersonCamera, "First Person Camera"}}, + {ActionBinds::CALL_MIDNA, {&getSettings().actionBindings.callMidna, "Call Midna"}}, + {ActionBinds::OPEN_DUSKLIGHT_MENU, {&getSettings().actionBindings.openDusklightMenu, "Open Dusklight Menu"}}, + {ActionBinds::TURBO_SPEED_BUTTON, {&getSettings().actionBindings.turboSpeedButton, "Turbo Speed Button"}}, + }; + return actionBinds; +} + +bool isActionBound(ActionBinds action, u32 port) { + auto& actionBinds = getActionBinds(); + // Check to make sure action is properly bound + if (!actionBinds.contains(action)) { + return false; + } + + return getActionBindButton(action, port) != PAD_NATIVE_BUTTON_INVALID; +} + +void updateActionBindings() { + for (u32 port = 0; port < PAD_CHANMAX; ++port) { + // Move the current press to the previous frame + for (auto& pressData : actionPressData[port]) { + pressData.pressedPrevFrame = pressData.pressedCurFrame; + pressData.pressedCurFrame = false; + } + + // Update current frame with whether action button is pressed + for (auto& [action, boundAction] : getActionBinds()) { + // If the action isn't bound, or if documents are visible and the action isn't + // opening the dusklight menu, don't update. Otherwise, we may accidentally + // perform actions while the dusklight menu is open. + if (!isActionBound(action, port) || + (ui::any_document_visible() && action != ActionBinds::OPEN_DUSKLIGHT_MENU)) { + continue; + } + + int button = boundAction.configVars->at(port); + + // If keyboard is active for this port + u32 count = 0; + if (PADGetKeyButtonBindings(port, &count) != nullptr) { + int numKeys = 0; + const bool* kbState = SDL_GetKeyboardState(&numKeys); + if (kbState[button]) { + actionPressData[port][static_cast(action)].pressedCurFrame = true; + } + } else { + // If controller is active + auto controller = aurora::input::get_controller_for_player(port); + if (controller) { + if (SDL_GetGamepadButton(controller->m_controller, static_cast(button))) { + actionPressData[port][static_cast(action)].pressedCurFrame = true; + } + } + } + } + } +} + +bool getActionBindTrig(ActionBinds action, u32 port) { + return isActionBound(action, port) && + actionPressData[port][static_cast(action)].pressedCurFrame && + !actionPressData[port][static_cast(action)].pressedPrevFrame; +} + +bool getActionBindHold(ActionBinds action, u32 port) { + return isActionBound(action, port) && + actionPressData[port][static_cast(action)].pressedCurFrame && + actionPressData[port][static_cast(action)].pressedPrevFrame; +} + +bool getActionBindHoldAnyPort(ActionBinds action) { + for (u32 port = 0; port < PAD_CHANMAX; ++port) { + if (getActionBindHold(action, port)) { + return true; + } + } + return false; +} + +int getActionBindButton(ActionBinds action, u32 port) { + return (*getActionBinds()[action].configVars)[port]; +} +} diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp index 46a689e287..de4cc227a3 100644 --- a/src/dusk/config.cpp +++ b/src/dusk/config.cpp @@ -11,6 +11,7 @@ #include #include "dusk/main.h" +#include "dusk/action_bindings.h" using namespace dusk::config; @@ -256,6 +257,13 @@ void dusk::config::Save() { io::FileStream::WriteAllText(reinterpret_cast(configJsonPath.c_str()), j.dump(4)); } +void dusk::config::ClearAllActionBindings(int port) { + for (auto& actionBinding : getActionBinds() | std::views::values) { + actionBinding.configVars->at(port).setValue(PAD_NATIVE_BUTTON_INVALID); + } + Save(); +} + ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) { const auto configVar = RegisteredConfigVars.find(name); if (configVar != RegisteredConfigVars.end()) { diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 7d1cb98524..9ba323eaad 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -13,6 +13,7 @@ #include "ImGuiEngine.hpp" #include "JSystem/JUtility/JUTGamePad.h" #include "SDL3/SDL_mouse.h" +#include "dusk/action_bindings.h" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/config.hpp" #include "dusk/data.hpp" @@ -239,7 +240,8 @@ namespace dusk { } void ImGuiConsole::UpdateSettings() { - getTransientSettings().skipFrameRateLimit = getSettings().game.enableTurboKeybind && ImGui::IsKeyDown(ImGuiKey_Tab); + getTransientSettings().skipFrameRateLimit = getSettings().game.enableTurboKeybind && + (ImGui::IsKeyDown(ImGuiKey_Tab) || getActionBindHoldAnyPort(ActionBinds::TURBO_SPEED_BUTTON)); if (dusk::frame_interp::get_ui_tick_pending() && mDoMain::developmentMode == 1 && (mDoCPd_c::getHold(PAD_1) & (PAD_TRIGGER_R | PAD_TRIGGER_L)) == (PAD_TRIGGER_R | PAD_TRIGGER_L) && mDoCPd_c::getTrigY(PAD_1)) { getTransientSettings().moveLinkActive = !getTransientSettings().moveLinkActive; diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 7475e5d48a..80f64a19d4 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -133,6 +133,34 @@ UserSettings g_userSettings = { .checkForUpdates {"backend.checkForUpdates", true}, .cardFileType {"backend.cardFileType", static_cast(CARD_GCIFOLDER)}, .enableAdvancedSettings {"backend.enableAdvancedSettings", false}, + }, + + // Not sure if there's a better way to declare this + .actionBindings = { + .firstPersonCamera { + ActionBindConfigVar{"actionBindings.firstPersonCamera_port0", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.firstPersonCamera_port1", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.firstPersonCamera_port2", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.firstPersonCamera_port3", PAD_NATIVE_BUTTON_INVALID}, + }, + .callMidna { + ActionBindConfigVar{"actionBindings.callMidna_port0", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.callMidna_port1", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.callMidna_port2", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.callMidna_port3", PAD_NATIVE_BUTTON_INVALID}, + }, + .openDusklightMenu { + ActionBindConfigVar{"actionBindings.openDusklightMenu_port0", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.openDusklightMenu_port1", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.openDusklightMenu_port2", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.openDusklightMenu_port3", PAD_NATIVE_BUTTON_INVALID}, + }, + .turboSpeedButton { + ActionBindConfigVar{"actionBindings.turboButton_port0", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.turboButton_port1", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.turboButton_port2", PAD_NATIVE_BUTTON_INVALID}, + ActionBindConfigVar{"actionBindings.turboButton_port3", PAD_NATIVE_BUTTON_INVALID}, + }, } }; @@ -248,6 +276,23 @@ void registerSettings() { Register(g_userSettings.backend.checkForUpdates); Register(g_userSettings.backend.cardFileType); Register(g_userSettings.backend.enableAdvancedSettings); + + Register(g_userSettings.actionBindings.firstPersonCamera[0]); + Register(g_userSettings.actionBindings.firstPersonCamera[1]); + Register(g_userSettings.actionBindings.firstPersonCamera[2]); + Register(g_userSettings.actionBindings.firstPersonCamera[3]); + Register(g_userSettings.actionBindings.callMidna[0]); + Register(g_userSettings.actionBindings.callMidna[1]); + Register(g_userSettings.actionBindings.callMidna[2]); + Register(g_userSettings.actionBindings.callMidna[3]); + Register(g_userSettings.actionBindings.openDusklightMenu[0]); + Register(g_userSettings.actionBindings.openDusklightMenu[1]); + Register(g_userSettings.actionBindings.openDusklightMenu[2]); + Register(g_userSettings.actionBindings.openDusklightMenu[3]); + Register(g_userSettings.actionBindings.turboSpeedButton[0]); + Register(g_userSettings.actionBindings.turboSpeedButton[1]); + Register(g_userSettings.actionBindings.turboSpeedButton[2]); + Register(g_userSettings.actionBindings.turboSpeedButton[3]); } // Transient settings diff --git a/src/dusk/ui/controller_config.cpp b/src/dusk/ui/controller_config.cpp index 2c1d815ceb..aef911f996 100644 --- a/src/dusk/ui/controller_config.cpp +++ b/src/dusk/ui/controller_config.cpp @@ -15,6 +15,9 @@ #include #include +#include "dusk/action_bindings.h" +#include "dusk/config.hpp" + namespace dusk::ui { namespace { @@ -108,68 +111,6 @@ const std::vector kGamepadButtonNames = { }; // clang-format on -Rml::String native_button_name(SDL_Gamepad* gamepad, u32 buttonUntyped) { - if (buttonUntyped == PAD_NATIVE_BUTTON_INVALID) { - return "Not bound"; - } - - auto button = static_cast(buttonUntyped); - if (gamepad != nullptr) { - switch (SDL_GetGamepadButtonLabel(gamepad, button)) { - case SDL_GAMEPAD_BUTTON_LABEL_A: - return "A"; - case SDL_GAMEPAD_BUTTON_LABEL_B: - return "B"; - case SDL_GAMEPAD_BUTTON_LABEL_X: - return "X"; - case SDL_GAMEPAD_BUTTON_LABEL_Y: - return "Y"; - case SDL_GAMEPAD_BUTTON_LABEL_CROSS: - return "Cross"; - case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE: - return "Circle"; - case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE: - return "Triangle"; - case SDL_GAMEPAD_BUTTON_LABEL_SQUARE: - return "Square"; - default: - break; - } - } - - const SDL_GamepadType type = - gamepad != nullptr ? SDL_GetGamepadType(gamepad) : SDL_GAMEPAD_TYPE_UNKNOWN; - for (const auto& buttonNames : kGamepadButtonNames) { - if (buttonNames.button != button) { - continue; - } - - for (const auto& name : buttonNames.names) { - if (name.type == type) { - return name.name; - } - } - } - - switch (button) { - case SDL_GAMEPAD_BUTTON_DPAD_LEFT: - return "D-pad left"; - case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: - return "D-pad right"; - case SDL_GAMEPAD_BUTTON_DPAD_UP: - return "D-pad up"; - case SDL_GAMEPAD_BUTTON_DPAD_DOWN: - return "D-pad down"; - default: - break; - } - - if (const char* name = PADGetNativeButtonName(buttonUntyped)) { - return name; - } - return "Unknown"; -} - Rml::String native_axis_name(const PADAxisMapping& mapping, SDL_Gamepad* gamepad) { if (mapping.nativeAxis.nativeAxis != -1) { Rml::String value = PADGetNativeAxisName(mapping.nativeAxis); @@ -367,6 +308,7 @@ void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) { addPageButton(Page::Triggers, "Triggers", [] { return Rml::String(">"); }, [] { return false; }); addPageButton(Page::Sticks, "Sticks", [] { return Rml::String(">"); }, [] { return false; }); addPageButton(Page::Rumble, "Rumble", [] { return Rml::String(">"); }, [port] { return !PADSupportsRumbleIntensity(static_cast(port)); }); + addPageButton(Page::Actions, "Custom Action Bindings", [] {return Rml::String(">"); }, [] { return false; }); leftPane.add_section("Options"); leftPane.register_control(leftPane.add_child(BoolButton::Props{ @@ -428,6 +370,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { PADClearPort(port); PADSetKeyboardActive(static_cast(port), FALSE); PADSerializeMappings(); + ClearAllActionBindings(port); }); pane.add_button({ @@ -440,6 +383,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { PADClearPort(port); PADSetKeyboardActive(static_cast(port), TRUE); PADSerializeMappings(); + ClearAllActionBindings(port); }); const u32 controllerCount = PADCount(); @@ -461,6 +405,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { PADSetKeyboardActive(static_cast(port), FALSE); PADSetPortForIndex(i, port); PADSerializeMappings(); + ClearAllActionBindings(port); }); } break; @@ -946,6 +891,77 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { pane.add_text("Configure your desired rumble intensities, then run a test to check how they feel."); break; } + case Page::Actions: { + if (keyboard_active(port)) { + auto addActionBinding = [&](auto actionBind, const std::string& key) { + pane.add_select_button( + { + .key = key, + .getValue = + [this, actionBind] { + if (mPendingActionBinding == actionBind) { + return pending_key_label(); + } + + return keyboard_key_name(actionBind->getValue()); + }, + }) + .on_pressed([this, port, actionBind] { + cancel_pending_binding(); + mPendingPort = port; + mPendingBindingArmed = false; + mPendingActionBinding = actionBind; + }); + }; + + pane.add_section("Custom Action Bindings"); + pane.add_text("A key bound to any action here will REPLACE the default control for" + " that action. Only bind buttons here that aren't used anywhere else."); + for (auto& [configVars, actionName] : getActionBinds() | std::views::values) { + addActionBinding(&configVars->at(port), actionName); + } + break; + } + + u32 buttonCount = 0; + PADButtonMapping* mappings = PADGetButtonMappings(port, &buttonCount); + if (mappings == nullptr) { + pane.add_text("No controller selected"); + break; + } + + SDL_Gamepad* gamepad = gamepad_for_port(port); + pane.add_section("Custom Action Bindings"); + pane.add_text("A button bound to any action here will REPLACE the default control for" + " that action. Only bind buttons here that aren't used anywhere else. The glyphs" + " shown for in game actions will not change. This is not recommended for " + " regular Gamecube controllers."); + auto addActionBinding = [&](auto actionBind, const std::string& key) { + pane.add_select_button({ + .key = key, + .getValue = + [this, gamepad, actionBind] { + if (mPendingActionBinding == actionBind) { + return pending_button_label(); + } + + return native_button_name( + gamepad, actionBind->getValue()); + }, + }) + .on_pressed([this, port, actionBind] { + cancel_pending_binding(); + mPendingPort = port; + mPendingBindingArmed = false; + mPendingActionBinding = actionBind; + }); + }; + + for (auto& [configVars, actionName] : getActionBinds() | std::views::values) { + addActionBinding(&configVars->at(port), actionName); + } + break; + } } } @@ -1020,12 +1036,31 @@ void ControllerConfigWindow::poll_pending_binding() { mPendingAxisMapping->nativeButton = nativeButton; finish_pending_binding(completedPort); } + return; + } + + if (mPendingActionBinding != nullptr) { + int button{}; + if (keyboard_active(mPendingPort)) { + button = keyboard_key_pressed(); + } else { + button = PADGetNativeButtonPressed(mPendingPort); + } + + if (button != -1) { + const int completedPort = mPendingPort; + mPendingActionBinding->setValue(button); + config::Save(); + finish_pending_binding(completedPort); + } + return; } } void ControllerConfigWindow::finish_pending_binding(int completedPort) { mPendingButtonMapping = nullptr; mPendingAxisMapping = nullptr; + mPendingActionBinding = nullptr; mPendingPort = -1; mPendingBindingArmed = false; mSuppressNavigationUntilNeutral = true; @@ -1035,7 +1070,7 @@ void ControllerConfigWindow::finish_pending_binding(int completedPort) { void ControllerConfigWindow::unmap_pending_binding() { if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr && - mPendingKeyButton < 0 && mPendingKeyAxis < 0) + mPendingActionBinding == nullptr && mPendingKeyButton < 0 && mPendingKeyAxis < 0) { return; } @@ -1048,6 +1083,9 @@ void ControllerConfigWindow::unmap_pending_binding() { mPendingAxisMapping->nativeAxis = {-1, AXIS_SIGN_POSITIVE}; mPendingAxisMapping->nativeButton = -1; finish_pending_binding(completedPort); + } else if (mPendingActionBinding != nullptr) { + mPendingActionBinding->setValue(PAD_NATIVE_BUTTON_INVALID); + finish_pending_binding(completedPort); } else if (mPendingKeyButton >= 0) { PADSetKeyButtonBinding(static_cast(completedPort), {PAD_KEY_INVALID, static_cast(mPendingKeyButton)}); @@ -1061,7 +1099,7 @@ void ControllerConfigWindow::unmap_pending_binding() { bool ControllerConfigWindow::capture_active() const { return mPendingButtonMapping != nullptr || mPendingAxisMapping != nullptr || - mPendingKeyButton >= 0 || mPendingKeyAxis >= 0; + mPendingActionBinding != nullptr || mPendingKeyButton >= 0 || mPendingKeyAxis >= 0; } bool ControllerConfigWindow::pending_input_neutral() const { @@ -1080,13 +1118,14 @@ Rml::String ControllerConfigWindow::pending_axis_label() const { } void ControllerConfigWindow::cancel_pending_binding() { - if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr && + if (mPendingButtonMapping == nullptr && mPendingAxisMapping == nullptr && mPendingActionBinding == nullptr && !mSuppressNavigationUntilNeutral && mPendingKeyButton < 0 && mPendingKeyAxis < 0) { return; } mPendingButtonMapping = nullptr; mPendingAxisMapping = nullptr; + mPendingActionBinding = nullptr; mPendingKeyButton = -1; mPendingKeyAxis = -1; mPendingPort = -1; @@ -1118,4 +1157,66 @@ void ControllerConfigWindow::stop_rumble_test() { mRumbleTestPort = -1; } +Rml::String native_button_name(SDL_Gamepad* gamepad, u32 buttonUntyped) { + if (buttonUntyped == PAD_NATIVE_BUTTON_INVALID) { + return "Not bound"; + } + + auto button = static_cast(buttonUntyped); + if (gamepad != nullptr) { + switch (SDL_GetGamepadButtonLabel(gamepad, button)) { + case SDL_GAMEPAD_BUTTON_LABEL_A: + return "A"; + case SDL_GAMEPAD_BUTTON_LABEL_B: + return "B"; + case SDL_GAMEPAD_BUTTON_LABEL_X: + return "X"; + case SDL_GAMEPAD_BUTTON_LABEL_Y: + return "Y"; + case SDL_GAMEPAD_BUTTON_LABEL_CROSS: + return "Cross"; + case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE: + return "Circle"; + case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE: + return "Triangle"; + case SDL_GAMEPAD_BUTTON_LABEL_SQUARE: + return "Square"; + default: + break; + } + } + + const SDL_GamepadType type = + gamepad != nullptr ? SDL_GetGamepadType(gamepad) : SDL_GAMEPAD_TYPE_UNKNOWN; + for (const auto& buttonNames : kGamepadButtonNames) { + if (buttonNames.button != button) { + continue; + } + + for (const auto& name : buttonNames.names) { + if (name.type == type) { + return name.name; + } + } + } + + switch (button) { + case SDL_GAMEPAD_BUTTON_DPAD_LEFT: + return "D-pad left"; + case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: + return "D-pad right"; + case SDL_GAMEPAD_BUTTON_DPAD_UP: + return "D-pad up"; + case SDL_GAMEPAD_BUTTON_DPAD_DOWN: + return "D-pad down"; + default: + break; + } + + if (const char* name = PADGetNativeButtonName(buttonUntyped)) { + return name; + } + return "Unknown"; +} + } // namespace dusk::ui diff --git a/src/dusk/ui/controller_config.hpp b/src/dusk/ui/controller_config.hpp index df8533a492..b5a50ad8b1 100644 --- a/src/dusk/ui/controller_config.hpp +++ b/src/dusk/ui/controller_config.hpp @@ -1,6 +1,7 @@ #pragma once #include "window.hpp" +#include "dusk/config_var.hpp" #include @@ -20,6 +21,7 @@ private: Triggers, Sticks, Rumble, + Actions, }; void build_port_tab(Rml::Element* content, int port); @@ -50,6 +52,9 @@ private: int mPendingKeyAxis = -1; bool mRumbleTestActive = false; int mRumbleTestPort = -1; + ActionBindConfigVar* mPendingActionBinding = nullptr; }; +Rml::String native_button_name(SDL_Gamepad* gamepad, u32 buttonUntyped); + } // namespace dusk::ui diff --git a/src/dusk/ui/input.cpp b/src/dusk/ui/input.cpp index cdaa2360c0..02a15e9a0f 100644 --- a/src/dusk/ui/input.cpp +++ b/src/dusk/ui/input.cpp @@ -12,6 +12,8 @@ #include #include +#include "dusk/action_bindings.h" + namespace dusk::ui::input { namespace { @@ -203,6 +205,9 @@ Rml::Input::KeyIdentifier map_raw_gamepad_button(SDL_GamepadButton button) noexc case SDL_GAMEPAD_BUTTON_SOUTH: return Rml::Input::KI_RETURN; case SDL_GAMEPAD_BUTTON_BACK: + if (isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, PAD_CHAN0)) { + return Rml::Input::KI_UNKNOWN; + } return Rml::Input::KI_F1; case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: return Rml::Input::KI_NEXT; @@ -216,6 +221,9 @@ Rml::Input::KeyIdentifier map_raw_gamepad_button(SDL_GamepadButton button) noexc Rml::Input::KeyIdentifier map_raw_button_alias(SDL_GamepadButton button) noexcept { switch (button) { case SDL_GAMEPAD_BUTTON_BACK: + if (isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, PAD_CHAN0)) { + return Rml::Input::KI_UNKNOWN; + } return Rml::Input::KI_F1; case SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER: return Rml::Input::KI_NEXT; @@ -318,12 +326,20 @@ bool find_event_pad_button( Rml::Input::KeyIdentifier map_gamepad_button(const SDL_GamepadButtonEvent& event) noexcept { const auto nativeButton = static_cast(event.button); - if (nativeButton == SDL_GAMEPAD_BUTTON_BACK) { + u32 port = 0; + bool foundEventPort = find_event_port(event.which, port); + if (foundEventPort) { + int openMenuButton = getActionBindButton(ActionBinds::OPEN_DUSKLIGHT_MENU, port); + if (openMenuButton != PAD_NATIVE_BUTTON_INVALID && openMenuButton == nativeButton) { + return Rml::Input::KI_F1; + } + } + + if (nativeButton == SDL_GAMEPAD_BUTTON_BACK && !isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, port)) { return Rml::Input::KI_F1; } - u32 port = 0; - if (!find_event_port(event.which, port)) { + if (!foundEventPort) { return map_raw_gamepad_button(nativeButton); } @@ -631,7 +647,7 @@ void process_axis_direction( if (chorded) { consume_menu_chord(port, context); } - const auto key = chorded ? Rml::Input::KI_F1 : map_gamepad_axis(event, sign); + const auto key = chorded && !isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, port) ? Rml::Input::KI_F1 : map_gamepad_axis(event, sign); if (key == Rml::Input::KI_UNKNOWN) { return; } @@ -719,7 +735,7 @@ void handle_event(const SDL_Event& event) noexcept { if (chorded) { consume_menu_chord(port, *context); } - const auto key = chorded ? Rml::Input::KI_F1 : map_gamepad_button(event.gbutton); + const auto key = chorded && !isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, port) ? Rml::Input::KI_F1 : map_gamepad_button(event.gbutton); if (key != Rml::Input::KI_UNKNOWN) { bool deferred = false; if (repeat != nullptr) { diff --git a/src/dusk/ui/overlay.cpp b/src/dusk/ui/overlay.cpp index 9a3ef78021..5bcd21c401 100644 --- a/src/dusk/ui/overlay.cpp +++ b/src/dusk/ui/overlay.cpp @@ -2,6 +2,8 @@ #include "aurora/lib/logging.hpp" #include "dusk/achievements.h" +#include "dusk/action_bindings.h" +#include "controller_config.hpp" #include "dusk/livesplit.h" #include "dusk/speedrun.h" #include "fmt/format.h" @@ -152,12 +154,22 @@ Rml::Element* create_menu_notification(Rml::Element* parent) { auto* elem = append(parent, "toast"); elem->SetClass("menu-notification", true); + // Get name of button for action binding if the action is bound + Rml::String padButton{}; + SDL_Gamepad* gamepad = gamepad_for_port(PAD_CHAN0); + if (isActionBound(ActionBinds::OPEN_DUSKLIGHT_MENU, PAD_CHAN0) && gamepad != nullptr) { + padButton = native_button_name(gamepad, + getActionBindButton(ActionBinds::OPEN_DUSKLIGHT_MENU, PAD_CHAN0)); + } else { + padButton = back_button_name(); + } + auto* message = append(elem, "message"); auto* row = append(message, "row"); append(row, "span")->SetInnerRML(kMenuNotificationPrefix); auto* icon = append(row, "icon"); icon->SetClass("controller", true); - append(row, "span")->SetInnerRML(escape(back_button_name())); + append(row, "span")->SetInnerRML(escape(padButton)); append(row, "span")->SetInnerRML("to open menu"); return elem; From 8c001f796801d7803d72cacb052d09c038caab2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFs?= <49660929+SailorSnoW@users.noreply.github.com> Date: Wed, 13 May 2026 03:37:31 +0200 Subject: [PATCH 10/12] Add Nix devshell for Linux and macOS development (#1044) - Restructure flake to expose `devShells..default` across x86_64-linux, aarch64-linux, x86_64-darwin, and aarch64-darwin via `nixpkgs.lib.genAttrs`. The existing `packages.x86_64-linux.default` build is preserved (still tied to the linux-x86_64 dawn/nod prebuilts). - Linux devshell (`mkShell`): gcc + clang/lld, cmake, ninja, pkg-config, python3 + markupsafe, rustc/cargo, sccache, plus the system libs mirrored from the Ubuntu apt list in .github/workflows/build.yml (X11/Wayland, Vulkan, GL, ALSA/PulseAudio/PipeWire, GTK3, freetype, zstd, ...). - macOS devshell (`mkShellNoCC`): cmake, ninja, python3 + markupsafe, rustc/cargo, sccache. No cc-wrapper so CMake picks up Apple Clang and the Xcode SDK directly, matching the build-apple CI job. - Ignore `.direnv/` and `.envrc` so local direnv state stays out of git. --- .gitignore | 4 + flake.nix | 299 ++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 209 insertions(+), 94 deletions(-) diff --git a/.gitignore b/.gitignore index a1839ff34e..43b2e4a1c3 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,10 @@ compile_commands.json # MacOS .DS_Store +# direnv / nix +.direnv/ +.envrc + # ISOs *.iso diff --git a/flake.nix b/flake.nix index 17467476fb..d9d007a8bd 100644 --- a/flake.nix +++ b/flake.nix @@ -4,101 +4,212 @@ }; outputs = { self, nixpkgs }: let - pkgs = import nixpkgs { system = "x86_64-linux"; }; - - # Dependencies that are not packaged in nixpkgs: - aurora-src = pkgs.fetchFromGitHub { - owner = "encounter"; - repo = "aurora"; - rev = "63606a43265a3bc18dafd500ab4d7a2108f109e6"; - hash = "sha256-xBvnAwGwNzav67Ac6oUz7RqDUwqgL2bsME3OOMn8Tqw="; - }; - dawn-src = pkgs.fetchzip { - url = "https://github.com/encounter/dawn-build/releases/download/v20260423.175430/dawn-linux-x86_64.tar.gz"; - hash = "sha256-HXfKTLHtMPwupnFnaflCARtXVPuS/0PoCePXidjE5xs="; - stripRoot = false; - }; - nod-src = pkgs.fetchzip { - url = "https://github.com/encounter/nod/releases/download/v2.0.0-alpha.8/libnod-linux-x86_64.tar.gz"; - hash = "sha256-mUqvLsbsqaZ+HAjMmHYPYO+MgtanGRTw7Gzn5uXR5rE="; - stripRoot = false; - }; - # The version of imgui on nixpkgs does not map cleanly. - imgui-src = pkgs.fetchFromGitHub { - owner = "ocornut"; - repo = "imgui"; - rev = "v1.91.9b-docking"; - hash = "sha256-mQOJ6jCN+7VopgZ61yzaCnt4R1QLrW7+47xxMhFRHLQ="; - }; - sqlite-src = pkgs.fetchzip { - url = "https://sqlite.org/2026/sqlite-amalgamation-3510300.zip"; - hash = "sha256-pNMR8zxaaqfAzQ0AQBOXMct4usdjey1Q0Gnitg06UhM="; - }; - rmlui-src = pkgs.fetchzip { - url = "https://github.com/mikke89/RmlUi/archive/f9b8c9e2935d5df2c7dff2c190d3968e99b0c3dc.tar.gz"; - hash = "sha256-g4O/JZUrrcseOz8o2QJRt+2CeuiLnVeuDJc906xvuIg="; - }; - # Dusklight Actual - dusklight = pkgs.stdenv.mkDerivation { - name = "dusklight"; - src = ./.; - postUnpack = '' - mkdir -p $sourceRoot/extern/aurora - cp -r ${aurora-src}/. $sourceRoot/extern/aurora/ - chmod -R u+w $sourceRoot/extern/aurora - sed -i '/add_subdirectory(tests)/d' $sourceRoot/extern/aurora/CMakeLists.txt - ''; - # Remove last line to re-enable tests - cmakeFlags = [ - "-DFETCHCONTENT_FULLY_DISCONNECTED=ON" - "-DFETCHCONTENT_SOURCE_DIR_CXXOPTS=${pkgs.cxxopts.src}" - "-DFETCHCONTENT_SOURCE_DIR_JSON=${pkgs.nlohmann_json.src}" - "-DFETCHCONTENT_SOURCE_DIR_DAWN_PREBUILT=${dawn-src}" - "-DFETCHCONTENT_SOURCE_DIR_XXHASH=${pkgs.xxHash.src}" - "-DFETCHCONTENT_SOURCE_DIR_FMT=${pkgs.fmt.src}" - "-DFETCHCONTENT_SOURCE_DIR_TRACY=${pkgs.tracy.src}" - "-DAURORA_SDL3_PROVIDER=system" - "-DFETCHCONTENT_SOURCE_DIR_NOD_PREBUILT=${nod-src}" - "-DAURORA_NOD_PROVIDER=package" - "-DFETCHCONTENT_SOURCE_DIR_FREETYPE=${pkgs.freetype.src}" - "-DFETCHCONTENT_SOURCE_DIR_ZSTD=${pkgs.zstd.src}" - "-DFETCHCONTENT_SOURCE_DIR_SQLITE3=${sqlite-src}" - "-DFETCHCONTENT_SOURCE_DIR_IMGUI=${imgui-src}" - "-DFETCHCONTENT_SOURCE_DIR_RMLUI=${rmlui-src}" - "-DCMAKE_CROSSCOMPILING=ON" # Tests are not working as I didn't want to work through getting google's test suite working as well. This is the only guard I could find to disable it. - ]; - installPhase = '' - mkdir -p $out/bin - cp dusklight $out/bin/dusklight - cp -r ./res $out/bin/res - ''; - nativeBuildInputs = [ - pkgs.cmake - pkgs.pkg-config - pkgs.wayland - ]; - buildInputs = [ - pkgs.libGL - pkgs.libX11 - pkgs.libXcursor - pkgs.libxi - pkgs.libxcb - pkgs.libxrandr - pkgs.libxscrnsaver - pkgs.libxtst - pkgs.libjpeg8 - pkgs.libxkbcommon - pkgs.libglvnd - pkgs.cxxopts - pkgs.abseil-cpp - pkgs.sdl3 - pkgs.fmt - pkgs.tracy - pkgs.freetype - pkgs.zstd - ]; + supportedSystems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + forAllSystems = nixpkgs.lib.genAttrs supportedSystems; + pkgsFor = system: import nixpkgs { inherit system; }; + + # Dependencies that are not packaged in nixpkgs (used by the Linux package build): + buildSources = pkgs: { + aurora-src = pkgs.fetchFromGitHub { + owner = "encounter"; + repo = "aurora"; + rev = "63606a43265a3bc18dafd500ab4d7a2108f109e6"; + hash = "sha256-xBvnAwGwNzav67Ac6oUz7RqDUwqgL2bsME3OOMn8Tqw="; + }; + dawn-src = pkgs.fetchzip { + url = "https://github.com/encounter/dawn-build/releases/download/v20260423.175430/dawn-linux-x86_64.tar.gz"; + hash = "sha256-HXfKTLHtMPwupnFnaflCARtXVPuS/0PoCePXidjE5xs="; + stripRoot = false; + }; + nod-src = pkgs.fetchzip { + url = "https://github.com/encounter/nod/releases/download/v2.0.0-alpha.8/libnod-linux-x86_64.tar.gz"; + hash = "sha256-mUqvLsbsqaZ+HAjMmHYPYO+MgtanGRTw7Gzn5uXR5rE="; + stripRoot = false; + }; + # The version of imgui on nixpkgs does not map cleanly. + imgui-src = pkgs.fetchFromGitHub { + owner = "ocornut"; + repo = "imgui"; + rev = "v1.91.9b-docking"; + hash = "sha256-mQOJ6jCN+7VopgZ61yzaCnt4R1QLrW7+47xxMhFRHLQ="; + }; + sqlite-src = pkgs.fetchzip { + url = "https://sqlite.org/2026/sqlite-amalgamation-3510300.zip"; + hash = "sha256-pNMR8zxaaqfAzQ0AQBOXMct4usdjey1Q0Gnitg06UhM="; + }; + rmlui-src = pkgs.fetchzip { + url = "https://github.com/mikke89/RmlUi/archive/f9b8c9e2935d5df2c7dff2c190d3968e99b0c3dc.tar.gz"; + hash = "sha256-g4O/JZUrrcseOz8o2QJRt+2CeuiLnVeuDJc906xvuIg="; + }; }; + + # Dusklight Actual (Linux x86_64 only — relies on prebuilt dawn/nod binaries) + mkDusklight = pkgs: + let srcs = buildSources pkgs; in + pkgs.stdenv.mkDerivation { + name = "dusklight"; + src = ./.; + postUnpack = '' + mkdir -p $sourceRoot/extern/aurora + cp -r ${srcs.aurora-src}/. $sourceRoot/extern/aurora/ + chmod -R u+w $sourceRoot/extern/aurora + sed -i '/add_subdirectory(tests)/d' $sourceRoot/extern/aurora/CMakeLists.txt + ''; + # Remove last line to re-enable tests + cmakeFlags = [ + "-DFETCHCONTENT_FULLY_DISCONNECTED=ON" + "-DFETCHCONTENT_SOURCE_DIR_CXXOPTS=${pkgs.cxxopts.src}" + "-DFETCHCONTENT_SOURCE_DIR_JSON=${pkgs.nlohmann_json.src}" + "-DFETCHCONTENT_SOURCE_DIR_DAWN_PREBUILT=${srcs.dawn-src}" + "-DFETCHCONTENT_SOURCE_DIR_XXHASH=${pkgs.xxHash.src}" + "-DFETCHCONTENT_SOURCE_DIR_FMT=${pkgs.fmt.src}" + "-DFETCHCONTENT_SOURCE_DIR_TRACY=${pkgs.tracy.src}" + "-DAURORA_SDL3_PROVIDER=system" + "-DFETCHCONTENT_SOURCE_DIR_NOD_PREBUILT=${srcs.nod-src}" + "-DAURORA_NOD_PROVIDER=package" + "-DFETCHCONTENT_SOURCE_DIR_FREETYPE=${pkgs.freetype.src}" + "-DFETCHCONTENT_SOURCE_DIR_ZSTD=${pkgs.zstd.src}" + "-DFETCHCONTENT_SOURCE_DIR_SQLITE3=${srcs.sqlite-src}" + "-DFETCHCONTENT_SOURCE_DIR_IMGUI=${srcs.imgui-src}" + "-DFETCHCONTENT_SOURCE_DIR_RMLUI=${srcs.rmlui-src}" + "-DCMAKE_CROSSCOMPILING=ON" # Tests are not working as I didn't want to work through getting google's test suite working as well. This is the only guard I could find to disable it. + ]; + installPhase = '' + mkdir -p $out/bin + cp dusklight $out/bin/dusklight + cp -r ./res $out/bin/res + ''; + nativeBuildInputs = [ + pkgs.cmake + pkgs.pkg-config + pkgs.wayland + ]; + buildInputs = [ + pkgs.libGL + pkgs.libX11 + pkgs.libXcursor + pkgs.libxi + pkgs.libxcb + pkgs.libxrandr + pkgs.libxscrnsaver + pkgs.libxtst + pkgs.libjpeg8 + pkgs.libxkbcommon + pkgs.libglvnd + pkgs.cxxopts + pkgs.abseil-cpp + pkgs.sdl3 + pkgs.fmt + pkgs.tracy + pkgs.freetype + pkgs.zstd + ]; + }; + + # Tooling common to every supported host (Linux and macOS). + commonDevTools = pkgs: [ + pkgs.cmake + pkgs.ninja + pkgs.pkg-config + pkgs.git + pkgs.python3 + pkgs.python3Packages.markupsafe + pkgs.rustc + pkgs.cargo + pkgs.sccache + ]; + + # Linux-only system libraries — mirrors the apt deps from .github/workflows/build.yml + # so the cmake presets resolve the same set of headers as CI. + linuxDevDeps = pkgs: [ + # Compilers / linkers + pkgs.clang + pkgs.lld + # C/C++ utilities + pkgs.curl + pkgs.openssl + pkgs.zlib + pkgs.libpng + pkgs.libjpeg_turbo + pkgs.freetype + pkgs.zstd + pkgs.fmt + pkgs.tracy + pkgs.cxxopts + pkgs.abseil-cpp + pkgs.sdl3 + pkgs.ncurses + pkgs.libunwind + pkgs.libusb1 + pkgs.fuse + # Wayland / display server + pkgs.wayland + pkgs.wayland-protocols + pkgs.libxkbcommon + pkgs.libdecor + # OpenGL / Vulkan + pkgs.libGL + pkgs.libGLU + pkgs.libglvnd + pkgs.vulkan-headers + pkgs.vulkan-loader + # X11 + pkgs.libX11 + pkgs.libxcb + pkgs.libXcursor + pkgs.libxi + pkgs.libxrandr + pkgs.libxscrnsaver + pkgs.libxtst + pkgs.libxinerama + # Audio + pkgs.alsa-lib + pkgs.libpulseaudio + pkgs.pipewire + # System integration + pkgs.dbus + pkgs.udev + pkgs.gtk3 + ]; + + # On macOS we deliberately avoid pulling Nix's cc-wrapper so CMake picks up + # Apple Clang and the Xcode SDK directly, matching the macOS CI workflow. + mkDarwinShell = pkgs: + pkgs.mkShellNoCC { + packages = commonDevTools pkgs; + shellHook = '' + echo "Dusklight dev shell (macOS)" + echo "Requires Xcode Command Line Tools for Apple Clang and the macOS SDK." + echo "Configure: cmake --preset macos-default-relwithdebinfo" + echo "Build: cmake --build --preset macos-default-relwithdebinfo" + ''; + }; + + mkLinuxShell = pkgs: + pkgs.mkShell { + packages = (commonDevTools pkgs) ++ (linuxDevDeps pkgs); + shellHook = '' + echo "Dusklight dev shell (Linux)" + echo "Configure: cmake --preset linux-default-relwithdebinfo" + echo " cmake --preset linux-clang-relwithdebinfo" + echo "Build: cmake --build --preset " + ''; + }; + + mkDevShell = pkgs: + if pkgs.stdenv.isDarwin + then mkDarwinShell pkgs + else mkLinuxShell pkgs; in { - packages.x86_64-linux.default = dusklight; + packages.x86_64-linux.default = mkDusklight (pkgsFor "x86_64-linux"); + + devShells = forAllSystems (system: { + default = mkDevShell (pkgsFor system); + }); }; } From 0c78376ba8c9e7104bbf04c363482643ae64560f Mon Sep 17 00:00:00 2001 From: SailorSnoW Date: Wed, 13 May 2026 03:39:47 +0200 Subject: [PATCH 11/12] Fix LJA achievement triggering from cutscene teleport --- src/dusk/achievements.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp index 28e66c891e..def7cd97ad 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -692,6 +692,12 @@ std::vector AchievementSystem::makeEntries() { return; } + // prevent stuff like https://github.com/TwilitRealm/dusklight/issues/949 + if (link->getDemoMode() != 0) { + inJump = false; + return; + } + if (!inJump) { if (link->mProcID == daAlink_c::PROC_CUT_JUMP) { inJump = true; From 861efaa05382ce4c72b4402925112367d074f545 Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 12 May 2026 20:32:58 -0600 Subject: [PATCH 12/12] Update aurora --- extern/aurora | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extern/aurora b/extern/aurora index 974d11dfe7..f7f3d52cfc 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit 974d11dfe727f213e34243f7c61fbcf0ef437968 +Subproject commit f7f3d52cfcc3cb43bb9445582f69798b3a622d4a