diff --git a/.gitignore b/.gitignore index e3cbb3930f..2d1ff6d3a9 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,10 @@ compile_commands.json # MacOS .DS_Store +# direnv / nix +.direnv/ +.envrc + # ISOs *.iso diff --git a/CMakeLists.txt b/CMakeLists.txt index c51bfb24f1..e04853cc82 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,8 +5,19 @@ if (NOT CMAKE_BUILD_TYPE) "Build type options: Debug Release RelWithDebInfo MinSizeRel" FORCE) endif () -# obtain revision info from git -find_package(Git) +set(DUSK_VERSION_OVERRIDE "" CACHE STRING "Override version string (skips git detection and format validation)") + +if (DUSK_VERSION_OVERRIDE) + set(DUSK_WC_DESCRIBE "${DUSK_VERSION_OVERRIDE}") + set(DUSK_VERSION_STRING "0.0.0.0") + set(DUSK_SHORT_VERSION_STRING "0.0.0") + set(DUSK_WC_REVISION "") + set(DUSK_WC_BRANCH "") + set(DUSK_WC_DATE "") + message(STATUS "Dusklight version overridden to ${DUSK_WC_DESCRIBE}") +else () + # obtain revision info from git + find_package(Git) if (GIT_FOUND) # make sure version information gets re-run when the current Git HEAD changes execute_process(WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD @@ -63,6 +74,8 @@ else () set(DUSK_SHORT_VERSION_STRING "0.0.0") endif () +endif () + # Add version information to CI environment variables if(DEFINED ENV{GITHUB_ENV}) file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION=${DUSK_WC_DESCRIBE}\n") @@ -119,11 +132,6 @@ option(DUSK_BUILD_WARNINGS "Enable compiler warnings (off by default)") option(DUSK_SELECTED_OPT "If on, selected parts of the project will be compiled with optimizations on Debug, intending to make the game run at 30 FPS. Note for MSVC: you will need to remove '/RTC1' from your debug flags in CMake.") option(DUSK_MOVIE_SUPPORT "If on, compile against libjpeg-turbo to enable THP file decoding" ON) option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking support" ON) - -if(ANDROID) - set(DUSK_MOVIE_SUPPORT OFF) -endif () - option(DUSK_ENABLE_SENTRY_NATIVE "Enable sentry-native crash reporting support" OFF) set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN") set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment") @@ -162,6 +170,8 @@ if (DUSK_MOVIE_SUPPORT) list(APPEND _jpeg_cmake_args -DCMAKE_TOOLCHAIN_FILE=${_jpeg_toolchain_file}) endif () set(_jpeg_passthrough_vars + ANDROID_ABI + ANDROID_PLATFORM CMAKE_BUILD_TYPE CMAKE_C_COMPILER CMAKE_C_COMPILER_LAUNCHER diff --git a/CMakePresets.json b/CMakePresets.json index 64ef90d452..0201c61ed6 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -249,22 +249,11 @@ "type": "BOOL", "value": false }, - "CMAKE_DISABLE_FIND_PACKAGE_BZip2": { + "CMAKE_DISABLE_FIND_PACKAGE_PkgConfig": { "type": "BOOL", "value": true }, - "CMAKE_DISABLE_FIND_PACKAGE_LibLZMA": { - "type": "BOOL", - "value": true - }, - "CMAKE_DISABLE_FIND_PACKAGE_zstd": { - "type": "BOOL", - "value": true - }, - "CMAKE_DISABLE_FIND_PACKAGE_Freetype": { - "type": "BOOL", - "value": true - } + "CMAKE_IGNORE_PREFIX_PATH": "/opt/homebrew" }, "vendor": { "microsoft.com/VisualStudioSettings/CMake/1.0": { @@ -329,7 +318,11 @@ "cacheVariables": { "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install", "CMAKE_TOOLCHAIN_FILE": "$env{ANDROID_HOME}/ndk/$env{ANDROID_NDK_VERSION}/build/cmake/android.toolchain.cmake", - "ANDROID_PLATFORM": "android-28" + "ANDROID_PLATFORM": "android-28", + "BUILD_SHARED_LIBS": { + "type": "BOOL", + "value": false + } } }, { @@ -416,7 +409,7 @@ }, "CMAKE_OSX_DEPLOYMENT_TARGET": "11.0", "CMAKE_IGNORE_PREFIX_PATH": "/opt/homebrew", - "DUSK_MOVIE_SUPPORT": { + "BUILD_SHARED_LIBS": { "type": "BOOL", "value": false } @@ -449,11 +442,7 @@ ], "cacheVariables": { "CMAKE_C_COMPILER_LAUNCHER": "sccache", - "CMAKE_CXX_COMPILER_LAUNCHER": "sccache", - "DUSK_MOVIE_SUPPORT": { - "type": "BOOL", - "value": false - } + "CMAKE_CXX_COMPILER_LAUNCHER": "sccache" } }, { @@ -463,11 +452,7 @@ ], "cacheVariables": { "CMAKE_C_COMPILER_LAUNCHER": "sccache", - "CMAKE_CXX_COMPILER_LAUNCHER": "sccache", - "DUSK_MOVIE_SUPPORT": { - "type": "BOOL", - "value": false - } + "CMAKE_CXX_COMPILER_LAUNCHER": "sccache" } }, { diff --git a/extern/aurora b/extern/aurora index ff9ca7170c..3643a369ad 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit ff9ca7170cc840b1fbb5c7015b2fa1966055a04d +Subproject commit 3643a369ad47d8acca8d73dba462eab2ee2bd150 diff --git a/files.cmake b/files.cmake index 85df9fcf4f..17105b6f8d 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 @@ -1457,7 +1458,6 @@ set(DUSK_FILES src/dusk/imgui/ImGuiHeapOverlay.cpp src/dusk/imgui/ImGuiControllerOverlay.cpp src/dusk/imgui/ImGuiStubLog.cpp - src/dusk/imgui/ImGuiMapLoader.cpp src/dusk/imgui/ImGuiSaveEditor.cpp src/dusk/imgui/ImGuiStateShare.hpp src/dusk/imgui/ImGuiStateShare.cpp @@ -1510,6 +1510,8 @@ set(DUSK_FILES src/dusk/ui/tab_bar.hpp src/dusk/ui/ui.cpp src/dusk/ui/ui.hpp + src/dusk/ui/warp.cpp + src/dusk/ui/warp.hpp src/dusk/ui/window.cpp src/dusk/ui/window.hpp src/dusk/achievements.cpp @@ -1527,6 +1529,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/flake.nix b/flake.nix index 17467476fb..edba703a3d 100644 --- a/flake.nix +++ b/flake.nix @@ -4,101 +4,225 @@ }; 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; + versionSuffix = if self ? shortRev && self.shortRev != null + then "nix-${self.shortRev}" + else "nix-dirty"; + 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 = [ + "-DDUSK_VERSION_OVERRIDE=${versionSuffix}" + "-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 + + mkdir -p $out/share/applications + cp $src/platforms/freedesktop/dusklight.desktop $out/share/applications/dusklight.desktop + + for size in 16 32 48 64 128 256 512 1024; do + install -Dm644 $src/platforms/freedesktop/''${size}x''${size}/apps/dusklight.png \ + $out/share/icons/hicolor/''${size}x''${size}/apps/dusklight.png + done + ''; + 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); + }); }; -} +} \ No newline at end of file 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/audio/DuskAudioSystem.h b/include/dusk/audio/DuskAudioSystem.h index 8cdd757c82..780187f9d2 100644 --- a/include/dusk/audio/DuskAudioSystem.h +++ b/include/dusk/audio/DuskAudioSystem.h @@ -1,8 +1,19 @@ #pragma once +#include #include namespace dusk::audio { + + // Converts a 0-1 volume to a linear amplitude multiplier. + // The curve is -4 dB per 10% step: 100% = 0 dB, 90% = -4 dB, ..., 0% = -inf dB + inline f32 MasterVolumeToLinear(f32 v) { + if (v <= 0.0f) { + return 0.0f; + } + return std::pow(10.0f, (v - 1.0f) * 2.0f); + } + /** * Initialize the audio system and start playing audio. */ 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/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..0bae27bfd3 100644 --- a/include/dusk/config_var.hpp +++ b/include/dusk/config_var.hpp @@ -175,6 +175,7 @@ class ConfigVar : public ConfigVarBase { T defaultValue; T value; T overrideValue; + ConfigVarLayer priorLayer = ConfigVarLayer::Default; public: /** @@ -265,6 +266,7 @@ public: void setSpeedrunValue(T newValue) { checkRegistered(); if (layer != ConfigVarLayer::Override) { + priorLayer = layer; overrideValue = std::move(newValue); layer = ConfigVarLayer::Speedrun; } @@ -282,11 +284,24 @@ public: checkRegistered(); if (layer == ConfigVarLayer::Speedrun) { overrideValue = {}; - layer = ConfigVarLayer::Value; + layer = priorLayer; } } + + /** + * \brief Get the user-persisted value, ignoring any temporary overrides. + * + * Used by Save() to write the correct value even when a speedrun override is active. + */ + [[nodiscard]] constexpr const T& getValueForSave() const noexcept { + checkRegistered(); + const ConfigVarLayer effectiveLayer = (layer == ConfigVarLayer::Speedrun) ? priorLayer : layer; + return effectiveLayer == ConfigVarLayer::Default ? defaultValue : value; + } }; +using ActionBindConfigVar = ConfigVar; + } #endif // DUSK_CONFIG_VAR_HPP diff --git a/include/dusk/hotkeys.h b/include/dusk/hotkeys.h index c3dd354538..7a774bde80 100644 --- a/include/dusk/hotkeys.h +++ b/include/dusk/hotkeys.h @@ -14,7 +14,6 @@ constexpr const char* SHOW_DEBUG_OVERLAY = "F3"; constexpr const char* SHOW_HEAP_VIEWER = "F4"; constexpr const char* SHOW_PLAYER_INFO = "F5"; constexpr const char* SHOW_SAVE_EDITOR = "F6"; -constexpr const char* SHOW_MAP_LOADER = "F7"; constexpr const char* SHOW_STATE_SHARE = "F8"; constexpr const char* SHOW_DEBUG_CAMERA = "F9"; constexpr const char* SHOW_AUDIO_DEBUG = "F10"; diff --git a/include/dusk/main.h b/include/dusk/main.h index 240c132f40..0a2be8734d 100644 --- a/include/dusk/main.h +++ b/include/dusk/main.h @@ -10,6 +10,7 @@ extern bool IsShuttingDown; extern bool IsGameLaunched; extern bool RestartRequested; extern std::filesystem::path ConfigPath; +extern std::filesystem::path CachePath; #if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \ (defined(TARGET_OS_TV) && TARGET_OS_TV) diff --git a/include/dusk/settings.h b/include/dusk/settings.h index de78432c98..43e565ed5e 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 { @@ -115,6 +117,7 @@ struct UserSettings { ConfigVar enableLinkDollRotation; ConfigVar enableAchievementToasts; ConfigVar enableControllerToasts; + ConfigVar enableDiscordPresence; // Graphics ConfigVar bloomMode; @@ -155,6 +158,7 @@ struct UserSettings { // Cheats ConfigVar infiniteHearts; ConfigVar infiniteArrows; + ConfigVar infiniteSeeds; ConfigVar infiniteBombs; ConfigVar infiniteOil; ConfigVar infiniteOxygen; @@ -168,18 +172,22 @@ struct UserSettings { ConfigVar fastRoll; ConfigVar fastSpinner; ConfigVar freeMagicArmor; + ConfigVar invincibleEnemies; // Technical ConfigVar restoreWiiGlitches; // Controls ConfigVar enableTurboKeybind; + ConfigVar enableResetKeybind; // Tools ConfigVar speedrunMode; ConfigVar liveSplitEnabled; ConfigVar showSpeedrunRTATimer; ConfigVar recordingMode; + ConfigVar showInputViewer; + ConfigVar showInputViewerGyro; } game; struct { @@ -193,6 +201,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 abbda0c5e2..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() { @@ -144,8 +145,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; @@ -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/actor/d_a_alink_swindow.inc b/src/d/actor/d_a_alink_swindow.inc index 9016f14205..33f1fee37b 100644 --- a/src/d/actor/d_a_alink_swindow.inc +++ b/src/d/actor/d_a_alink_swindow.inc @@ -77,7 +77,12 @@ int daAlink_c::loadModelDVD() { mpWlMidnaHairModel = NULL; if (!checkNoResetFlg2(FLG2_UNK_280000)) { - dComIfG_resDelete(&mPhaseReq, mArcName); + if (!dComIfG_resDelete(&mPhaseReq, mArcName)) { +#if TARGET_PC + // resDelete no-ops if load was in-progress; force-unregister before freeAll + dComIfG_deleteObjectResMain(mArcName); +#endif + } cPhs_Reset(&mPhaseReq); mpArcHeap->freeAll(); diff --git a/src/d/actor/d_a_alink_wolf.inc b/src/d/actor/d_a_alink_wolf.inc index b2e60aea01..67fde3a7a7 100644 --- a/src/d/actor/d_a_alink_wolf.inc +++ b/src/d/actor/d_a_alink_wolf.inc @@ -8723,6 +8723,12 @@ int daAlink_c::procWolfCargoCarry() { return checkNextActionWolf(0); } +#if TARGET_PC + if (field_0x280c.getActor() == NULL) { + return checkNextActionWolf(0); + } +#endif + mDoMtx_stack_c::copy(((e_yc_class*)field_0x280c.getActor())->getLegR3Mtx()); mDoMtx_stack_c::transM(-9.0f, -7.0f, -30.0f); mDoMtx_stack_c::multVecZero(¤t.pos); 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_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/d/d_cc_uty.cpp b/src/d/d_cc_uty.cpp index ec9a177683..218cb362f4 100644 --- a/src/d/d_cc_uty.cpp +++ b/src/d/d_cc_uty.cpp @@ -15,6 +15,7 @@ #include "f_op/f_op_actor_mng.h" #if TARGET_PC #include "dusk/achievements.h" +#include "dusk/settings.h" #endif static int plCutLRC[58] = { @@ -429,6 +430,13 @@ fopAc_ac_c* cc_at_check(fopAc_ac_c* i_enemy, dCcU_AtInfo* i_AtInfo) { } } +#if TARGET_PC + if (dusk::getSettings().game.invincibleEnemies && + fopAcM_GetGroup(i_enemy) == fopAc_ENEMY_e) { + i_AtInfo->mAttackPower = 0; + } +#endif + if (i_AtInfo->mAttackPower != 0) { i_enemy->health -= i_AtInfo->mAttackPower; } diff --git a/src/d/d_ev_camera.cpp b/src/d/d_ev_camera.cpp index 442c8cc4af..736f9f7a82 100644 --- a/src/d/d_ev_camera.cpp +++ b/src/d/d_ev_camera.cpp @@ -3882,7 +3882,11 @@ bool dCamera_c::hintTalkEvCamera() { cSAngle acStack_1fc(20.0f); for (i = 0; i < 2; i++) { +#if AVOID_UB + for (j = 0; j < 10; j++) { +#else for (j = 0; j < 12; j++) { +#endif cSAngle acStack_200(local_b0[j] * fVar22); hintTalk->mDirection.U(acStack_1f8 + acStack_200); hintTalk->mDirection.V(((hintTalk->field_0x28.V() * acStack_200.Cos()) * 0.2f) + acStack_1fc); 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/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; } 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; 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/autosave.cpp b/src/dusk/autosave.cpp index 779cb19915..13fc2929eb 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; @@ -25,8 +26,12 @@ void updateAutoSave() { (AutoSaveFuncsProc[mAutoSaveProc])(); } -void writeAutoSave() { - int stageNo = dStage_stagInfo_GetSaveTbl(dComIfGp_getStageStagInfo()); +bool writeAutoSave() { + stage_stag_info_class* stagInfo = dComIfGp_getStageStagInfo(); + if (stagInfo == nullptr) { + return false; + } + int stageNo = dStage_stagInfo_GetSaveTbl(stagInfo); dComIfGs_putSave(stageNo); dComIfGs_setMemoryToCard(mSaveBuffer, dComIfGs_getDataNum()); @@ -39,6 +44,7 @@ void writeAutoSave() { } g_mDoMemCd_control.save(mSaveBuffer, sizeof(mSaveBuffer), 0); + return true; } void autoSaving() { @@ -47,8 +53,9 @@ void autoSaving() { if (cardState == 2) { mAutoSaveProc = 1; } else if (cardState == 1) { - writeAutoSave(); - mAutoSaveProc = 3; + if (writeAutoSave()) { + mAutoSaveProc = 3; + } } } } @@ -89,4 +96,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/config.cpp b/src/dusk/config.cpp index 46a689e287..f0a4b986bd 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; @@ -60,7 +61,7 @@ void ConfigImpl::loadFromJson(ConfigVar& cVar, const json& jsonValue) { template nlohmann::json ConfigImpl::dumpToJson(const ConfigVar& cVar) { - return cVar.getValue(); + return cVar.getValueForSave(); } template requires std::is_integral_v && std::is_signed_v @@ -248,7 +249,8 @@ void dusk::config::Save() { json j; for (const auto& pair : RegisteredConfigVars) { - if (pair.second->getLayer() == ConfigVarLayer::Value) { + const auto layer = pair.second->getLayer(); + if (layer == ConfigVarLayer::Value || layer == ConfigVarLayer::Speedrun) { j[pair.first] = pair.second->getImpl()->dumpToJson(*pair.second); } } @@ -256,6 +258,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/crash_reporting.cpp b/src/dusk/crash_reporting.cpp index 5c75388361..2c2a77079b 100644 --- a/src/dusk/crash_reporting.cpp +++ b/src/dusk/crash_reporting.cpp @@ -61,7 +61,7 @@ std::string release_name() { } std::filesystem::path sentry_database_path() { - return dusk::ConfigPath / "sentry"; + return dusk::CachePath / "sentry"; } std::filesystem::path log_attachment_path() { diff --git a/src/dusk/data.cpp b/src/dusk/data.cpp index 154083b365..ce61965947 100644 --- a/src/dusk/data.cpp +++ b/src/dusk/data.cpp @@ -6,6 +6,7 @@ #include "dusk/main.h" #include +#include #include #include #include @@ -30,6 +31,21 @@ constexpr auto kLocationDescriptorName = "data_location.json"; constexpr auto kPipelineCacheName = "pipeline_cache.db"; constexpr auto kInitialPipelineCacheName = "initial_pipeline_cache.db"; +constexpr std::array kUserDataDirectories = { + "texture_replacements", + "USA", + "EUR", + "JAP", +}; +constexpr std::array kUserDataFiles = { + "achievements.json", + "config.json", + "controller_ports.dat", + "imgui.ini", + "keyboard_bindings.dat", + "states.json", +}; + enum class LocationMode { Default, Portable, @@ -62,6 +78,7 @@ struct MigrationStats { std::optional sConfiguredDataPath; std::optional sActiveDescriptorPath; +std::optional sActivePrefPath; std::filesystem::path path_from_utf8(std::string_view value) { return std::filesystem::path{ @@ -70,19 +87,22 @@ std::filesystem::path path_from_utf8(std::string_view value) { }; } -std::filesystem::path get_legacy_path() { - if (std::string_view{LegacyAppName}.empty()) { +std::filesystem::path legacy_path_for_pref_path(const std::filesystem::path& prefPath) { + if (std::string_view{LegacyAppName}.empty() || prefPath.empty()) { return {}; } - char* prefPath = SDL_GetPrefPath(OrgName, LegacyAppName); - if (!prefPath) { - Log.fatal("Unable to get PrefPath: {}", SDL_GetError()); + auto normalizedPrefPath = prefPath; + if (normalizedPrefPath.filename().empty()) { + normalizedPrefPath = normalizedPrefPath.parent_path(); } - std::filesystem::path result{reinterpret_cast(prefPath)}; - SDL_free(prefPath); - return result; + const auto parentPath = normalizedPrefPath.parent_path(); + if (parentPath.empty()) { + return {}; + } + + return parentPath / LegacyAppName; } std::filesystem::path get_pref_path() { @@ -96,6 +116,13 @@ std::filesystem::path get_pref_path() { return result; } +std::filesystem::path active_pref_path() { + if (sActivePrefPath) { + return *sActivePrefPath; + } + return get_pref_path(); +} + std::filesystem::path base_path_relative(const std::filesystem::path& path) { const auto* basePath = SDL_GetBasePath(); if (!basePath) { @@ -249,6 +276,69 @@ std::filesystem::path absolute_path(const std::filesystem::path& path) { return absolute.lexically_normal(); } +std::filesystem::path rename_legacy_pref_path( + const std::filesystem::path& legacyPath, const std::filesystem::path& prefPath) { + if (legacyPath.empty() || prefPath.empty() || + normalized_path(legacyPath) == normalized_path(prefPath)) + { + return prefPath; + } + + std::error_code ec; + if (!std::filesystem::exists(legacyPath, ec)) { + if (ec) { + Log.warn("Failed to inspect legacy data directory '{}': {}", + io::fs_path_to_string(legacyPath), ec.message()); + } + return prefPath; + } + + const bool prefExists = std::filesystem::exists(prefPath, ec); + if (ec) { + Log.warn("Failed to inspect data directory '{}': {}", io::fs_path_to_string(prefPath), + ec.message()); + return prefPath; + } + if (prefExists) { + if (!std::filesystem::is_directory(prefPath, ec) || + !std::filesystem::is_empty(prefPath, ec)) + { + if (ec) { + Log.warn("Failed to inspect data directory '{}': {}", + io::fs_path_to_string(prefPath), ec.message()); + } else { + Log.info("Skipping legacy data directory rename because '{}' is not empty", + io::fs_path_to_string(prefPath)); + } + return prefPath; + } + + std::filesystem::remove(prefPath, ec); + if (ec) { + Log.warn("Failed to remove empty data directory '{}' before legacy rename: {}", + io::fs_path_to_string(prefPath), ec.message()); + return prefPath; + } + } + + std::filesystem::rename(legacyPath, prefPath, ec); + if (ec) { + Log.warn("Failed to rename legacy data directory '{}' to '{}': {}", + io::fs_path_to_string(legacyPath), io::fs_path_to_string(prefPath), ec.message()); + ec.clear(); + if (!std::filesystem::exists(prefPath, ec) && !ec) { + Log.info("Using legacy data directory '{}' because the new data directory is absent", + io::fs_path_to_string(legacyPath)); + return legacyPath; + } + return prefPath; + } + + Log.info("Renamed legacy data directory '{}' to '{}'", io::fs_path_to_string(legacyPath), + io::fs_path_to_string(prefPath)); + return prefPath; +} + bool is_same_or_inside(const std::filesystem::path& root, const std::filesystem::path& path) { const auto normalizedRoot = normalized_path(root); const auto normalizedPath = normalized_path(path); @@ -283,85 +373,46 @@ bool should_skip_migration_path(const std::filesystem::path& path, return false; } -bool has_location_descriptor(const std::filesystem::path& path) { - std::error_code ec; - return std::filesystem::exists(path / kLocationDescriptorName, ec); +bool matches_name(std::string_view name, const auto& names) { + return std::ranges::find(names, name) != names.end(); } -bool remove_empty_destination_for_rename(const std::filesystem::path& path) { - std::error_code ec; - const bool exists = std::filesystem::exists(path, ec); - if (ec) { - Log.debug("Could not inspect migration destination '{}': {}", io::fs_path_to_string(path), - ec.message()); +bool should_migrate_user_data_path( + const std::filesystem::path& sourcePath, const std::filesystem::path& from) { + const auto relativePath = sourcePath.lexically_relative(from); + if (relativePath.empty() || relativePath.is_absolute()) { return false; } - if (!exists) { + + auto it = relativePath.begin(); + if (it == relativePath.end() || *it == "..") { + return false; + } + + const auto first = io::fs_path_to_string(*it); + if (matches_name(first, kUserDataDirectories)) { return true; } - const bool canRemove = std::filesystem::is_directory(path, ec) && - std::filesystem::is_empty(path, ec) && !has_location_descriptor(path); - if (ec || !canRemove) { - if (ec) { - Log.debug("Could not inspect migration destination '{}': {}", - io::fs_path_to_string(path), ec.message()); - } + ++it; + if (it != relativePath.end()) { return false; } - std::filesystem::remove(path, ec); - if (ec) { - Log.debug("Could not remove empty migration destination '{}': {}", - io::fs_path_to_string(path), ec.message()); - return false; + const auto filename = io::fs_path_to_string(relativePath.filename()); + if (matches_name(filename, kUserDataFiles)) { + return true; } - return true; -} - -bool try_rename_directory_migration( - const std::filesystem::path& from, const std::filesystem::path& to) { - std::error_code ec; - if (!std::filesystem::is_directory(from, ec)) { - return false; - } - if (ec) { - Log.debug("Could not inspect migration source '{}': {}", io::fs_path_to_string(from), - ec.message()); - return false; - } - if (has_location_descriptor(from)) { - return false; - } - if (!remove_empty_destination_for_rename(to)) { - return false; - } - - std::filesystem::create_directories(to.parent_path(), ec); - if (ec) { - Log.debug("Could not create migration destination parent '{}': {}", - io::fs_path_to_string(to.parent_path()), ec.message()); - return false; - } - - std::filesystem::rename(from, to, ec); - if (ec) { - Log.debug("Could not rename data directory '{}' to '{}': {}", io::fs_path_to_string(from), - io::fs_path_to_string(to), ec.message()); - return false; - } - - Log.info("Renamed data directory '{}' to '{}'", io::fs_path_to_string(from), - io::fs_path_to_string(to)); - return true; + return relativePath.extension() == ".controller" || relativePath.extension() == ".gci" || + (filename.starts_with("MemoryCard") && filename.ends_with(".raw")); } std::filesystem::path current_data_path() { if (!ConfigPath.empty()) { return ConfigPath; } - const auto prefPath = get_pref_path(); + const auto prefPath = active_pref_path(); const auto descriptor = read_location_descriptor(prefPath); if (descriptor) { sActiveDescriptorPath = descriptor->path; @@ -427,7 +478,7 @@ bool write_location_descriptor(LocationMode mode, const std::filesystem::path& t json["previousPath"] = io::fs_path_to_string(descriptor.previousPath); } - const auto prefPath = get_pref_path(); + const auto prefPath = active_pref_path(); for (const auto& path : descriptor_write_paths(prefPath)) { if (write_descriptor_json(path, json)) { sActiveDescriptorPath = path; @@ -439,6 +490,61 @@ bool write_location_descriptor(LocationMode mode, const std::filesystem::path& t return false; } +void set_error(std::string* errorOut, std::string error) { + if (errorOut != nullptr) { + *errorOut = std::move(error); + } +} + +bool validate_writable_data_path(const std::filesystem::path& path, std::string* errorOut) { + if (path.empty()) { + set_error(errorOut, "Choose a folder."); + return false; + } + + std::error_code ec; + std::filesystem::create_directories(path, ec); + if (ec) { + set_error(errorOut, fmt::format("{} could not create the selected folder.", AppName)); + Log.warn("Failed to create custom data folder '{}': {}", io::fs_path_to_string(path), + ec.message()); + return false; + } + + if (!std::filesystem::is_directory(path, ec)) { + set_error(errorOut, "The selected path is not a folder."); + if (ec) { + Log.warn("Failed to inspect custom data folder '{}': {}", io::fs_path_to_string(path), + ec.message()); + } + return false; + } + + const auto probePath = path / fmt::format(".write-probe-{}.tmp", + std::chrono::steady_clock::now().time_since_epoch().count()); + try { + io::FileStream::WriteAllText(probePath, "dusk"); + } catch (const std::exception& e) { + set_error(errorOut, fmt::format("{} could not write to the selected folder.", AppName)); + Log.warn("Failed write probe for custom data folder '{}': {}", io::fs_path_to_string(path), + e.what()); + return false; + } + + std::filesystem::remove(probePath, ec); + if (ec) { + set_error( + errorOut, fmt::format("{} could write to the selected folder, but could not remove " + "the test file it created.", + AppName)); + Log.warn("Failed to remove custom data folder write probe '{}': {}", + io::fs_path_to_string(probePath), ec.message()); + return false; + } + + return true; +} + std::uintmax_t remove_empty_directories(const std::filesystem::path& root, bool includeRoot) { std::error_code ec; std::vector directories; @@ -647,14 +753,6 @@ void migrate_directory(const std::filesystem::path& from, const std::filesystem: return; } - if (try_rename_directory_migration(from, to)) { - return; - } - - if (try_rename_directory_migration(from, to)) { - return; - } - std::filesystem::create_directories(to, ec); if (ec) { ++stats.failures; @@ -700,6 +798,16 @@ void migrate_directory(const std::filesystem::path& from, const std::filesystem: continue; } + if (!should_migrate_user_data_path(sourcePath, from)) { + ++stats.skippedUnsupportedEntries; + if (std::filesystem::is_directory(status)) { + it.disable_recursion_pending(); + } + ec.clear(); + it.increment(ec); + continue; + } + const auto relativePath = sourcePath.lexically_relative(from); if (relativePath.empty() || relativePath.is_absolute()) { ++stats.failures; @@ -761,8 +869,6 @@ void migrate_data(const std::filesystem::path& prefPath, const std::filesystem:: const LocationDescriptor* descriptor) { if (descriptor && !descriptor->previousPath.empty()) { migrate_directory(descriptor->previousPath, dataPath, prefPath); - } else if (const auto legacyPath = get_legacy_path(); !legacyPath.empty()) { - migrate_directory(legacyPath, dataPath, prefPath); } } @@ -899,17 +1005,25 @@ bool open_data_path() { #endif } -bool set_custom_data_path(const std::filesystem::path& path) { - if (path.empty()) { - Log.warn("Ignoring empty custom data path"); +bool set_custom_data_path(const std::filesystem::path& path, std::string* errorOut) { + if (!validate_writable_data_path(path, errorOut)) { return false; } - return write_location_descriptor(LocationMode::Custom, path); + if (!write_location_descriptor(LocationMode::Custom, path)) { + set_error(errorOut, fmt::format("{} could not save the data folder setting.", AppName)); + return false; + } + + return true; } -bool set_custom_data_path(const char* path) { - return set_custom_data_path(path_from_utf8(path)); +bool set_custom_data_path(const char* path, std::string* errorOut) { + if (path == nullptr) { + set_error(errorOut, "Choose a folder."); + return false; + } + return set_custom_data_path(path_from_utf8(path), errorOut); } bool set_portable_data_path() { @@ -917,12 +1031,12 @@ bool set_portable_data_path() { } bool reset_data_path() { - const auto prefPath = get_pref_path(); + const auto prefPath = active_pref_path(); return write_location_descriptor(LocationMode::Default, default_data_path(prefPath)); } bool is_default_data_path() { - const auto prefPath = get_pref_path(); + const auto prefPath = active_pref_path(); return normalized_path(configured_data_path()) == normalized_path(default_data_path(prefPath)); } @@ -931,7 +1045,7 @@ std::filesystem::path configured_data_path() { return *sConfiguredDataPath; } - const auto prefPath = get_pref_path(); + const auto prefPath = active_pref_path(); const auto descriptor = read_location_descriptor(prefPath); if (descriptor) { sActiveDescriptorPath = descriptor->path; @@ -941,6 +1055,13 @@ std::filesystem::path configured_data_path() { return *sConfiguredDataPath; } +std::filesystem::path cache_path() { + if (!CachePath.empty()) { + return CachePath; + } + return active_pref_path(); +} + bool is_data_path_restart_pending() { if (ConfigPath.empty()) { return false; @@ -949,8 +1070,12 @@ bool is_data_path_restart_pending() { return normalized_path(ConfigPath) != normalized_path(configured_data_path()); } -std::filesystem::path initialize_data() { - const auto prefPath = get_pref_path(); +Paths initialize_data() { + const auto preferredPrefPath = get_pref_path(); + const auto prefPath = + rename_legacy_pref_path(legacy_path_for_pref_path(preferredPrefPath), preferredPrefPath); + sActivePrefPath = prefPath; + const auto descriptor = read_location_descriptor(prefPath); if (descriptor) { sActiveDescriptorPath = descriptor->path; @@ -963,9 +1088,13 @@ std::filesystem::path initialize_data() { migrate_data(prefPath, dataPath, descriptor ? &descriptor->descriptor : nullptr); ensure_data_directory(dataPath); - ensure_initial_pipeline_cache(dataPath); + ensure_data_directory(prefPath); + ensure_initial_pipeline_cache(prefPath); - return dataPath; + return Paths{ + .userPath = dataPath, + .cachePath = prefPath, + }; } } // namespace dusk::data diff --git a/src/dusk/data.hpp b/src/dusk/data.hpp index 85e9457bcb..c3ce495b46 100644 --- a/src/dusk/data.hpp +++ b/src/dusk/data.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #if defined(__APPLE__) #include @@ -14,7 +15,7 @@ #define DUSK_CAN_OPEN_DATA_FOLDER 0 #endif -#if defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST +#if (defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST) || defined(__ANDROID__) #define DUSK_CAN_CHANGE_DATA_FOLDER 0 #else #define DUSK_CAN_CHANGE_DATA_FOLDER 1 @@ -22,11 +23,17 @@ namespace dusk::data { -std::filesystem::path initialize_data(); +struct Paths { + std::filesystem::path userPath; + std::filesystem::path cachePath; +}; + +Paths initialize_data(); std::filesystem::path configured_data_path(); +std::filesystem::path cache_path(); bool open_data_path(); -bool set_custom_data_path(const char* path); -bool set_custom_data_path(const std::filesystem::path& path); +bool set_custom_data_path(const char* path, std::string* errorOut); +bool set_custom_data_path(const std::filesystem::path& path, std::string* errorOut); bool set_portable_data_path(); bool reset_data_path(); bool is_default_data_path(); diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 3dafc6b908..b1349ac385 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" @@ -240,7 +241,7 @@ namespace dusk { void ImGuiConsole::UpdateSettings() { getTransientSettings().skipFrameRateLimit = getSettings().game.enableTurboKeybind && - SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_TAB]; + (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; @@ -261,6 +262,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; @@ -361,7 +368,6 @@ namespace dusk { m_menuTools.ShowProcessManager(); m_menuTools.ShowHeapOverlay(); m_menuTools.ShowStubLog(); - m_menuTools.ShowMapLoader(); m_menuTools.ShowBloomWindow(); m_menuTools.ShowPlayerInfo(); m_menuTools.ShowAudioDebug(); 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/ImGuiMapLoader.cpp b/src/dusk/imgui/ImGuiMapLoader.cpp deleted file mode 100644 index cd6f292152..0000000000 --- a/src/dusk/imgui/ImGuiMapLoader.cpp +++ /dev/null @@ -1,149 +0,0 @@ -#include "d/d_com_inf_game.h" - -#include "imgui.h" -#include -#include "ImGuiConsole.hpp" -#include "ImGuiMenuTools.hpp" -#include "dusk/map_loader_definitions.h" -#include "fmt/format.h" - -namespace dusk { - void ImGuiMenuTools::ShowMapLoader() { - if (!getSettings().backend.enableAdvancedSettings || - !ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F7, m_showMapLoader)) - { - return; - } - - ImGuiWindowFlags windowFlags = ImGuiWindowFlags_AlwaysAutoResize | - ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav; - - // ImGui::SetNextWindowBgAlpha(0.65f); - - if (!ImGui::Begin("Map Loader", &m_showMapLoader, windowFlags)) { - ImGui::End(); - return; - } - - ImGui::Checkbox("Show Internal Names", &m_mapLoaderInfo.showInternalNames); - - const char* previewRegion = "None"; - if (m_mapLoaderInfo.regionIdx != -1) { - previewRegion = gameRegions[m_mapLoaderInfo.regionIdx].regionName; - } - - if (ImGui::BeginCombo("Select Region", previewRegion)) { - int idx = 0; - for (const auto& region : gameRegions) { - if (ImGui::Selectable(region.regionName)) { - if (m_mapLoaderInfo.regionIdx != idx) { - m_mapLoaderInfo.mapIdx = 0; - m_mapLoaderInfo.roomNoIdx = 0; - m_mapLoaderInfo.pointNoIdx = 0; - } - m_mapLoaderInfo.regionIdx = idx; - } - idx++; - } - ImGui::EndCombo(); - } - - if (m_mapLoaderInfo.regionIdx != -1) { - const auto& region = gameRegions[m_mapLoaderInfo.regionIdx]; - - std::string previewMap = "None"; - if (m_mapLoaderInfo.mapIdx != -1) { - const auto& map = region.maps[m_mapLoaderInfo.mapIdx]; - previewMap = m_mapLoaderInfo.showInternalNames ? fmt::format("{} ({})", map.mapName, map.mapFile) : map.mapName; - } - - if (ImGui::BeginCombo("Select Map", previewMap.data())) { - int prevMapIdx = m_mapLoaderInfo.mapIdx; - for (int i = 0; i < region.maps.size(); ++i) { - const auto& map = region.maps[i]; - std::string label = m_mapLoaderInfo.showInternalNames ? fmt::format("{} ({})", map.mapName, map.mapFile) : map.mapName; - if (ImGui::Selectable(label.data())) { - m_mapLoaderInfo.mapIdx = i; - } - } - ImGui::EndCombo(); - if (m_mapLoaderInfo.mapIdx != prevMapIdx) { - m_mapLoaderInfo.roomNoIdx = 0; - m_mapLoaderInfo.pointNoIdx = 0; - } - } - } else { - ImGui::Text("No region selected."); - } - - if (m_mapLoaderInfo.regionIdx != -1 && m_mapLoaderInfo.mapIdx != -1) { - const auto& region = gameRegions[m_mapLoaderInfo.regionIdx]; - const auto& map = region.maps[m_mapLoaderInfo.mapIdx]; - const auto& room = map.mapRooms[m_mapLoaderInfo.roomNoIdx]; - - if (map.mapRooms.size() > 1) { - ImGui::Text("Selected Room: %2d", room.roomNo); - ImGui::SameLine(); - if (ImGui::Button("-###RoomNoIdxDec")) { - m_mapLoaderInfo.roomNoIdx--; - if (m_mapLoaderInfo.roomNoIdx < 0) { - m_mapLoaderInfo.roomNoIdx = map.mapRooms.size() - 1; - } - m_mapLoaderInfo.pointNoIdx = 0; - } - ImGui::SameLine(); - if (ImGui::Button("+###RoomNoIdxInc")) { - m_mapLoaderInfo.roomNoIdx++; - if (m_mapLoaderInfo.roomNoIdx >= map.mapRooms.size()) { - m_mapLoaderInfo.roomNoIdx = 0; - } - m_mapLoaderInfo.pointNoIdx = 0; - } - } - - constexpr int MAX_LAYER = 14; - - ImGui::Text("Selected Layer: %3d", m_mapLoaderInfo.layer); - ImGui::SameLine(); - if (ImGui::Button("-###layerDec")) { - m_mapLoaderInfo.layer--; - if (m_mapLoaderInfo.layer < -1) { - m_mapLoaderInfo.layer = MAX_LAYER; - } - } - ImGui::SameLine(); - if (ImGui::Button("+###layerInc")) { - m_mapLoaderInfo.layer++; - if (m_mapLoaderInfo.layer > MAX_LAYER) { - m_mapLoaderInfo.layer = -1; - } - } - - if (room.roomPoints.size() > 1) { - ImGui::Text("Selected Point: %3d", room.roomPoints[m_mapLoaderInfo.pointNoIdx]); - ImGui::SameLine(); - if (ImGui::Button("-###PointNoIdxDec")) { - m_mapLoaderInfo.pointNoIdx--; - if (m_mapLoaderInfo.pointNoIdx < 0) { - m_mapLoaderInfo.pointNoIdx = room.roomPoints.size() - 1; - } - } - ImGui::SameLine(); - if (ImGui::Button("+###PointNoIdxInc")) { - m_mapLoaderInfo.pointNoIdx++; - if (m_mapLoaderInfo.pointNoIdx >= room.roomPoints.size()) { - m_mapLoaderInfo.pointNoIdx = 0; - } - } - } - - if (ImGui::Button("Warp")) { - dComIfGp_setNextStage(map.mapFile, room.roomPoints[m_mapLoaderInfo.pointNoIdx], room.roomNo, m_mapLoaderInfo.layer); - } - } - - - ImGui::End(); - } - - } // namespace dusk diff --git a/src/dusk/imgui/ImGuiMenuTools.cpp b/src/dusk/imgui/ImGuiMenuTools.cpp index 2206e0e3c6..3045e09df1 100644 --- a/src/dusk/imgui/ImGuiMenuTools.cpp +++ b/src/dusk/imgui/ImGuiMenuTools.cpp @@ -40,7 +40,6 @@ namespace dusk { ImGui::BeginDisabled(getSettings().game.speedrunMode); ImGui::MenuItem("Save Editor", hotkeys::SHOW_SAVE_EDITOR, &m_showSaveEditor); - ImGui::MenuItem("Map Loader", hotkeys::SHOW_MAP_LOADER, &m_showMapLoader); ImGui::MenuItem("State Share", hotkeys::SHOW_STATE_SHARE, &m_showStateShare); ImGui::EndDisabled(); @@ -49,9 +48,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..61fab81f79 100644 --- a/src/dusk/imgui/ImGuiMenuTools.hpp +++ b/src/dusk/imgui/ImGuiMenuTools.hpp @@ -21,7 +21,6 @@ namespace dusk { void ShowProcessManager(); void ShowHeapOverlay(); void ShowStubLog(); - void ShowMapLoader(); void ShowBloomWindow(); void ShowPlayerInfo(); void ShowAudioDebug(); @@ -43,22 +42,9 @@ namespace dusk { bool m_showStubLog = false; - bool m_showMapLoader = false; - bool m_showBloomWindow = false; bool m_showAudioDebug = false; - struct { - int mapIdx = -1; - int regionIdx = -1; - int roomNoIdx = 0; - int pointNoIdx = 0; - int roomNo = -1; - int pointNo = -1; - int spawnId = 0; - int layer = -1; - bool showInternalNames = false; - } m_mapLoaderInfo; bool m_showPlayerInfo = false; int m_playerInfoOverlayCorner = 1; // top-right @@ -69,8 +55,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/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'; 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() { diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp index 9a13d0ecc8..dbe27db8aa 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -13,7 +13,7 @@ UserSettings g_userSettings = { }, .audio = { - .masterVolume {"audio.masterVolume", 80}, + .masterVolume {"audio.masterVolume", 60}, .mainMusicVolume {"audio.mainMusicVolume", 100}, .subMusicVolume {"audio.subMusicVolume", 100}, .soundEffectsVolume {"audio.soundEffectsVolume", 100}, @@ -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}, @@ -92,6 +93,7 @@ UserSettings g_userSettings = { // Cheats .infiniteHearts {"game.infiniteHearts", false}, .infiniteArrows {"game.infiniteArrows", false}, + .infiniteSeeds {"game.infiniteSeeds", false}, .infiniteBombs {"game.infiniteBombs", false}, .infiniteOil {"game.infiniteOil", false}, .infiniteOxygen {"game.infiniteOxygen", false}, @@ -105,18 +107,22 @@ UserSettings g_userSettings = { .fastRoll {"game.fastRoll", false}, .fastSpinner {"game.fastSpinner", false}, .freeMagicArmor {"game.freeMagicArmor", false}, + .invincibleEnemies {"game.invincibleEnemies", false}, // Technical .restoreWiiGlitches {"game.restoreWiiGlitches", false}, // 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 = { @@ -129,6 +135,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}, + }, } }; @@ -180,6 +214,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); @@ -200,13 +235,17 @@ 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); + Register(g_userSettings.game.infiniteSeeds); Register(g_userSettings.game.infiniteBombs); Register(g_userSettings.game.infiniteOil); Register(g_userSettings.game.infiniteOxygen); @@ -215,6 +254,7 @@ void registerSettings() { Register(g_userSettings.game.moonJump); Register(g_userSettings.game.superClawshot); Register(g_userSettings.game.alwaysGreatspin); + Register(g_userSettings.game.invincibleEnemies); Register(g_userSettings.game.enableFrameInterpolation); Register(g_userSettings.game.gyroMode); Register(g_userSettings.game.enableGyroAim); @@ -240,6 +280,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/speedrun.cpp b/src/dusk/speedrun.cpp index 0dc193d494..feb2178c41 100644 --- a/src/dusk/speedrun.cpp +++ b/src/dusk/speedrun.cpp @@ -20,6 +20,7 @@ void resetForSpeedrunMode() { 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); 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/editor.cpp b/src/dusk/ui/editor.cpp index 469bb920e3..bd6dc3e067 100644 --- a/src/dusk/ui/editor.cpp +++ b/src/dusk/ui/editor.cpp @@ -26,6 +26,43 @@ #include namespace dusk::ui { + +Rml::String stage_option_label(const MapEntry& map, bool showInternalNames) { + return showInternalNames ? fmt::format("{} ({})", map.mapName, map.mapFile) : map.mapName; +} + +Rml::String stage_label_for_file(const Rml::String& stageFile, bool showInternalNames) { + for (const auto& region : gameRegions) { + for (const auto& map : region.maps) { + if (stageFile == map.mapFile) { + return stage_option_label(map, showInternalNames); + } + } + } + return stageFile; +} + +void populate_stage_picker(Pane& pane, std::function getStageFile, + std::function setStageFile, bool showInternalNames) { + pane.clear(); + for (const auto& region : gameRegions) { + pane.add_section(region.regionName); + for (const auto& map : region.maps) { + pane.add_button({ + .text = stage_option_label(map, showInternalNames), + .isSelected = + [getStageFile, stageFile = map.mapFile] { + return getStageFile() == stageFile; + }, + }) + .on_pressed([setStageFile, stageFile = map.mapFile] { + mDoAud_seStartMenu(kSoundItemChange); + setStageFile(stageFile); + }); + } + } +} + namespace { bool has_save_data() { @@ -155,44 +192,6 @@ bool parse_vec3(const Rml::String& value, float& x, float& y, float& z) { return *cursor == '\0'; } -Rml::String stage_option_label(const MapEntry& map) { - // TODO: option to show internal name? - // return fmt::format("{} ({})", map.mapName, map.mapFile); - return map.mapName; -} - -Rml::String stage_label_for_file(const Rml::String& stageFile) { - for (const auto& region : gameRegions) { - for (const auto& map : region.maps) { - if (stageFile == map.mapFile) { - return stage_option_label(map); - } - } - } - return stageFile; -} - -void populate_stage_picker(Pane& pane, std::function getStageFile, - std::function setStageFile) { - pane.clear(); - for (const auto& region : gameRegions) { - pane.add_section(region.regionName); - for (const auto& map : region.maps) { - pane.add_button({ - .text = stage_option_label(map), - .isSelected = - [getStageFile, stageFile = map.mapFile] { - return getStageFile() == stageFile; - }, - }) - .on_pressed([setStageFile, stageFile = map.mapFile] { - mDoAud_seStartMenu(kSoundItemChange); - setStageFile(stageFile); - }); - } - } -} - Rml::String get_player_name() { if (!has_save_data()) { return ""; diff --git a/src/dusk/ui/editor.hpp b/src/dusk/ui/editor.hpp index cec381e277..c2cbbcd834 100644 --- a/src/dusk/ui/editor.hpp +++ b/src/dusk/ui/editor.hpp @@ -1,8 +1,19 @@ #pragma once + #include "window.hpp" +struct MapEntry; + namespace dusk::ui { +class Pane; + +Rml::String stage_option_label(const MapEntry& map, bool showInternalNames = false); +Rml::String stage_label_for_file(const Rml::String& stageFile, bool showInternalNames = false); +void populate_stage_picker(Pane& pane, std::function getStageFile, + std::function setStageFile, + bool showInternalNames = false); + class EditorWindow : public Window { public: EditorWindow(); 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/menu_bar.cpp b/src/dusk/ui/menu_bar.cpp index 6ec374abb2..36dc981fa3 100644 --- a/src/dusk/ui/menu_bar.cpp +++ b/src/dusk/ui/menu_bar.cpp @@ -19,6 +19,7 @@ #include "mods_window.hpp" #include "settings.hpp" #include "ui.hpp" +#include "warp.hpp" #include "window.hpp" #include @@ -52,11 +53,9 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById( .autoSelect = false, }); mTabBar->add_tab("Settings", [this] { push(std::make_unique()); }); - // mTabBar->add_tab("Warp", [] { - // // TODO - // }); if (getSettings().backend.enableAdvancedSettings) { + mTabBar->add_tab("Warp", [this] { push(std::make_unique()); }); mTabBar->add_tab("Editor", [this] { push(std::make_unique()); }); } diff --git a/src/dusk/ui/overlay.cpp b/src/dusk/ui/overlay.cpp index 9a3ef78021..c9b2b8614d 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; @@ -342,8 +354,9 @@ void Overlay::update() { } } + u32 count = 0; const bool showControllerWarning = PADGetIndexForPort(PAD_CHAN0) < 0 && - PADGetKeyButtonBindings(PAD_CHAN0, nullptr) == nullptr && + PADGetKeyButtonBindings(PAD_CHAN0, &count) == nullptr && dynamic_cast(top_document()) == nullptr && dynamic_cast(top_document()) == nullptr; if (showControllerWarning && mControllerWarning == nullptr) { diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 305f05f3b2..c54b3784be 100644 --- a/src/dusk/ui/settings.cpp +++ b/src/dusk/ui/settings.cpp @@ -3,18 +3,22 @@ #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" @@ -23,6 +27,7 @@ #include #include +#include #if DUSK_ENABLE_SENTRY_NATIVE #include "dusk/crash_reporting.h" @@ -182,6 +187,7 @@ void reset_for_speedrun_mode() { 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); @@ -195,6 +201,7 @@ void reset_for_speedrun_mode() { 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); @@ -293,13 +300,49 @@ 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::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 || path == nullptr) { + if (error != nullptr) { + show_data_folder_error_modal(error); return; } - if (data::set_custom_data_path(path)) { - mDoAud_seStartMenu(kSoundItemChange); + 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 = @@ -710,7 +753,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, @@ -815,9 +857,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( @@ -892,6 +934,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) { @@ -908,7 +953,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { [](int value) { getSettings().audio.masterVolume.setValue(value); config::Save(); - audio::SetMasterVolume(value / 100.f); + audio::SetMasterVolume(audio::MasterVolumeToLinear(value / 100.0f)); }, .isModified = [] { @@ -1103,6 +1148,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { 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, @@ -1129,6 +1175,8 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { "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) { @@ -1249,6 +1297,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", @@ -1267,6 +1329,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, diff --git a/src/dusk/ui/warp.cpp b/src/dusk/ui/warp.cpp new file mode 100644 index 0000000000..efae7f1d65 --- /dev/null +++ b/src/dusk/ui/warp.cpp @@ -0,0 +1,334 @@ +#include "warp.hpp" + +#include "editor.hpp" +#include "pane.hpp" + +#include "dusk/map_loader_definitions.h" +#include "fmt/format.h" + +namespace dusk::ui { +namespace { + +constexpr int kMinLayer = -1; +constexpr int kMaxLayer = 14; + +struct WarpSelectionState { + int regionIdx = 0; + int mapIdx = 0; + int roomIdx = 0; + int pointIdx = 0; + int layer = -1; + bool showInternalNames = false; +}; + +WarpSelectionState& selection_state() { + static WarpSelectionState state; + return state; +} + +const RegionEntry* selected_region(const WarpSelectionState& state) { + if (state.regionIdx < 0 || state.regionIdx >= static_cast(gameRegions.size())) { + return nullptr; + } + return &gameRegions[state.regionIdx]; +} + +const MapEntry* selected_map(const WarpSelectionState& state) { + const auto* region = selected_region(state); + if (region == nullptr || state.mapIdx < 0 || state.mapIdx >= static_cast(region->maps.size())) { + return nullptr; + } + return ®ion->maps[state.mapIdx]; +} + +const RoomEntry* selected_room(const WarpSelectionState& state) { + const auto* map = selected_map(state); + if (map == nullptr || state.roomIdx < 0 || state.roomIdx >= static_cast(map->mapRooms.size())) { + return nullptr; + } + return &map->mapRooms[state.roomIdx]; +} + +const s16* selected_point(const WarpSelectionState& state) { + const auto* room = selected_room(state); + if (room == nullptr || state.pointIdx < 0 || + state.pointIdx >= static_cast(room->roomPoints.size())) + { + return nullptr; + } + return &room->roomPoints[state.pointIdx]; +} + +void clamp_indices(WarpSelectionState& state) { + if (gameRegions.empty()) { + state.regionIdx = -1; + state.mapIdx = -1; + state.roomIdx = -1; + state.pointIdx = -1; + state.layer = std::clamp(state.layer, kMinLayer, kMaxLayer); + return; + } + + state.regionIdx = std::clamp(state.regionIdx, 0, static_cast(gameRegions.size()) - 1); + const auto& region = gameRegions[state.regionIdx]; + if (region.maps.empty()) { + state.mapIdx = -1; + state.roomIdx = -1; + state.pointIdx = -1; + state.layer = std::clamp(state.layer, kMinLayer, kMaxLayer); + return; + } + + state.mapIdx = std::clamp(state.mapIdx, 0, static_cast(region.maps.size()) - 1); + const auto& map = region.maps[state.mapIdx]; + if (map.mapRooms.empty()) { + state.roomIdx = -1; + state.pointIdx = -1; + state.layer = std::clamp(state.layer, kMinLayer, kMaxLayer); + return; + } + + state.roomIdx = std::clamp(state.roomIdx, 0, static_cast(map.mapRooms.size()) - 1); + const auto& room = map.mapRooms[state.roomIdx]; + if (room.roomPoints.empty()) { + state.pointIdx = -1; + } else { + state.pointIdx = std::clamp(state.pointIdx, 0, static_cast(room.roomPoints.size()) - 1); + } + + state.layer = std::clamp(state.layer, kMinLayer, kMaxLayer); +} + +bool can_warp(const WarpSelectionState& state) { + return selected_point(state) != nullptr; +} + +void reset_selection(WarpSelectionState& state) { + state.roomIdx = 0; + state.pointIdx = 0; + state.layer = kMinLayer; + clamp_indices(state); +} + +void populate_map_picker(Pane& pane, WarpSelectionState& state) { + pane.clear(); + clamp_indices(state); + if (state.regionIdx < 0 || state.regionIdx >= static_cast(gameRegions.size())) { + return; + } + + pane.add_button({ + .text = "Show Internal Names", + .isSelected = [&state] { return state.showInternalNames; }, + }) + .on_pressed([&pane, &state] { + mDoAud_seStartMenu(kSoundItemChange); + state.showInternalNames = !state.showInternalNames; + populate_map_picker(pane, state); + }); + + pane.add_section("Maps"); + const auto& region = gameRegions[state.regionIdx]; + for (int i = 0; i < static_cast(region.maps.size()); ++i) { + pane.add_button({ + .text = stage_option_label(region.maps[i], state.showInternalNames), + .isSelected = [i, &state] { return state.mapIdx == i; }, + }) + .on_pressed([i, &state] { + mDoAud_seStartMenu(kSoundItemChange); + if (state.mapIdx != i) { + state.mapIdx = i; + reset_selection(state); + } + }); + } +} + +} // namespace + +WarpWindow::WarpWindow() { + add_tab("Warp", [this](Rml::Element* content) { + auto& leftPane = add_child(content, Pane::Type::Controlled); + auto& rightPane = add_child(content, Pane::Type::Uncontrolled); + auto& state = selection_state(); + clamp_indices(state); + + leftPane.add_section("Destination"); + leftPane.register_control( + leftPane.add_select_button({ + .key = "Region", + .getValue = + [&state] { + clamp_indices(state); + const auto* region = selected_region(state); + return region == nullptr ? Rml::String{"None"} : + Rml::String{region->regionName}; + }, + }), + rightPane, [&state](Pane& pane) { + pane.clear(); + for (int i = 0; i < static_cast(gameRegions.size()); ++i) { + pane.add_button({ + .text = gameRegions[i].regionName, + .isSelected = [i, &state] { return state.regionIdx == i; }, + }) + .on_pressed([i, &state] { + mDoAud_seStartMenu(kSoundItemChange); + if (state.regionIdx != i) { + state.regionIdx = i; + state.mapIdx = 0; + reset_selection(state); + } + }); + } + }); + + leftPane.register_control( + leftPane.add_select_button({ + .key = "Map", + .getValue = + [&state] { + clamp_indices(state); + const auto* map = selected_map(state); + return map == nullptr ? Rml::String{"None"} : + stage_option_label(*map, state.showInternalNames); + }, + }), + rightPane, [&state](Pane& pane) { populate_map_picker(pane, state); }); + + leftPane.register_control( + leftPane.add_select_button({ + .key = "Room", + .getValue = [&state] { + clamp_indices(state); + const auto* room = selected_room(state); + return room == nullptr ? Rml::String{"None"} : + fmt::format("{}", room->roomNo); + }, + .isDisabled = [&state] { + clamp_indices(state); + const auto* map = selected_map(state); + return map == nullptr || map->mapRooms.size() <= 1; + }, + }), + rightPane, [&state](Pane& pane) { + pane.clear(); + clamp_indices(state); + if (state.regionIdx < 0 || state.regionIdx >= static_cast(gameRegions.size())) { + return; + } + const auto& region = gameRegions[state.regionIdx]; + if (state.mapIdx < 0 || state.mapIdx >= static_cast(region.maps.size())) { + return; + } + const auto& map = region.maps[state.mapIdx]; + for (int i = 0; i < static_cast(map.mapRooms.size()); ++i) { + pane.add_button({ + .text = fmt::format("{}", map.mapRooms[i].roomNo), + .isSelected = [i, &state] { return state.roomIdx == i; }, + }) + .on_pressed([i, &state] { + mDoAud_seStartMenu(kSoundItemChange); + if (state.roomIdx != i) { + state.roomIdx = i; + state.pointIdx = 0; + clamp_indices(state); + } + }); + } + }); + + leftPane.register_control( + leftPane.add_select_button({ + .key = "Point", + .getValue = [&state] { + clamp_indices(state); + const auto* point = selected_point(state); + return point == nullptr ? Rml::String{"None"} : fmt::format("{}", *point); + }, + .isDisabled = [&state] { + clamp_indices(state); + const auto* room = selected_room(state); + return room == nullptr || room->roomPoints.size() <= 1; + }, + }), + rightPane, [&state](Pane& pane) { + pane.clear(); + clamp_indices(state); + if (state.regionIdx < 0 || state.regionIdx >= static_cast(gameRegions.size())) { + return; + } + const auto& region = gameRegions[state.regionIdx]; + if (state.mapIdx < 0 || state.mapIdx >= static_cast(region.maps.size())) { + return; + } + const auto& map = region.maps[state.mapIdx]; + if (state.roomIdx < 0 || state.roomIdx >= static_cast(map.mapRooms.size())) { + return; + } + const auto& room = map.mapRooms[state.roomIdx]; + for (int i = 0; i < static_cast(room.roomPoints.size()); ++i) { + pane.add_button({ + .text = fmt::format("{}", room.roomPoints[i]), + .isSelected = [i, &state] { return state.pointIdx == i; }, + }) + .on_pressed([i, &state] { + if (state.pointIdx != i) { + mDoAud_seStartMenu(kSoundItemChange); + state.pointIdx = i; + clamp_indices(state); + } + }); + } + }); + + leftPane.register_control( + leftPane.add_select_button({ + .key = "Layer", + .getValue = [&state] { return fmt::format("{}", state.layer); }, + }), + rightPane, [&state](Pane& pane) { + pane.clear(); + for (int layer = kMinLayer; layer <= kMaxLayer; ++layer) { + pane.add_button({ + .text = fmt::format("{}", layer), + .isSelected = [layer, &state] { return state.layer == layer; }, + }) + .on_pressed([layer, &state] { + if (state.layer != layer) { + mDoAud_seStartMenu(kSoundItemChange); + state.layer = layer; + } + }); + } + }); + + leftPane.add_section("Action"); + leftPane.register_control( + leftPane.add_button({ + .text = "Warp", + .isDisabled = [&state] { + clamp_indices(state); + return !can_warp(state); + }, + }) + .on_pressed([&state] { + clamp_indices(state); + if (!can_warp(state)) { + return; + } + + mDoAud_seStartMenu(kSoundClick); + const auto& region = gameRegions[state.regionIdx]; + const auto& map = region.maps[state.mapIdx]; + const auto& room = map.mapRooms[state.roomIdx]; + dComIfGp_setNextStage( map.mapFile, room.roomPoints[state.pointIdx], room.roomNo, state.layer); + }), + rightPane, [](Pane& pane) { + pane.clear(); + pane.add_text("Warp to the selected destination."); + }); + }); +} + +} // namespace dusk::ui diff --git a/src/dusk/ui/warp.hpp b/src/dusk/ui/warp.hpp new file mode 100644 index 0000000000..9031474b39 --- /dev/null +++ b/src/dusk/ui/warp.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "window.hpp" + +namespace dusk::ui { + +class WarpWindow : public Window { +public: + WarpWindow(); +}; + +} // namespace dusk::ui diff --git a/src/f_ap/f_ap_game.cpp b/src/f_ap/f_ap_game.cpp index 92b48a74c7..0b7226c9ab 100644 --- a/src/f_ap/f_ap_game.cpp +++ b/src/f_ap/f_ap_game.cpp @@ -793,6 +793,10 @@ static void duskExecute() { dComIfGs_setArrowNum(dComIfGs_getArrowMax()); } + if (dusk::getSettings().game.infiniteSeeds) { + dComIfGs_setPachinkoNum(dComIfGs_getPachinkoMax()); + } + if (dusk::getSettings().game.infiniteBombs) { dComIfGs_setBombNum(0, 99); dComIfGs_setBombNum(1, 99); diff --git a/src/f_pc/f_pc_base.cpp b/src/f_pc/f_pc_base.cpp index 848153b836..7bb53ae634 100644 --- a/src/f_pc/f_pc_base.cpp +++ b/src/f_pc/f_pc_base.cpp @@ -136,8 +136,17 @@ base_process_class* fpcBs_Create(s16 i_profname, fpc_ProcID i_procID, void* i_ap u32 size; pprofile = (process_profile_definition*)fpcPf_Get(i_profname); + if (pprofile == NULL) { +#if TARGET_PC + DuskLog.debug("fpcBs_Create: profile not found for profname={}", i_profname); +#endif + return NULL; + } +#if TARGET_PC + const char* procName = getProcName(i_profname); DuskLog.debug("fpcBs_Create: pid={} profname={} ({}) profile={} procSize={} unkSize={}", - i_procID, getProcName(i_profname), i_profname, (void*)pprofile, pprofile->process_size, pprofile->unk_size); + i_procID, procName ? procName : "(unknown)", i_profname, (void*)pprofile, pprofile->process_size, pprofile->unk_size); +#endif size = pprofile->process_size + pprofile->unk_size; pprocess = (base_process_class*)cMl::memalignB(-4, size); diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 6ca37a6962..a356bbd571 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -72,6 +72,7 @@ #include #include +#include "SDL3/SDL_init.h" #include "SDL3/SDL_filesystem.h" #include "SDL3/SDL_iostream.h" #include "SDL3/SDL_misc.h" @@ -80,6 +81,7 @@ #include "dusk/audio/DuskAudioSystem.h" #include "dusk/audio/DuskDsp.hpp" #include "dusk/config.hpp" +#include "dusk/speedrun.h" #include "dusk/settings.h" #include "dusk/io.hpp" #include "dusk/version.hpp" @@ -119,6 +121,7 @@ bool dusk::IsShuttingDown = false; bool dusk::IsGameLaunched = false; bool dusk::RestartRequested = false; std::filesystem::path dusk::ConfigPath; +std::filesystem::path dusk::CachePath; #endif void dusk::RequestRestart() noexcept { @@ -465,6 +468,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 // ========================================================================= @@ -512,10 +520,17 @@ int game_main(int argc, char* argv[]) { const auto startupLogLevel = static_cast(parsed_arg_options["log-level"].as()); - dusk::ConfigPath = dusk::data::initialize_data(); - dusk::InitializeFileLogging(dusk::ConfigPath, startupLogLevel); + const auto dataPaths = dusk::data::initialize_data(); + dusk::ConfigPath = dataPaths.userPath; + dusk::CachePath = dataPaths.cachePath; + dusk::InitializeFileLogging(dusk::CachePath, startupLogLevel); + + log_build_info(); dusk::config::LoadFromUserPreferences(); + if (dusk::getSettings().game.speedrunMode) { + dusk::resetForSpeedrunMode(); + } ApplyCVarOverrides(parsed_arg_options["cvar"]); dusk::crash_reporting::initialize(); // TODO: How to handle this? @@ -529,11 +544,16 @@ 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(); + const auto userPathString = dusk::ConfigPath.u8string(); + const auto cachePathString = dusk::CachePath.u8string(); AuroraConfig config{}; config.appName = dusk::AppName; - config.configPath = reinterpret_cast(configPathString.c_str()); + config.userPath = reinterpret_cast(userPathString.c_str()); + config.cachePath = reinterpret_cast(cachePathString.c_str()); config.vsync = dusk::getSettings().video.enableVsync; config.startFullscreen = dusk::getSettings().video.enableFullscreen; config.windowPosX = -1; @@ -554,7 +574,9 @@ int game_main(int argc, char* argv[]) { } #ifdef DUSK_DISCORD - dusk::discord::initialize(); + if (dusk::getSettings().game.enableDiscordPresence) { + dusk::discord::initialize(); + } #endif VISetWindowTitle( @@ -568,7 +590,7 @@ int game_main(int argc, char* argv[]) { } VISetFrameBufferScale(dusk::getSettings().game.internalResolutionScale.getValue()); - dusk::audio::SetMasterVolume(dusk::getSettings().audio.masterVolume / 100.0f); + dusk::audio::SetMasterVolume(dusk::audio::MasterVolumeToLinear(dusk::getSettings().audio.masterVolume / 100.0f)); dusk::audio::SetEnableReverb(dusk::getSettings().audio.enableReverb); dusk::audio::EnableHrtf = dusk::getSettings().audio.enableHrtf;