diff --git a/.gitignore b/.gitignore index a1839ff34e..43b2e4a1c3 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,10 @@ compile_commands.json # MacOS .DS_Store +# direnv / nix +.direnv/ +.envrc + # ISOs *.iso diff --git a/CMakeLists.txt b/CMakeLists.txt index aaf08be2ef..cf69a8e2ac 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_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") @@ -112,11 +125,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") @@ -155,6 +163,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 @@ -266,6 +276,12 @@ if (DUSK_ENABLE_SENTRY_NATIVE) endif () endif () +# Use signed char on ARM to match the original game (and x86) +string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _arch) +if(_arch MATCHES "^(arm|aarch64)" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "GNU") + add_compile_options(-fsigned-char) +endif() + if (CMAKE_SYSTEM_NAME STREQUAL Windows) set(PLATFORM_NAME win32) elseif (CMAKE_SYSTEM_NAME STREQUAL Darwin) 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/README.md b/README.md index 322137d86f..106d2c0a0e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ It aims to be as accurate as possible to the original while also providing new o > Dusklight does *not* provide any copyrighted assets. You must provide your own copy of the original game. > [!IMPORTANT] -> At a minimum, Dusklight requires a GPU with support for either D3D12, Vulkan, or Metal. Your experience with specific hardware, operating systems, and drivers may vary. In particular, older Intel iGPUs have a high likelyhood of incompatibility. We are also aware of a number of issues on devices with Adreno GPUs and are working to resolve them. +> At a minimum, Dusklight requires a GPU with support for either D3D12, Vulkan, or Metal. Your experience with specific hardware, operating systems, and drivers may vary. In particular, older Intel iGPUs have a high likelihood of incompatibility. We are also aware of a number of issues on devices with Adreno GPUs and are working to resolve them. ### 1. Verify your dump diff --git a/docs/building.md b/docs/building.md index 1001c42788..9f7879ab48 100644 --- a/docs/building.md +++ b/docs/building.md @@ -1,50 +1,164 @@ -### Building -#### Prerequisites +# Building Dusklight + +## Dependencies + +The following dependencies are required: + * [CMake 3.25+](https://cmake.org) - * Windows: Install `CMake Tools` in Visual Studio - * macOS: `brew install cmake` * [Python 3+](https://python.org) - * Windows: [Microsoft Store](https://go.microsoft.com/fwlink?linkID=2082640) - * Verify it's added to `%PATH%` by typing `python` in `cmd`. - * macOS: `brew install python@3` -* **[Windows]** [Visual Studio 2026 Community](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx) - * Select `C++ Development` and verify the following packages are included: - * `Windows 11 SDK` - * `CMake Tools` - * `C++ Clang Compiler` - * `C++ Clang-cl` -* **[macOS]** [Xcode 16.4+](https://developer.apple.com/xcode/download/) -* **[Linux]** Actively tested on Ubuntu 24.04, Arch Linux & derivatives. - * Ubuntu 24.04+ packages - ``` - build-essential curl git ninja-build clang lld zlib1g-dev libcurl4-openssl-dev \ - libglu1-mesa-dev libdbus-1-dev libvulkan-dev libxi-dev libxrandr-dev libasound2-dev libpulse-dev \ - libudev-dev libpng-dev libncurses5-dev cmake libx11-xcb-dev python3 python-is-python3 \ - libclang-dev libfreetype-dev libxinerama-dev libxcursor-dev python3-markupsafe libgtk-3-dev \ - libxss-dev libxtst-dev - ``` - * Arch Linux packages - ``` - base-devel cmake ninja llvm vulkan-headers python python-markupsafe clang lld alsa-lib libpulse libxrandr freetype2 - ``` - * Fedora packages - ``` - cmake vulkan-headers ninja-build clang-devel llvm-devel libpng-devel - ``` - * It's also important that you install the developer tools and libraries - ``` - sudo dnf groupinstall "Development Tools" "Development Libraries" - ``` -#### Setup -Clone and initialize the Dusklight repository + +### Windows + +* Install [CMake 3.25+](https://cmake.org) by searching `CMake Tools` in Visual Studio +* Install Python 3 from the [Microsoft Store](https://go.microsoft.com/fwlink?linkID=2082640) and verify it's added to `%PATH%` by typing `python` in `cmd`. + +Recommended IDEs: + +* [Visual Studio 2026 Community](https://www.visualstudio.com/en-us/products/visual-studio-community-vs.aspx). During installation: + * Select `C++ Development` and verify the following packages are included: + * `Windows 11 SDK` + * `CMake Tools` + * `C++ Clang Compiler` + * `C++ Clang-cl` + +### macOS + +* Make sure [Homebrew](https://brew.sh) is installed +* Install [CMake 3.25+](https://cmake.org) + ```sh -git clone --recursive https://github.com/TwilitRealm/dusklight.git -cd dusklight -git pull -git submodule update --init --recursive +brew install cmake ``` -#### Building +* Install Python 3 + +```sh +brew install python@3 +``` + +Recommended IDEs: + +* [Xcode 16.4 or later](https://developer.apple.com/xcode/) +* [Visual Studio Code](https://code.visualstudio.com/download/) +* [CLion](https://www.jetbrains.com/clion/) + +### Linux + +Actively tested on Ubuntu 24.04, Arch Linux & derivatives. + +**Ubuntu 24.04+ packages** + +
+Click to expand + +* Run the following command to install the required dependencies: + +```sh +sudo apt update && sudo apt install -y \ + build-essential \ + clang \ + cmake \ + curl \ + git \ + libasound2-dev \ + libclang-dev \ + libcurl4-openssl-dev \ + libdbus-1-dev \ + libfreetype-dev \ + libglu1-mesa-dev \ + libgtk-3-dev \ + libncurses5-dev \ + libpng-dev \ + libpulse-dev \ + libudev-dev \ + libvulkan-dev \ + libx11-xcb-dev \ + libxcursor-dev \ + libxi-dev \ + libxinerama-dev \ + libxrandr-dev \ + libxss-dev \ + libxtst-dev \ + lld \ + ninja-build \ + python-is-python3 \ + python3 \ + python3-markupsafe \ + zlib1g-dev +``` + +
+
+ +**Arch Linux packages** + +
+Click to expand + +* Run the following command to install the required dependencies: + +```sh +sudo pacman -S --needed \ + alsa-lib \ + base-devel \ + clang \ + cmake \ + freetype2 \ + libpulse \ + libxrandr \ + lld \ + llvm \ + ninja \ + python \ + python-markupsafe \ + vulkan-headers +``` + +
+
+ +**Fedora packages** + +
+Click to expand + +* Run the following command to install the required dependencies: + +```sh +sudo dnf install -y \ + clang-devel \ + cmake \ + libpng-devel \ + llvm-devel \ + ninja-build \ + vulkan-headers +``` + +* It's also important that you install the developer tools and libraries + +```sh +sudo dnf groupinstall \ + "Development Libraries" "Development Tools" +``` + +
+
+ +Recommended IDEs: + +* [CLion](https://www.jetbrains.com/clion/) +* [Visual Studio Code](https://code.visualstudio.com/download/) + +## Building + +* Clone and initialize the Dusklight repository: + +```sh +git clone --recursive https://github.com/TwilitRealm/dusklight.git +git pull +cd dusklight +git submodule update --init --recursive +``` **CLion (Windows / macOS / Linux)** @@ -64,7 +178,8 @@ cmake --build --preset macos-default-relwithdebinfo ``` Alternate presets available: -- `macos-default-debug`: Clang, Debug + +* `macos-default-debug`: Clang, Debug **ninja (Linux)** @@ -74,9 +189,10 @@ cmake --build --preset linux-default-relwithdebinfo ``` Alternate presets available: -- `linux-default-debug`: GCC, Debug -- `linux-clang-relwithdebinfo`: Clang, RelWithDebInfo -- `linux-clang-debug`: Clang, Debug + +* `linux-default-debug`: GCC, Debug +* `linux-clang-relwithdebinfo`: Clang, RelWithDebInfo +* `linux-clang-debug`: Clang, Debug **ninja (Windows)** @@ -86,13 +202,27 @@ cmake --build --preset windows-msvc-relwithdebinfo ``` Alternate presets available: -- `windows-msvc-debug`: MSVC, Debug -- `windows-clang-relwithdebinfo`: Clang-cl, RelWithDebInfo -- `windows-clang-debug`: Clang-cl, Debug -#### Running -Pass the disc image as a positional argument. Supported formats: ISO (GCM), RVZ, WIA, WBFS, CISO, GCZ +* `windows-msvc-debug`: MSVC, Debug +* `windows-clang-relwithdebinfo`: Clang-cl, RelWithDebInfo +* `windows-clang-debug`: Clang-cl, Debug + +## Running + +**Windows / Linux** + +* Pass the disc image as a positional argument using the `--dvd` flag. Supported formats are: ISO (GCM), RVZ, WIA, WBFS, CISO, GCZ + ```sh -build/{preset}/dusklight/path/to/game.rvz +build/{preset}/dusklight --dvd /path/to/game.iso +``` + +**macOS** + +macOS builds an `.app` bundle which contains the executable and all necessary resources. + +* Pass the disc image as a positional argument using the `--dvd` flag. Supported formats are: ISO (GCM), RVZ, WIA, WBFS, CISO, GCZ + +```sh +build/{preset}/Dusklight.app/Contents/MacOS/Dusklight --dvd /path/to/game.iso ``` -If no path is specified, Dusklight defaults to `game.iso` in the current working directory. diff --git a/docs/ios-install-altstore.md b/docs/ios-install-altstore.md index 1eece215c0..5c0f071878 100644 --- a/docs/ios-install-altstore.md +++ b/docs/ios-install-altstore.md @@ -1,46 +1,48 @@ -# Installing Dusklight on iOS via AltStore +# Installing Dusklight on iOS via iloader ## Prerequisites -- Mac with Homebrew installed -- iPhone connected via USB -- Dusklight IPA file (download the latest `Dusklight-vX.X.X-ios-arm64.ipa` from the [releases page](https://github.com/TwilitRealm/dusk/releases)) -- Game disc - `GZ2E01` (Gamecube USA) or `GZ2PE01` (Gamecube PAL) +- A Windows, Linux, or macOS device +- iOS device connected to computer via USB +- Dusklight IPA file (download the latest `Dusklight-vX.X.X-ios-arm64.ipa` from the [releases page](https://github.com/TwilitRealm/dusklight/releases)) +- Legally acquired game disc - `GZ2E01` (Gamecube USA) or `GZ2PE01` (Gamecube PAL) -## 1. Install AltServer +## 1. Install iloader -```sh -brew install altserver -open -a AltServer -``` - -AltServer will appear in your menu bar. +- Executable bundles can be installed from [iloader's main page](https://iloader.app/) or [their GitHub](https://github.com/nab138/iloader) for Windows, Linux, and macOS. +- Windows WILL require iTunes to be installed +- Linux WILL require usbmuxd to be installed, this is installed by default in most distros though ## 2. Enable Developer Mode (iOS 16+) - On your iPhone, go to **Settings > Privacy & Security > Developer Mode** -- Toggle it on and restart when prompted +- Toggle it on, put in your device passcode, and restart when prompted -## 3. Install AltStore on Your iPhone +## 3. Install Dusklight on Your iPhone -- Click AltServer in the menu bar -- Click **Install AltStore > [Your iPhone]** -- Enter your Apple ID credentials when prompted -- On your iPhone, go to **Settings > General > VPN & Device Management** -- Tap your Apple ID under "Developer App" and tap **Trust** +1. Sign into your Apple ID (this is required for registering app IDs, it is sent securely directly to Apple and not stored by iloader) + * You may be prompted to put in a code from your iOS device if you have 2FA enabled, do so +2. Plug in your iOS device via USB into your PC. If you're missing a dependency, an error pop-up will tell you to install it + * You will need to hit `Refresh` after plugging it in at this stage so that it can be detected, it does not automatically refresh +3. Leave settings unchanged (the Anisette server should stay Sidestore (.io)) + 3.(a) Installing SideStore directly is not required, but provides you a way to install Dusklight on your phone without being plugged into a computer later +4. Press `Import IPA` and choose your downloaded `Dusklight-v.X.X.X-ios-arm64.ipa`, it will begin installing on your device + +**NOTE:** *At various stages, you may be prompted to trust your device, do so* + +## 3. Getting Dusklight trusted +When installing sideloaded iOS applications, at first you will need to manually trust the app due to Apple's security policies +* Go to **Settings > General > VPN & Device Management** +* Tap the Apple ID you signed into iloader with under "Developer App" and tap **Trust** +* Tap **Allow** on the pop-up ## 4. Copy Files to Your iPhone -Transfer the IPA and game disc to your iPhone so they're accessible in the Files app. A few ways to do this: +Transfer the game disc (and optionally, the Dusklight IPA) to your iPhone so they are accessible in the Files app. A few ways to do this: - **AirDrop** - Right-click the files on your Mac and choose Share > AirDrop - **iCloud Drive** - Place files in iCloud Drive on your Mac and they'll sync to Files on your iPhone - **USB transfer** - Connect your iPhone and drag files via Finder's sidebar - **Cloud storage** - Upload to Google Drive, Dropbox, etc. and download on your iPhone -## 5. Install via AltStore - -- Open **AltStore** on your iPhone -- Go to the **My Apps** tab -- Tap the **+** button (top left) -- Open the **Files** app and select the `.ipa` file \ No newline at end of file +You may now use Dusklight on iOS and iPadOS! \ No newline at end of file diff --git a/extern/aurora b/extern/aurora index ff9ca7170c..10006618ee 160000 --- a/extern/aurora +++ b/extern/aurora @@ -1 +1 @@ -Subproject commit ff9ca7170cc840b1fbb5c7015b2fa1966055a04d +Subproject commit 10006618ee493f248b8597e4dfa1d2871d76a1d9 diff --git a/files.cmake b/files.cmake index 47527da735..eb8c62bee5 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 @@ -1508,6 +1508,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 @@ -1522,6 +1524,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..29c99b10a5 100644 --- a/flake.nix +++ b/flake.nix @@ -4,101 +4,216 @@ }; 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: { + 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 = '' + 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); + }); }; } diff --git a/include/d/d_meter_button.h b/include/d/d_meter_button.h index 0f05d32440..2d9a4bb5d8 100644 --- a/include/d/d_meter_button.h +++ b/include/d/d_meter_button.h @@ -1,6 +1,7 @@ #ifndef D_METER_D_METER_BUTTON_H #define D_METER_D_METER_BUTTON_H +#include "global.h" #include "JSystem/J2DGraph/J2DScreen.h" #include "JSystem/J2DGraph/J2DTextBox.h" #include "d/d_drawlist.h" @@ -194,7 +195,7 @@ public: /* 0x0FC */ CPaneMgr* field_0x0fc[4]; /* 0x10C */ JKRHeap* mpHeap; /* 0x110 */ void* mpFishingTex; - /* 0x114 */ char mButtonText[2][15]; + /* 0x114 */ char mButtonText[2][DUSK_IF_ELSE(32, 15)]; /* 0x132 */ u8 field_0x132[0x134 - 0x132]; /* 0x134 */ f32 field_0x134; /* 0x138 */ f32 field_0x138; 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..c32cd1e22b 100644 --- a/include/dusk/autosave.h +++ b/include/dusk/autosave.h @@ -5,6 +5,7 @@ #include #include +#include void noAutoSave(); void triggerAutoSave(); @@ -13,5 +14,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/endian.h b/include/dusk/endian.h index 0fec59411f..d16d0aa80c 100644 --- a/include/dusk/endian.h +++ b/include/dusk/endian.h @@ -227,6 +227,28 @@ struct BE { } }; +typedef f32 Mtx23[2][3]; +template <> +struct BE { + BE contents[2][3]; + + auto& operator[](int x) { + return contents[x]; + } + + auto& operator[](int x) const { + return contents[x]; + } + + void to_host(Mtx23& mtx) const { + for (int i = 0; i < 2; i++) { + for (int j = 0; j < 3; j++) { + mtx[i][j] = contents[i][j]; + } + } + } +}; + template void be_swap(T& val) { val = BE::swap(val); diff --git a/include/dusk/frame_interpolation.h b/include/dusk/frame_interpolation.h index 8c19e7e59b..5f2f3d134b 100644 --- a/include/dusk/frame_interpolation.h +++ b/include/dusk/frame_interpolation.h @@ -4,6 +4,7 @@ #include #include #include +#include "settings.h" class camera_process_class; class view_class; @@ -17,7 +18,8 @@ void ensure_initialized(); void begin_record(); void end_record(); void begin_sim_tick(); -void begin_frame(bool enabled, bool is_sim_frame, float step); +uint64_t sim_tick_seq(); +void begin_frame(FrameInterpMode mode, bool is_sim_frame, float step); void interpolate(); float get_interpolation_step(); 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 a8aed9f313..0d85b714a5 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 { @@ -13,6 +15,11 @@ enum class BloomMode : int { Dusk = 2, }; +enum class Resampler : int { + Bilinear = 0, + Area = 1, +}; + enum class GameLanguage : u8 { English = OS_LANGUAGE_ENGLISH, German = OS_LANGUAGE_GERMAN, @@ -32,6 +39,12 @@ enum class GyroMode : u8 { Mouse = 1, }; +enum class FrameInterpMode : u8 { + Off = 0, + Capped = 1, + Unlimited = 2, +}; + namespace config { template <> struct ConfigEnumRange { @@ -39,6 +52,12 @@ struct ConfigEnumRange { static constexpr auto max = BloomMode::Dusk; }; +template <> +struct ConfigEnumRange { + static constexpr auto min = Resampler::Bilinear; + static constexpr auto max = Resampler::Area; +}; + template <> struct ConfigEnumRange { static constexpr auto min = GameLanguage::English; @@ -56,7 +75,13 @@ struct ConfigEnumRange { static constexpr auto min = GyroMode::Sensor; static constexpr auto max = GyroMode::Mouse; }; -} + +template <> +struct ConfigEnumRange { + static constexpr auto min = FrameInterpMode::Off; + static constexpr auto max = FrameInterpMode::Unlimited; +}; +} // namespace config // Persistent user settings @@ -70,6 +95,7 @@ struct UserSettings { ConfigVar lockAspectRatio; ConfigVar enableFpsOverlay; ConfigVar fpsOverlayCorner; + ConfigVar maxFrameRate; } video; struct { @@ -115,14 +141,17 @@ struct UserSettings { ConfigVar enableLinkDollRotation; ConfigVar enableAchievementToasts; ConfigVar enableControllerToasts; + ConfigVar enableDiscordPresence; // Graphics ConfigVar bloomMode; ConfigVar bloomMultiplier; ConfigVar disableWaterRefraction; - ConfigVar enableFrameInterpolation; + ConfigVar enableTextureReplacements; + ConfigVar enableFrameInterpolation; ConfigVar internalResolutionScale; ConfigVar shadowResolutionMultiplier; + ConfigVar resampler; ConfigVar enableDepthOfField; ConfigVar enableMapBackground; ConfigVar disableCutscenePillarboxing; @@ -155,6 +184,7 @@ struct UserSettings { // Cheats ConfigVar infiniteHearts; ConfigVar infiniteArrows; + ConfigVar infiniteSeeds; ConfigVar infiniteBombs; ConfigVar infiniteOil; ConfigVar infiniteOxygen; @@ -168,12 +198,14 @@ struct UserSettings { ConfigVar fastRoll; ConfigVar fastSpinner; ConfigVar freeMagicArmor; + ConfigVar invincibleEnemies; // Technical ConfigVar restoreWiiGlitches; // Controls ConfigVar enableTurboKeybind; + ConfigVar enableResetKeybind; // Tools ConfigVar speedrunMode; @@ -181,6 +213,8 @@ struct UserSettings { ConfigVar showSpeedrunRTATimer; ConfigVar recordingMode; ConfigVar removeQuestMapMarkers; + ConfigVar showInputViewer; + ConfigVar showInputViewerGyro; } game; struct { @@ -194,6 +228,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/include/dusk/time.h b/include/dusk/time.h index c43437f639..ead946188b 100644 --- a/include/dusk/time.h +++ b/include/dusk/time.h @@ -17,16 +17,24 @@ #include #include #endif +#ifdef __APPLE__ +#include +#if defined(__x86_64__) || defined(__i386__) +#include +#endif +#endif class Limiter { public: using duration_t = Uint64; - void Reset() { m_oldTime = SDL_GetTicksNS(); } + void Reset() { + m_oldTime = SDL_GetTicksNS(); + } - void Sleep(duration_t targetFrameTime) { + duration_t Sleep(duration_t targetFrameTime) { if (targetFrameTime == 0) { - return; + return 0; } const Uint64 start = SDL_GetTicksNS(); @@ -41,6 +49,8 @@ public: } } Reset(); + + return adjustedSleepTime; } duration_t SleepTime(duration_t targetFrameTime) { @@ -74,7 +84,6 @@ private: if (!initialized || numSleeps++ % 1000 == 0) { LARGE_INTEGER freq; if (QueryPerformanceFrequency(&freq) == 0) { - DuskLog.warn("QueryPerformanceFrequency failed: {}", GetLastError()); return; } countPerNs = static_cast(freq.QuadPart) / 1e9; @@ -98,6 +107,33 @@ private: #endif } while (current.QuadPart - start.QuadPart < ticksToWait); } +#elif defined (__APPLE__) + void NanoSleep(const duration_t duration) { + // Hybrid approach using Apple Mach + uint64_t start_mach = mach_absolute_time(); + + mach_timebase_info_data_t timebase_info; + mach_timebase_info(&timebase_info); + + uint64_t total_mach_ticks = (duration * timebase_info.denom) / timebase_info.numer; + uint64_t target_mach = start_mach + total_mach_ticks; + + uint64_t buffer_ns = 2'000'000ULL; + uint64_t buffer_mach_ticks = (buffer_ns * timebase_info.denom) / timebase_info.numer; + + if (total_mach_ticks > buffer_mach_ticks) { + uint64_t sleep_until_mach = target_mach - buffer_mach_ticks; + mach_wait_until(sleep_until_mach); + } + + while (mach_absolute_time() < target_mach) { +#if defined(__aarch64__) || defined(__arm__) + asm volatile("yield" ::: "memory"); // Hardware hint, not a scheduler hint. +#else + _mm_pause(); +#endif + } + } #else void NanoSleep(const duration_t duration) { SDL_DelayPrecise(duration); } #endif diff --git a/include/f_op/f_op_actor_mng.h b/include/f_op/f_op_actor_mng.h index 5b11f0e57a..76a9f8517c 100644 --- a/include/f_op/f_op_actor_mng.h +++ b/include/f_op/f_op_actor_mng.h @@ -108,7 +108,7 @@ struct fopAcM_search_prm { struct fOpAcm_HIO_entry_c : public mDoHIO_entry_c { virtual ~fOpAcm_HIO_entry_c() {} - #if DEBUG + #if DEBUG && !TARGET_PC void removeHIO(const fopAc_ac_c* i_this) { removeHIO(*i_this); } void removeHIO(const fopAc_ac_c& i_this) { removeHIO(i_this.base); } void removeHIO(const fopEn_enemy_c& i_this) { removeHIO(i_this.base); } diff --git a/libs/JSystem/include/JSystem/J2DGraph/J2DTevs.h b/libs/JSystem/include/JSystem/J2DGraph/J2DTevs.h index 2d5f3ca2d6..ea81bc869f 100644 --- a/libs/JSystem/include/JSystem/J2DGraph/J2DTevs.h +++ b/libs/JSystem/include/JSystem/J2DGraph/J2DTevs.h @@ -116,7 +116,7 @@ private: * */ struct J2DIndTexMtxInfo { - /* 0x00 */ Mtx23 mMtx; + /* 0x00 */ BE(Mtx23) mMtx; /* 0x18 */ s8 mScaleExp; J2DIndTexMtxInfo& operator=(const J2DIndTexMtxInfo& other) { diff --git a/libs/JSystem/include/JSystem/JAudio2/JASHeapCtrl.h b/libs/JSystem/include/JSystem/JAudio2/JASHeapCtrl.h index 7a3ae12470..996fef6fef 100644 --- a/libs/JSystem/include/JSystem/JAudio2/JASHeapCtrl.h +++ b/libs/JSystem/include/JSystem/JAudio2/JASHeapCtrl.h @@ -287,28 +287,28 @@ template class JASPoolAllocObject { public: #if TARGET_PC - static void* operator new(size_t n, JKRHeapToken) { + static void* operator new(size_t n, JKRHeapToken) IF_DUSK(noexcept) { return operator new(n); } #endif - static void* operator new(size_t n) { + static void* operator new(size_t n) IF_DUSK(noexcept) { #if PLATFORM_GCN JASMemPool& memPool_ = getMemPool_(); #endif return memPool_.alloc(n); } - static void* operator new(size_t n, void* ptr) { + static void* operator new(size_t n, void* ptr) IF_DUSK(noexcept) { return ptr; } #if TARGET_PC - static void operator delete(void* ptr, size_t n, JKRHeapToken) { + static void operator delete(void* ptr, size_t n, JKRHeapToken) IF_DUSK(noexcept) { operator delete(ptr, n); } #endif - static void operator delete(void* ptr, size_t n) { + static void operator delete(void* ptr, size_t n) IF_DUSK(noexcept) { #if PLATFORM_GCN JASMemPool& memPool_ = getMemPool_(); #endif @@ -402,28 +402,28 @@ template class JASPoolAllocObject_MultiThreaded { public: #if TARGET_PC - static void* operator new(size_t n, JKRHeapToken) { + static void* operator new(size_t n, JKRHeapToken) IF_DUSK(noexcept) { return operator new(n); } #endif - static void* operator new(size_t n) { + static void* operator new(size_t n) IF_DUSK(noexcept) { #if PLATFORM_GCN JASMemPool_MultiThreaded& memPool_ = getMemPool(); #endif return memPool_.alloc(n); } - static void* operator new(size_t n, void* ptr) { + static void* operator new(size_t n, void* ptr) IF_DUSK(noexcept) { return ptr; } #if TARGET_PC - static void operator delete(void* ptr, size_t n, JKRHeapToken) { + static void operator delete(void* ptr, size_t n, JKRHeapToken) IF_DUSK(noexcept) { return operator delete(ptr, n); } #endif - static void operator delete(void* ptr, size_t n) { + static void operator delete(void* ptr, size_t n) IF_DUSK(noexcept) { #if PLATFORM_GCN JASMemPool_MultiThreaded& memPool_ = getMemPool(); #endif diff --git a/libs/JSystem/include/JSystem/JKernel/JKRHeap.h b/libs/JSystem/include/JSystem/JKernel/JKRHeap.h index d3710bd5df..d1b27a0fd0 100644 --- a/libs/JSystem/include/JSystem/JKernel/JKRHeap.h +++ b/libs/JSystem/include/JSystem/JKernel/JKRHeap.h @@ -237,11 +237,11 @@ enum class JKRHeapToken { Dummy }; -inline void* operator new(size_t, JKRHeapToken, void* where) { +inline void* operator new(size_t, JKRHeapToken, void* where) noexcept { return where; } -inline void* operator new[](size_t, JKRHeapToken, void* where) { +inline void* operator new[](size_t, JKRHeapToken, void* where) noexcept { return where; } @@ -264,21 +264,21 @@ inline void* operator new[](size_t, JKRHeapToken, void* where) { #define JKR_HEAP_TOKEN_PARAM #endif -void* operator new(size_t size JKR_HEAP_TOKEN_PARAM); -void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, int alignment); -void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, JKRHeap* heap, int alignment); +void* operator new(size_t size JKR_HEAP_TOKEN_PARAM) IF_DUSK(noexcept); +void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, int alignment) IF_DUSK(noexcept); +void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, JKRHeap* heap, int alignment) IF_DUSK(noexcept); // On PC, these new[] overloads are only used to catch usages of JKR_NEW with []. -void* operator new[](size_t size JKR_HEAP_TOKEN_PARAM); -void* operator new[](size_t size JKR_HEAP_TOKEN_PARAM, int alignment); -void* operator new[](size_t size JKR_HEAP_TOKEN_PARAM, JKRHeap* heap, int alignment); +void* operator new[](size_t size JKR_HEAP_TOKEN_PARAM) IF_DUSK(noexcept); +void* operator new[](size_t size JKR_HEAP_TOKEN_PARAM, int alignment) IF_DUSK(noexcept); +void* operator new[](size_t size JKR_HEAP_TOKEN_PARAM, JKRHeap* heap, int alignment) IF_DUSK(noexcept); -void operator delete(void* ptr JKR_HEAP_TOKEN_PARAM); -void operator delete[](void* ptr JKR_HEAP_TOKEN_PARAM); +void operator delete(void* ptr JKR_HEAP_TOKEN_PARAM) IF_DUSK(noexcept); +void operator delete[](void* ptr JKR_HEAP_TOKEN_PARAM) IF_DUSK(noexcept); #if TARGET_PC template -void jkrDelete(T* ptr) { +void jkrDelete(T* ptr) IF_DUSK(noexcept) { if (ptr == nullptr) { return; } @@ -298,7 +298,7 @@ void jkrDelete(T* ptr) { } template<> -inline void jkrDelete(void* ptr) { +inline void jkrDelete(void* ptr) IF_DUSK(noexcept) { if (ptr == nullptr) { return; } @@ -322,7 +322,7 @@ constexpr bool newArgsHasCustomAlignment() { } template -T* jkrNewArray(size_t count, std::in_place_type_t, Args&&... args) { +T* jkrNewArray(size_t count, std::in_place_type_t, Args&&... args) IF_DUSK(noexcept) { size_t allocSize = count * sizeof(T); if constexpr (!std::is_trivially_destructible()) { static_assert( @@ -333,6 +333,10 @@ T* jkrNewArray(size_t count, std::in_place_type_t, Args&&... args) { } void* ptr = operator new(allocSize, JKRHeapToken::Dummy, args...); + if (!ptr) { + return nullptr; + } + T* dataPtr; if constexpr (!std::is_trivially_destructible()) { auto length = static_cast(ptr); @@ -352,7 +356,7 @@ T* jkrNewArray(size_t count, std::in_place_type_t, Args&&... args) { } template -void jkrDeleteArray(T* pointer) { +void jkrDeleteArray(T* pointer) IF_DUSK(noexcept) { if (pointer == nullptr) { return; } @@ -372,7 +376,7 @@ void jkrDeleteArray(T* pointer) { } template<> -inline void jkrDeleteArray(void* pointer) { +inline void jkrDeleteArray(void* pointer) IF_DUSK(noexcept) { if (pointer == nullptr) { return; } diff --git a/libs/JSystem/src/J2DGraph/J2DTevs.cpp b/libs/JSystem/src/J2DGraph/J2DTevs.cpp index 8daf82872c..33a65dd273 100644 --- a/libs/JSystem/src/J2DGraph/J2DTevs.cpp +++ b/libs/JSystem/src/J2DGraph/J2DTevs.cpp @@ -68,8 +68,14 @@ void J2DIndTevStage::load(u8 tevStage) { } void J2DIndTexMtx::load(u8 indTexMtx) { +#ifdef TARGET_PC + Mtx23 mtx; + mIndTexMtxInfo.mMtx.to_host(mtx); + GXSetIndTexMtx((GXIndTexMtxID)(GX_ITM_0 + indTexMtx), mtx, mIndTexMtxInfo.mScaleExp); +#else GXSetIndTexMtx((GXIndTexMtxID)(GX_ITM_0 + indTexMtx), mIndTexMtxInfo.mMtx, mIndTexMtxInfo.mScaleExp); +#endif } void J2DIndTexCoordScale::load(u8 indTexStage) { diff --git a/libs/JSystem/src/JFramework/JFWDisplay.cpp b/libs/JSystem/src/JFramework/JFWDisplay.cpp index b8054e2130..2d4c9c7126 100644 --- a/libs/JSystem/src/JFramework/JFWDisplay.cpp +++ b/libs/JSystem/src/JFramework/JFWDisplay.cpp @@ -370,28 +370,28 @@ constexpr auto FRAME_PERIOD = std::chrono::duration_cast(sleepTime) / static_cast(targetNs)); - limiter.Sleep(targetNs); } #endif static void waitForTick(u32 p1, u16 p2) { #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation && !dusk::getTransientSettings().skipFrameRateLimit) { - dusk::frameUsagePct = 0.f; - return; + static Limiter limiter; + + if (dusk::frame_interp::is_enabled() && !dusk::getTransientSettings().skipFrameRateLimit) { + dusk::frameUsagePct = 0.f; + return; } + if (dusk::getTransientSettings().skipFrameRateLimit) { p1 = OS_TIMER_CLOCK / 120; } - #if TARGET_PC if (fopOvlpM_IsPeek() && dusk::getTransientSettings().stateShareLoadActive) { return; } - #endif ZoneScopedC(tracy::Color::DimGray); #endif diff --git a/libs/JSystem/src/JKernel/JKRHeap.cpp b/libs/JSystem/src/JKernel/JKRHeap.cpp index ce1899a4ba..c4849437ac 100644 --- a/libs/JSystem/src/JKernel/JKRHeap.cpp +++ b/libs/JSystem/src/JKernel/JKRHeap.cpp @@ -559,7 +559,7 @@ void* operator new(size_t size) { return JKRHeap::alloc(size, 4, NULL); } #else -void* operator new(size_t size JKR_HEAP_TOKEN_PARAM) { +void* operator new(size_t size JKR_HEAP_TOKEN_PARAM) noexcept { if (sCurrentHeap == NULL) { return fallback_alloc(size, 0, false); } @@ -576,7 +576,7 @@ void* operator new(size_t size, int alignment) { return JKRHeap::alloc(size, alignment, NULL); } #else -void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, int alignment) { +void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, int alignment) noexcept { void* mem = JKRHeap::alloc(size, alignment, nullptr); if (mem == nullptr) { return fallback_alloc(size, abs(alignment), true); @@ -585,7 +585,7 @@ void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, int alignment) { } #endif -void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, JKRHeap* heap, int alignment) { +void* operator new(size_t size JKR_HEAP_TOKEN_PARAM, JKRHeap* heap, int alignment) IF_DUSK(noexcept) { void* mem = JKRHeap::alloc(size, alignment, heap); #if TARGET_PC if (mem == nullptr) { @@ -600,7 +600,7 @@ void* operator new[](size_t size) { return JKRHeap::alloc(size, 4, NULL); } #else -void* operator new[](size_t JKR_HEAP_TOKEN_PARAM) { +void* operator new[](size_t JKR_HEAP_TOKEN_PARAM) IF_DUSK(noexcept) { OSPanic(__FILE__, __LINE__, "Allocation should go through JKR_NEW_ARRAY instead"); } #endif @@ -610,12 +610,12 @@ void* operator new[](size_t size, int alignment) { return JKRHeap::alloc(size, alignment, NULL); } #else -void* operator new[](size_t JKR_HEAP_TOKEN_PARAM, int) { +void* operator new[](size_t JKR_HEAP_TOKEN_PARAM, int) IF_DUSK(noexcept) { OSPanic(__FILE__, __LINE__, "Allocation should go through JKR_NEW_ARRAY instead"); } #endif -void* operator new[](size_t JKR_HEAP_TOKEN_PARAM, JKRHeap*, int) { +void* operator new[](size_t JKR_HEAP_TOKEN_PARAM, JKRHeap*, int) IF_DUSK(noexcept) { OSPanic(__FILE__, __LINE__, "Allocation should go through JKR_NEW_ARRAY instead"); } @@ -624,7 +624,7 @@ void operator delete(void* ptr) { JKRHeap::free(ptr, NULL); } #else -void operator delete(void* ptr JKR_HEAP_TOKEN_PARAM) { +void operator delete(void* ptr JKR_HEAP_TOKEN_PARAM) IF_DUSK(noexcept) { if (ptr == NULL) return; JKRHeap* heap = JKRHeap::findFromRoot(ptr); @@ -645,7 +645,7 @@ void operator delete[](void* ptr) { JKRHeap::free(ptr, NULL); } #else -void operator delete[](void* ptr JKR_HEAP_TOKEN_PARAM) { +void operator delete[](void* ptr JKR_HEAP_TOKEN_PARAM) IF_DUSK(noexcept) { if (ptr == NULL) return; JKRHeap* heap = JKRHeap::findFromRoot(ptr); diff --git a/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp b/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp index ee99429ee2..864dec39fc 100644 --- a/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp +++ b/libs/JSystem/src/JStudio/JStudio/jstudio-object.cpp @@ -655,7 +655,7 @@ value_or_fun: value: #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation && u <= 5 && + if (dusk::frame_interp::is_enabled() && u <= 5 && (operation == data::UNK_0x2 || operation == data::UNK_0x3 || operation == data::UNK_0x12)) { dusk::frame_interp::request_presentation_sync(); @@ -666,7 +666,7 @@ value: value_n: #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation && + if (dusk::frame_interp::is_enabled() && (pN == TAdaptor_camera::sauVariableValue_3_POSITION_XYZ || pN == TAdaptor_camera::sauVariableValue_3_TARGET_POSITION_XYZ) && (operation == data::UNK_0x2 || operation == data::UNK_0x3 || operation == data::UNK_0x12)) { 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/platforms/macos/Info.plist.in b/platforms/macos/Info.plist.in index 46c9641c17..7f29358033 100644 --- a/platforms/macos/Info.plist.in +++ b/platforms/macos/Info.plist.in @@ -28,7 +28,9 @@ ${MACOSX_BUNDLE_SHORT_VERSION_STRING} NSHighResolutionCapable + LSApplicationCategoryType + public.app-category.adventure-games LSSupportsGameMode - + diff --git a/res/rml/overlay.rcss b/res/rml/overlay.rcss index 20e084665b..8927e1a01c 100644 --- a/res/rml/overlay.rcss +++ b/res/rml/overlay.rcss @@ -58,6 +58,10 @@ toast:active { background-color: rgba(45, 43, 26, 80%); }*/ +b { + font-weight: bold; +} + toast heading { display: flex; gap: 18dp; diff --git a/res/rml/window.rcss b/res/rml/window.rcss index 99b3753d68..ab338e97c7 100644 --- a/res/rml/window.rcss +++ b/res/rml/window.rcss @@ -14,6 +14,10 @@ body { color: #E0DBC8; } +b { + font-weight: bold; +} + window { display: flex; flex-flow: column; diff --git a/src/d/actor/d_a_alink.cpp b/src/d/actor/d_a_alink.cpp index 0e0f0d06c7..e2fe7a890c 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); @@ -5987,7 +5990,7 @@ void daAlink_c::setItemMatrix(int param_0) { mDoMtx_stack_c::XrotS(-0x8000); #ifdef TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { Mtx boot_mtx; mDoMtx_concat(mpLinkModel->getAnmMtx(0x18), mDoMtx_stack_c::get(), boot_mtx); mpLinkBootModels[1]->setAnmMtx(1, boot_mtx); @@ -7559,12 +7562,7 @@ void daAlink_c::setBlendMoveAnime(f32 i_morf) { f32 sp2C; f32 sp28 = mpHIO->mMove.m.mFootPositionRatio; BOOL sp24 = checkEventRun(); - BOOL sp20 = checkBootsMoveAnime(1); -#if TARGET_PC - if (dusk::getSettings().game.enableFastIronBoots) { - sp20 = FALSE; - } -#endif + BOOL sp20 = checkBootsMoveAnime(1) IF_DUSK(&& !dusk::getSettings().game.enableFastIronBoots); f32 var_f29; @@ -8077,7 +8075,7 @@ void daAlink_c::setBlendAtnBackMoveAnime(f32 i_morf) { daAlink_ANM var_r27; daAlink_ANM var_r29; - if (checkBootsMoveAnime(1)) { + if (checkBootsMoveAnime(1) IF_DUSK(&& !dusk::getSettings().game.enableFastIronBoots)) { mMaxSpeed = mpHIO->mAtnMove.m.mMaxBackwardsSpeed; var_f27 = mpHIO->mAtnMove.m.mMinBackWalkFrame; var_f31 = mpHIO->mAtnMove.m.mBackWalkChangeRate; @@ -9363,6 +9361,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; } @@ -19763,7 +19767,7 @@ int daAlink_c::draw() { dComIfGd_getOpaListDark()->entryImm(mpHookChain, 0); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation && + if (dusk::frame_interp::is_enabled() && mEquipItem == dItemNo_IRONBALL_e && mIronBallChainPos != NULL && mIronBallChainAngle != NULL) { 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/actor/d_a_b_gnd.cpp b/src/d/actor/d_a_b_gnd.cpp index f5b03be1b1..d96e6314af 100644 --- a/src/d/actor/d_a_b_gnd.cpp +++ b/src/d/actor/d_a_b_gnd.cpp @@ -397,7 +397,7 @@ static int daB_GND_Draw(b_gnd_class* i_this) { i_this->field_0x21e8.update(2, l_color, &a_this->tevStr); dComIfGd_set3DlineMat(&i_this->field_0x21e8); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->mReinsInterpCurrValid) { memcpy(i_this->mReinsInterpPrev, i_this->mReinsInterpCurr, sizeof(i_this->mReinsInterpCurr)); memcpy(i_this->mReinsTexInterpPrev, i_this->mReinsTexInterpCurr, sizeof(i_this->mReinsTexInterpCurr)); diff --git a/src/d/actor/d_a_e_arrow.cpp b/src/d/actor/d_a_e_arrow.cpp index 11746e3a76..155f36e2fb 100644 --- a/src/d/actor/d_a_e_arrow.cpp +++ b/src/d/actor/d_a_e_arrow.cpp @@ -674,7 +674,29 @@ static int daE_ARROW_Create(fopAc_ac_c* i_this) { } int phase_state = dComIfG_resLoad(&a_this->mPhase, a_this->mResName); +#if TARGET_PC + static int s_create_frames = 0; + static bool s_first_arrow = true; + static fpc_ProcID s_last_scene_id = 0; + + s_create_frames++; + fpc_ProcID cur_scene_id = dStage_roomControl_c::getProcID(); + + if (cur_scene_id != s_last_scene_id) { + s_first_arrow = true; + s_last_scene_id = cur_scene_id; + } + + if (phase_state == cPhs_COMPLEATE_e && s_first_arrow && s_create_frames < 4) { + return cPhs_INIT_e; + } +#endif if (phase_state == cPhs_COMPLEATE_e) { +#if TARGET_PC + s_create_frames = 0; + s_first_arrow = false; +#endif + a_this->mArrowType = fopAcM_GetParam(a_this) & 0xF; a_this->mFlags = fopAcM_GetParam(a_this) & 0xF0; diff --git a/src/d/actor/d_a_e_db.cpp b/src/d/actor/d_a_e_db.cpp index 4d60fbd6f6..6a4ed52aba 100644 --- a/src/d/actor/d_a_e_db.cpp +++ b/src/d/actor/d_a_e_db.cpp @@ -116,7 +116,7 @@ static int daE_DB_Draw(e_db_class* i_this) { i_this->stalkLine.update(12, l_color, &actor->tevStr); dComIfGd_set3DlineMat(&i_this->stalkLine); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->mStalkLineInterpCurrValid) { memcpy(i_this->mStalkLineInterpPrev, i_this->mStalkLineInterpCurr, sizeof(i_this->mStalkLineInterpCurr)); i_this->mStalkLineInterpPrevValid = true; diff --git a/src/d/actor/d_a_e_hb.cpp b/src/d/actor/d_a_e_hb.cpp index 8ed9058c6f..2b13b8a153 100644 --- a/src/d/actor/d_a_e_hb.cpp +++ b/src/d/actor/d_a_e_hb.cpp @@ -103,7 +103,7 @@ static int daE_HB_Draw(e_hb_class* i_this) { i_this->stalkLine.update(12, l_color, &actor->tevStr); dComIfGd_set3DlineMat(&i_this->stalkLine); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->mStalkLineInterpCurrValid) { memcpy(i_this->mStalkLineInterpPrev, i_this->mStalkLineInterpCurr, sizeof(i_this->mStalkLineInterpCurr)); i_this->mStalkLineInterpPrevValid = true; diff --git a/src/d/actor/d_a_e_mb.cpp b/src/d/actor/d_a_e_mb.cpp index aa77a99612..31352f92cf 100644 --- a/src/d/actor/d_a_e_mb.cpp +++ b/src/d/actor/d_a_e_mb.cpp @@ -105,7 +105,7 @@ static int daE_MB_Draw(e_mb_class* i_this) { i_this->mRopeMat.update(16, l_color, &a_this->tevStr); dComIfGd_set3DlineMat(&i_this->mRopeMat); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->mRopeInterpCurrValid) { memcpy(i_this->mRopeInterpPrev, i_this->mRopeInterpCurr, sizeof(i_this->mRopeInterpCurr)); i_this->mRopeInterpPrevValid = true; diff --git a/src/d/actor/d_a_e_mk.cpp b/src/d/actor/d_a_e_mk.cpp index c3a684ea8e..8099419410 100644 --- a/src/d/actor/d_a_e_mk.cpp +++ b/src/d/actor/d_a_e_mk.cpp @@ -157,6 +157,21 @@ static void* s_h_sub(void* i_actor, void* i_data) { return NULL; } +#if TARGET_PC +static void sort_target_info_by_id() { + for (int i = 1; i < target_info_count; i++) { + void* key = target_info[i]; + fpc_ProcID key_id = fopAcM_GetID(key); + int j = i - 1; + while (j >= 0 && fopAcM_GetID(target_info[j]) > key_id) { + target_info[j + 1] = target_info[j]; + j--; + } + target_info[j + 1] = key; + } +} +#endif + static daPillar_c* search_hasira(e_mk_class* i_this) { fopEn_enemy_c* actor = (fopEn_enemy_c*)&i_this->actor; daPillar_c* pillar_p; @@ -170,6 +185,9 @@ static daPillar_c* search_hasira(e_mk_class* i_this) { if (i_this->firstHasiraFlag == 0) { i_this->firstHasiraFlag++; +#if TARGET_PC + sort_target_info_by_id(); +#endif return (daPillar_c*)target_info[TREG_S(7) + 5]; } diff --git a/src/d/actor/d_a_e_rd.cpp b/src/d/actor/d_a_e_rd.cpp index e540e7dccd..460bc9dd26 100644 --- a/src/d/actor/d_a_e_rd.cpp +++ b/src/d/actor/d_a_e_rd.cpp @@ -7053,6 +7053,12 @@ static int daE_RD_IsDelete(e_rd_class*) { } static int daE_RD_Delete(e_rd_class* i_this) { +#if TARGET_PC + if (boss == i_this) { + boss = NULL; + } +#endif + fopEn_enemy_c* enemy = (fopEn_enemy_c*)&i_this->enemy; fopAcM_RegisterDeleteID(i_this, "E_RD"); diff --git a/src/d/actor/d_a_e_s1.cpp b/src/d/actor/d_a_e_s1.cpp index a5a05fce20..c4455f02d1 100644 --- a/src/d/actor/d_a_e_s1.cpp +++ b/src/d/actor/d_a_e_s1.cpp @@ -117,6 +117,13 @@ static void daE_S1_interp_callback(bool isSimFrame, void* pUserWork) { dst[i] = p0 + (p1 - p0) * alpha; } } + GXColor line_color; + line_color.r = JREG_S(0) + 5; + line_color.g = JREG_S(1) + 10; + line_color.b = JREG_S(2) + 10; + line_color.a = 0xFF; + + i_this->mLineMat.update(16, line_color, &i_this->tevStr); } #endif @@ -154,7 +161,7 @@ static int daE_S1_Draw(e_s1_class* i_this) { dComIfGd_set3DlineMatDark(&i_this->mLineMat); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->mHairInterpCurrValid) { memcpy(i_this->mHairInterpPrev, i_this->mHairInterpCurr, sizeof(i_this->mHairInterpCurr)); i_this->mHairInterpPrevValid = true; diff --git a/src/d/actor/d_a_e_wb.cpp b/src/d/actor/d_a_e_wb.cpp index 47004737cb..a353ea3e87 100644 --- a/src/d/actor/d_a_e_wb.cpp +++ b/src/d/actor/d_a_e_wb.cpp @@ -535,7 +535,7 @@ static int daE_WB_Draw(e_wb_class* i_this) { i_this->himo_tex.update(2, l_color, &actor->tevStr); dComIfGd_set3DlineMat(&i_this->himo_tex); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->himo_interp_curr_valid) { memcpy(i_this->himo_mat_interp_prev, i_this->himo_mat_interp_curr, sizeof(i_this->himo_mat_interp_curr)); memcpy(i_this->himo_tex_interp_prev, i_this->himo_tex_interp_curr, sizeof(i_this->himo_tex_interp_curr)); diff --git a/src/d/actor/d_a_e_yd.cpp b/src/d/actor/d_a_e_yd.cpp index 18809e05ea..a10f47240c 100644 --- a/src/d/actor/d_a_e_yd.cpp +++ b/src/d/actor/d_a_e_yd.cpp @@ -107,7 +107,7 @@ static s32 daE_YD_Draw(e_yd_class* i_this) { i_this->mLineMat.update(12, l_color, &i_this->actor.tevStr); dComIfGd_set3DlineMat(&i_this->mLineMat); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->mLineMatInterpCurrValid) { memcpy(i_this->mLineMatInterpPrev, i_this->mLineMatInterpCurr, sizeof(i_this->mLineMatInterpCurr)); i_this->mLineMatInterpPrevValid = true; diff --git a/src/d/actor/d_a_e_yg.cpp b/src/d/actor/d_a_e_yg.cpp index 44a8aee249..9eae74b1af 100644 --- a/src/d/actor/d_a_e_yg.cpp +++ b/src/d/actor/d_a_e_yg.cpp @@ -139,6 +139,7 @@ static BOOL pl_check(e_yg_class* i_this, f32 i_dist) { #if TARGET_PC static void daE_YG_interp_callback(bool isSimFrame, void* pUserWork) { e_yg_class* i_this = (e_yg_class*)pUserWork; + fopAc_ac_c* actor = (fopAc_ac_c*)&i_this->actor; if (!i_this->mTentacleInterpPrevValid || !i_this->mTentacleInterpCurrValid) { return; } @@ -152,6 +153,13 @@ static void daE_YG_interp_callback(bool isSimFrame, void* pUserWork) { dst[i] = p0 + (p1 - p0) * alpha; } } + GXColor color; + color.r = JREG_S(0) + 20; + color.g = JREG_S(1) + 20; + color.b = JREG_S(2) + 20; + color.a = 0xFF; + + i_this->mLineMat.update(10, color, &actor->tevStr); } #endif @@ -183,7 +191,7 @@ static int daE_YG_Draw(e_yg_class* i_this) { dComIfGd_set3DlineMatDark(&i_this->mLineMat); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->mTentacleInterpCurrValid) { memcpy(i_this->mTentacleInterpPrev, i_this->mTentacleInterpCurr, sizeof(i_this->mTentacleInterpCurr)); i_this->mTentacleInterpPrevValid = true; diff --git a/src/d/actor/d_a_e_yh.cpp b/src/d/actor/d_a_e_yh.cpp index cf9955a7db..bf71e18818 100644 --- a/src/d/actor/d_a_e_yh.cpp +++ b/src/d/actor/d_a_e_yh.cpp @@ -135,7 +135,7 @@ static int daE_YH_Draw(e_yh_class* i_this) { i_this->mLine.update(12, l_color, &a_this->tevStr); dComIfGd_set3DlineMat(&i_this->mLine); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (i_this->mLineInterpCurrValid) { memcpy(i_this->mLineInterpPrev, i_this->mLineInterpCurr, sizeof(i_this->mLineInterpCurr)); i_this->mLineInterpPrevValid = true; diff --git a/src/d/actor/d_a_horse.cpp b/src/d/actor/d_a_horse.cpp index 68112eba47..42f2c9b8ce 100644 --- a/src/d/actor/d_a_horse.cpp +++ b/src/d/actor/d_a_horse.cpp @@ -22,6 +22,7 @@ #if TARGET_PC #include "dusk/dusk.h" +#include "dusk/frame_interpolation.h" namespace { // FRAME INTERP NOTE: Sim tick control point snapshots for interpolation @@ -32,6 +33,7 @@ int s_horseReinSimNumPrev; int s_horseReinSimNumCurr; bool s_horseReinSimPrevValid; bool s_horseReinSimCurrValid; +uint64_t s_horseReinSimRolledSeq; } // namespace #endif @@ -3033,10 +3035,14 @@ void daHorse_c::copyReinPos() { } #if TARGET_PC if (field_0x1204 > 0) { - if (s_horseReinSimCurrValid && s_horseReinSimNumCurr > 0) { - memcpy(s_horseReinSimPrev, s_horseReinSimCurr, s_horseReinSimNumCurr * sizeof(cXyz)); - s_horseReinSimNumPrev = s_horseReinSimNumCurr; - s_horseReinSimPrevValid = true; + const uint64_t simSeq = dusk::frame_interp::sim_tick_seq(); + if (simSeq != s_horseReinSimRolledSeq) { + s_horseReinSimRolledSeq = simSeq; + if (s_horseReinSimCurrValid && s_horseReinSimNumCurr > 0) { + memcpy(s_horseReinSimPrev, s_horseReinSimCurr, s_horseReinSimNumCurr * sizeof(cXyz)); + s_horseReinSimNumPrev = s_horseReinSimNumCurr; + s_horseReinSimPrevValid = true; + } } memcpy(s_horseReinSimCurr, m_reinLine.getPos(0), field_0x1204 * sizeof(cXyz)); s_horseReinSimNumCurr = field_0x1204; @@ -3159,7 +3165,7 @@ void daHorse_c::setReinPosNormalSubstance() { #if TARGET_PC void daHorse_c::lerpControlPoints(f32 alpha) { // FRAME INTERP NOTE: Currently only lerping points for Epona's reins. Need a more global solution. - if (!dusk::getSettings().game.enableFrameInterpolation || !s_horseReinSimPrevValid || !s_horseReinSimCurrValid) { + if (!dusk::frame_interp::is_enabled() || !s_horseReinSimPrevValid || !s_horseReinSimCurrValid) { return; } const int nCurr = s_horseReinSimNumCurr; diff --git a/src/d/actor/d_a_npc_henna.cpp b/src/d/actor/d_a_npc_henna.cpp index 339d5ee82c..40dd9b3091 100644 --- a/src/d/actor/d_a_npc_henna.cpp +++ b/src/d/actor/d_a_npc_henna.cpp @@ -1967,7 +1967,11 @@ static void demo_camera_shop(npc_henna_class* i_this) { i_this->mMsgFlow.init(actor, 0x365, 0, NULL); /* dSv_event_flag_c::KORO2_ALLCLEAR - Fishing - After all stages (8-8) of roll goal game cleared */ dComIfGs_onEventBit(dSv_event_flag_c::saveBitLabels[0x335]); +#if TARGET_PC + dComIfGp_setItemRupeeCount(dComIfGs_getRupeeMax()); +#else dComIfGp_setItemRupeeCount(1000); +#endif } else if ((lbl_82_bss_91 & 0x38) == 0) { i_this->mMsgFlow.init(actor, 0x34f, 0, NULL); /* dSv_event_flag_c::F_0469 - Fishing Pond - Reserved for fishing */ diff --git a/src/d/actor/d_a_obj_drop.cpp b/src/d/actor/d_a_obj_drop.cpp index 18d27840f7..62f3bf9f17 100644 --- a/src/d/actor/d_a_obj_drop.cpp +++ b/src/d/actor/d_a_obj_drop.cpp @@ -299,7 +299,8 @@ int daObjDrop_c::modeParentWait() { #if TARGET_PC static inline BOOL checkGetCargoRide() { - if ((daPy_getPlayerActorClass()->checkCargoCarry() && strcmp(dComIfGp_getStartStageName(), "F_SP112") == 0) || + if (daPy_getPlayerActorClass()->checkCargoCarry() && + strcmp(dComIfGp_getStartStageName(), "F_SP112") == 0 && dComIfGs_isLightDropGetFlag(dComIfGp_getStartStageDarkArea())) { return true; diff --git a/src/d/actor/d_a_obj_fchain.cpp b/src/d/actor/d_a_obj_fchain.cpp index 024b4ecf8b..acd3610b6e 100644 --- a/src/d/actor/d_a_obj_fchain.cpp +++ b/src/d/actor/d_a_obj_fchain.cpp @@ -325,7 +325,7 @@ int daObjFchain_c::draw() { dComIfGd_getOpaListDark()->entryImm(&mShape, 0); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (mChainInterpCurrValid) { memcpy(mChainInterpPrev, mChainInterpCurr, sizeof(mChainInterpCurr)); mChainInterpPrevValid = true; diff --git a/src/d/actor/d_a_obj_klift00.cpp b/src/d/actor/d_a_obj_klift00.cpp index e44cce7467..c7cede3b0e 100644 --- a/src/d/actor/d_a_obj_klift00.cpp +++ b/src/d/actor/d_a_obj_klift00.cpp @@ -493,7 +493,7 @@ int daObjKLift00_c::Draw() { dComIfGd_setList(); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { if (mChainInterpCurrValid) { memcpy(mChainInterpPrev, mChainInterpCurr, mNumChains * sizeof(cXyz)); mChainInterpPrevValid = true; diff --git a/src/d/actor/d_a_title.cpp b/src/d/actor/d_a_title.cpp index a75a96cb2e..5c9e503ea3 100644 --- a/src/d/actor/d_a_title.cpp +++ b/src/d/actor/d_a_title.cpp @@ -170,7 +170,7 @@ int daTitle_c::Execute() { } #ifdef TARGET_PC - if (!dusk::getSettings().game.enableFrameInterpolation) { + if (!dusk::frame_interp::is_enabled()) { #endif dMenu_Collect3D_c::setViewPortOffsetY(0.0f); #ifdef TARGET_PC @@ -354,7 +354,7 @@ void daTitle_c::fastLogoDispInit() { mProcID = 5; #ifdef TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { dusk::frame_interp::request_presentation_sync(); } #endif diff --git a/src/d/d_bright_check.cpp b/src/d/d_bright_check.cpp index 42ed7845cb..ccb04c69e5 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; @@ -142,15 +143,16 @@ void dBrightCheck_c::modeMove() { if (mDoCPd_c::getTrigA(PAD_1) || mDoCPd_c::getTrigStart(PAD_1)) { mDoAud_seStart(Z2SE_ENTER_GAME, NULL, 0, 0); #ifdef TARGET_PC - dusk::speedrun::start(); - if (dusk::getSettings().game.speedrunMode && !dusk::getSettings().game.hideTvSettingsScreen) { // start a new run if a run isn't already in progress if (!dusk::m_speedrunInfo.m_isRunStarted) { dusk::resetForSpeedrunMode(); dusk::m_speedrunInfo.startRun(); + dusk::speedrun::start(); } } + + 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..0de75747f2 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; @@ -10421,7 +10431,7 @@ bool dCamera_c::eventCamera(s32 param_0) { #endif #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { switch (var_r29) { case 3: case 4: @@ -11312,7 +11322,7 @@ static int camera_execute(camera_process_class* i_this) { #ifdef TARGET_PC widezoom_correction(i_this, i_this->mCamera.TrimHeight()); - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { dusk::frame_interp::add_interpolation_callback([](bool _, void* pUserWork) { const auto i_this = static_cast(pUserWork); const auto camera = &i_this->mCamera; 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_com_inf_game.cpp b/src/d/d_com_inf_game.cpp index aa5b5659e9..7e17cb501e 100644 --- a/src/d/d_com_inf_game.cpp +++ b/src/d/d_com_inf_game.cpp @@ -19,6 +19,7 @@ #include "d/d_timer.h" #include "f_op/f_op_msg_mng.h" #include "f_op/f_op_scene_mng.h" +#include "m_Do/m_Do_MemCard.h" #include "m_Do/m_Do_Reset.h" #include "m_Do/m_Do_controller_pad.h" #include "m_Do/m_Do_graphic.h" @@ -1238,6 +1239,13 @@ BOOL dComIfG_resetToOpening(scene_class* i_scene) { } #endif + #ifdef TARGET_PC + if (!mDoMemCd_isCardCommNone()) { + return 0; + } + g_mDoMemCd_control.SaveSync(); + #endif + dComIfG_changeOpeningScene(i_scene, fpcNm_OPENING_SCENE_e); mDoAud_bgmStop(30); mDoAud_resetProcess(); 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_menu_dmap.cpp b/src/d/d_menu_dmap.cpp index b39c9bb287..51cb11cdd1 100644 --- a/src/d/d_menu_dmap.cpp +++ b/src/d/d_menu_dmap.cpp @@ -991,7 +991,7 @@ void dMenu_DmapBg_c::draw() { -35.0f + (local_224.x - local_218.x), -35.0f + (local_224.y - local_218.y)); #if TARGET_PC - if (!dusk::getSettings().game.enableFrameInterpolation) { + if (!dusk::frame_interp::is_enabled()) { field_0xdda = 0; } #else @@ -2624,7 +2624,7 @@ void dMenu_Dmap_c::zoomIn_proc() { void dMenu_Dmap_c::zoomOut_init_proc() { #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { mpDrawBg->resetScrollArrowMask(); } #endif diff --git a/src/d/d_menu_fmap.cpp b/src/d/d_menu_fmap.cpp index e9efc9d1a7..72b97ddbc2 100644 --- a/src/d/d_menu_fmap.cpp +++ b/src/d/d_menu_fmap.cpp @@ -1146,7 +1146,7 @@ void dMenu_Fmap_c::zoom_spot_to_region_init() { field_0x1ec = 1.0f; #if TARGET_PC // Frame interp note: field_0x122d used to be set every draw, causing flickering. Do it here instead. - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { mpDraw2DBack->resetScrollArrowMask(); } #endif diff --git a/src/d/d_menu_fmap2D.cpp b/src/d/d_menu_fmap2D.cpp index d62f7d1037..a63f6ec282 100644 --- a/src/d/d_menu_fmap2D.cpp +++ b/src/d/d_menu_fmap2D.cpp @@ -437,7 +437,7 @@ void dMenu_Fmap2DBack_c::draw() { if (field_0x122d) { mpMeterHaihai->drawHaihai(field_0x122d); #if TARGET_PC - if (!dusk::getSettings().game.enableFrameInterpolation) { + if (!dusk::frame_interp::is_enabled()) { field_0x122d = 0; } #else diff --git a/src/d/d_menu_window.cpp b/src/d/d_menu_window.cpp index 7f76f807df..e2e7b4e91f 100644 --- a/src/d/d_menu_window.cpp +++ b/src/d/d_menu_window.cpp @@ -26,6 +26,10 @@ #include "f_op/f_op_overlap_mng.h" #include "m_Do/m_Do_controller_pad.h" +#ifdef TARGET_PC +#include "dusk/frame_interpolation.h" +#endif + class dDlst_MENU_CAPTURE_c : public dDlst_base_c { public: virtual void draw() { @@ -1088,6 +1092,10 @@ void dMw_c::dMw_ring_create(u8 i_origin) { } mpCapture->setCaptureFlag(); + +#ifdef TARGET_PC + dusk::frame_interp::request_presentation_sync(); +#endif } bool dMw_c::dMw_ring_delete() { diff --git a/src/d/d_meter_button.cpp b/src/d/d_meter_button.cpp index e55b42bc1a..f89a483c39 100644 --- a/src/d/d_meter_button.cpp +++ b/src/d/d_meter_button.cpp @@ -18,6 +18,9 @@ #include "d/d_pane_class.h" #include "dusk/frame_interpolation.h" #include +#if TARGET_PC +#include "dusk/string.hpp" +#endif #if VERSION == VERSION_GCN_JPN #define STR_BUF_LEN 528 diff --git a/src/d/d_msg_scrn_tree.cpp b/src/d/d_msg_scrn_tree.cpp index d17ced9bd8..899a8f449a 100644 --- a/src/d/d_msg_scrn_tree.cpp +++ b/src/d/d_msg_scrn_tree.cpp @@ -188,6 +188,14 @@ void dMsgScrnTree_c::exec() { fukiAlpha(1.0f); } mpPmP_c->scale(g_MsgObject_HIO_c.mBoxWoodScaleX, g_MsgObject_HIO_c.mBoxWoodScaleY); + +#if TARGET_PC + const f32 hudScale = mDoGph_gInf_c::hudAspectScaleUp; + if (hudScale > 1.0f) { + field_0xc4->getPanePtr()->setBasePosition(J2DBasePosition_4); + field_0xc4->getPanePtr()->scale(hudScale, 1.0f); + } +#endif } void dMsgScrnTree_c::draw() { diff --git a/src/d/d_name.cpp b/src/d/d_name.cpp index 1f3ccbb1d7..abc7512f14 100644 --- a/src/d/d_name.cpp +++ b/src/d/d_name.cpp @@ -77,16 +77,16 @@ static const char* l_mojiEisu[65] = { // That can't work on a modern platform, so instead I've filled them out ahead of time. static const char* l_mojiEisuPal_1[65] = { "A", "N", "\xC0", "\xCF", "1", "B", "O", "\xC1", "\xD0", "2", "C", "P", "\xC2", "\xD1", "3", "D", "Q", - "\xC3", "\xD2", "4", "E", "R", "\xC4", "\xD3", "5", "F", "S", "\xC5", "\xD4", "6", "G", "T", "\xC6", "\xD5", - "7", "H", "U", "\xC7", "\xD6", "8", "I", "V", "\xC8", "\xD7", "9", "J", "W", "\xC9", "\xD8", "0", "K", - "X", "\xCA", "\xD9", ",", "L", "Y", "\xCB", "\xDA", ".", "M", "Z", "\xCC", "\xDB", " ", + "\xC4", "\xD2", "4", "E", "R", "\xC6", "\xD3", "5", "F", "S", "\xC7", "\xD4", "6", "G", "T", "\xC8", "\xD6", + "7", "H", "U", "\xC9", "\x8C", "8", "I", "V", "\xCA", "\xD9", "9", "J", "W", "\xCB", "\xDA", "0", "K", + "X", "\xCC", "\xDB", ",", "L", "Y", "\xCD", "\xDC", ".", "M", "Z", "\xCE", "\x2D", " ", }; static const char* l_mojiEisuPal_2[65] = { "a", "n", "\xE0", "\xEF", "1", "b", "o", "\xE1", "\xF0", "2", "c", "p", "\xE2", "\xF1", "3", "d", "q", - "\xE3", "\xF2", "4", "e", "r", "\xE4", "\xF3", "5", "f", "s", "\xE5", "\xF4", "6", "g", "t", "\xE6", - "\xF5", "7", "h", "u", "\xE7", "\xF6", "8", "i", "v", "\xE8", "\xF7", "9", "j", "w", "\xE9", "\xF8", "0", - "k", "x", "\xEA", "\xF9", ",", "l", "y", "\xEB", "\xFA", ".", "m", "z", "\xEC", "\xFB", " ", + "\xE4", "\xF2", "4", "e", "r", "\xE6", "\xF3", "5", "f", "s", "\xE7", "\xF4", "6", "g", "t", "\xE8", + "\xF6", "7", "h", "u", "\xE9", "\x9C", "8", "i", "v", "\xEA", "\xF9", "9", "j", "w", "\xEB", "\xFA", "0", + "k", "x", "\xEC", "\xFB", ",", "l", "y", "\xED", "\xFC", ".", "m", "z", "\xEE", "\xDF", " ", }; #elif REGION_PAL static const char* l_mojiEisuPal_1[65] = { @@ -295,6 +295,7 @@ void dName_c::_move() { } } else { #endif +#if !TARGET_PC if (mDoCPd_c::getTrigRight(PAD_1)) { // BUG: this check only fails if the cursor is at exactly 7 // setMoji allows the cursor to reach 8, which is out of bounds here @@ -311,7 +312,9 @@ void dName_c::_move() { mCurPos--; nameCursorMove(); } - } else if (mDoCPd_c::getTrigB(PAD_1)) { + } else +#endif + if (mDoCPd_c::getTrigB(PAD_1)) { if (mCurPos == 0) { mDoAud_seStart(Z2SE_SY_MENU_BACK, 0, 0, 0); field_0x2ac = mSelProc; diff --git a/src/d/d_s_name.cpp b/src/d/d_s_name.cpp index cfeeb722d6..92f5a6c518 100644 --- a/src/d/d_s_name.cpp +++ b/src/d/d_s_name.cpp @@ -10,6 +10,7 @@ #include "d/d_meter2_info.h" #include "d/d_s_name.h" #include "dusk/imgui/ImGuiConsole.hpp" +#include "dusk/livesplit.h" #include "dusk/memory.h" #include "dusk/speedrun.h" #include "dusk/settings.h" @@ -20,6 +21,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) @@ -421,8 +423,11 @@ void dScnName_c::changeGameScene() { if (!dusk::m_speedrunInfo.m_isRunStarted) { dusk::resetForSpeedrunMode(); dusk::m_speedrunInfo.startRun(); + dusk::speedrun::start(); } } + + 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/d/d_save.cpp b/src/d/d_save.cpp index 8e4af75322..fc1cbc4efe 100644 --- a/src/d/d_save.cpp +++ b/src/d/d_save.cpp @@ -30,7 +30,9 @@ #if TARGET_PC #include "dusk/settings.h" #include -#include + +#include "dusk/string.hpp" +#define strcpy dusk::SafeStringCopy #endif static u8 dSv_item_rename(u8 i_itemNo) { @@ -349,10 +351,6 @@ void dSv_player_item_c::setItem(int i_slotNo, u8 i_itemNo) { dComIfGp_setSelectItem(i); } } - - #if TARGET_PC - triggerAutoSave(); - #endif } u8 dSv_player_item_c::getItem(int i_slotNo, bool i_checkCombo) const { 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..a8e4ff3916 100644 --- a/src/dusk/achievements.cpp +++ b/src/dusk/achievements.cpp @@ -191,13 +191,17 @@ std::vector AchievementSystem::makeEntries() { } bool hasJewelRod = false; - for (int slot = 0; slot < 24 && !hasJewelRod; ++slot) { + bool hasAncientDoc = false; + for (int slot = 0; slot < 24; ++slot) { const u8 item = dComIfGs_getItem(slot, false); if (item == dItemNo_JEWEL_ROD_e || item == dItemNo_JEWEL_BEE_ROD_e || item == dItemNo_JEWEL_WORM_ROD_e) { hasJewelRod = true; } + if (item == dItemNo_ANCIENT_DOCUMENT_e || item == dItemNo_ANCIENT_DOCUMENT2_e || item == dItemNo_AIR_LETTER_e) { + hasAncientDoc = true; + } } - if (!hasJewelRod) { + if (!hasJewelRod || !hasAncientDoc) { return; } @@ -212,7 +216,6 @@ std::vector AchievementSystem::makeEntries() { dItemNo_KANTERA_e, dItemNo_PACHINKO_e, dItemNo_HAWK_EYE_e, - dItemNo_ANCIENT_DOCUMENT_e, dItemNo_HORSE_FLUTE_e, }; for (u8 required : requiredWheelItems) { @@ -692,6 +695,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..57488a27a1 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; @@ -13,9 +14,23 @@ static AutoSaveFuncs AutoSaveFuncsProc[] = { void noAutoSave() {} +bool canAutoSave() { + daAlink_c* player = (daAlink_c*)daAlink_getAlinkActorClass(); + if (player == nullptr) { + return false; + } + + if (player->checkCargoCarry() || player->checkCanoeRide()) { + return false; + } + + return dusk::getSettings().game.autoSave && shouldAutoSave && mAutoSaveProc == 0 && + strcmp(dComIfGp_getStartStageName(), "F_SP102") != 0 && + strcmp(dComIfGp_getStartStageName(), "F_SP112") != 0; +} + void triggerAutoSave() { - if (dusk::getSettings().game.autoSave && mAutoSaveProc == 0 && - strcmp(dComIfGp_getStartStageName(), "F_SP102") != 0) + if (canAutoSave()) { mAutoSaveProc = 1; } @@ -25,8 +40,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 +58,7 @@ void writeAutoSave() { } g_mDoMemCd_control.save(mSaveBuffer, sizeof(mSaveBuffer), 0); + return true; } void autoSaving() { @@ -47,8 +67,9 @@ void autoSaving() { if (cardState == 2) { mAutoSaveProc = 1; } else if (cardState == 1) { - writeAutoSave(); - mAutoSaveProc = 3; + if (writeAutoSave()) { + mAutoSaveProc = 3; + } } } } @@ -89,4 +110,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..1f9e7c54a6 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; @@ -55,12 +56,29 @@ static T sanitizeEnumValue(const ConfigVar& cVar, T value) { template void ConfigImpl::loadFromJson(ConfigVar& cVar, const json& jsonValue) { + if constexpr (std::is_enum_v) { + if (jsonValue.is_boolean()) { + using Underlying = std::underlying_type_t; + const bool b = jsonValue.get(); + + Underlying raw; + if constexpr (std::is_same_v) { + raw = b ? static_cast(2) : static_cast(0); + } else { + raw = b ? static_cast(1) : static_cast(0); + } + + cVar.setValue(sanitizeEnumValue(cVar, static_cast(raw)), false); + return; + } + } + cVar.setValue(sanitizeEnumValue(cVar, jsonValue.get()), false); } template nlohmann::json ConfigImpl::dumpToJson(const ConfigVar& cVar) { - return cVar.getValue(); + return cVar.getValueForSave(); } template requires std::is_integral_v && std::is_signed_v @@ -157,6 +175,8 @@ namespace dusk::config { template class ConfigImpl; template class ConfigImpl; template class ConfigImpl; + template class ConfigImpl; + template class ConfigImpl; } void dusk::config::Register(ConfigVarBase& configVar) { @@ -248,7 +268,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 +277,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/frame_interpolation.cpp b/src/dusk/frame_interpolation.cpp index 941c9ed318..b2ef0309e3 100644 --- a/src/dusk/frame_interpolation.cpp +++ b/src/dusk/frame_interpolation.cpp @@ -21,6 +21,7 @@ bool g_sync_presentation = false; float g_step = 0.0f; bool g_is_sim_frame = false; bool g_ui_tick_pending = false; +uint64_t g_sim_tick_seq = 0; Recording g_current_recording; Recording g_previous_recording; @@ -66,19 +67,18 @@ void copy_view_to_snap(CameraSnapshot* dst, const view_class& v) { } inline void lerp_matrix(Mtx out, const Mtx lhs, const Mtx rhs, float step) { - const float old_weight = 1.0f - step; for (size_t row = 0; row < 3; ++row) { for (size_t col = 0; col < 4; ++col) { - out[row][col] = lhs[row][col] * old_weight + rhs[row][col] * step; + const float l = lhs[row][col]; + out[row][col] = l + (rhs[row][col] - l) * step; } } } inline void lerp_xyz(cXyz* out, const cXyz& lhs, const cXyz& rhs, float step) { - const float old_weight = 1.0f - step; - out->x = lhs.x * old_weight + rhs.x * step; - out->y = lhs.y * old_weight + rhs.y * step; - out->z = lhs.z * old_weight + rhs.z * step; + out->x = lhs.x + (rhs.x - lhs.x) * step; + out->y = lhs.y + (rhs.y - lhs.y) * step; + out->z = lhs.z + (rhs.z - lhs.z) * step; } static s16 lerp_bank(s16 a, s16 b, f32 t) { @@ -135,10 +135,15 @@ void begin_sim_tick() { s_interpolationCallBackWork.clear(); s_cam_prev = std::move(s_cam_curr); + ++g_sim_tick_seq; } -void begin_frame(bool enabled, bool is_sim_frame, float step) { - g_enabled = enabled; +uint64_t sim_tick_seq() { + return g_sim_tick_seq; +} + +void begin_frame(FrameInterpMode mode, bool is_sim_frame, float step) { + g_enabled = mode != FrameInterpMode::Off; g_is_sim_frame = is_sim_frame; g_step = std::clamp(step, 0.0f, 1.0f); } diff --git a/src/dusk/game_clock.cpp b/src/dusk/game_clock.cpp index 8b887f610c..1dc7fbf322 100644 --- a/src/dusk/game_clock.cpp +++ b/src/dusk/game_clock.cpp @@ -4,6 +4,7 @@ #include #include #include +#include namespace dusk::game_clock { @@ -45,7 +46,8 @@ MainLoopPacer advance_main_loop() { MainLoopPacer out{}; out.presentation_dt_seconds = presentation_dt; - const bool should_interpolate = dusk::getSettings().game.enableFrameInterpolation && + const bool should_interpolate = dusk::getSettings().game.enableFrameInterpolation.getValue() != + dusk::FrameInterpMode::Off && !dusk::getTransientSettings().skipFrameRateLimit; out.is_interpolating = should_interpolate; out.sim_pace = sim_pace(); diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp index 4ac21d81df..077b4c15bc 100644 --- a/src/dusk/imgui/ImGuiConsole.cpp +++ b/src/dusk/imgui/ImGuiConsole.cpp @@ -13,6 +13,7 @@ #include "ImGuiEngine.hpp" #include "JSystem/JUtility/JUTGamePad.h" #include "SDL3/SDL_mouse.h" +#include "dusk/action_bindings.h" #include "dusk/audio/DuskAudioSystem.h" #include "dusk/config.hpp" #include "dusk/data.hpp" @@ -239,7 +240,8 @@ namespace dusk { } void ImGuiConsole::UpdateSettings() { - getTransientSettings().skipFrameRateLimit = getSettings().game.enableTurboKeybind && ImGui::IsKeyDown(ImGuiKey_Tab); + getTransientSettings().skipFrameRateLimit = getSettings().game.enableTurboKeybind && + (ImGui::IsKeyDown(ImGuiKey_Tab) || getActionBindHoldAnyPort(ActionBinds::TURBO_SPEED_BUTTON)); if (dusk::frame_interp::get_ui_tick_pending() && mDoMain::developmentMode == 1 && (mDoCPd_c::getHold(PAD_1) & (PAD_TRIGGER_R | PAD_TRIGGER_L)) == (PAD_TRIGGER_R | PAD_TRIGGER_L) && mDoCPd_c::getTrigY(PAD_1)) { getTransientSettings().moveLinkActive = !getTransientSettings().moveLinkActive; @@ -260,6 +262,12 @@ namespace dusk { config::Save(); } + if (getSettings().game.enableResetKeybind && ImGui::GetIO().KeyCtrl && + ImGui::IsKeyReleased(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; @@ -314,8 +322,8 @@ namespace dusk { } ImGui::PushFont(ImGuiEngine::fontLarge); ImGuiTextCenter("Failed to initialize any graphics backend."); - ImGuiTextCenter("\nYour system may be misconfigured, or your hardware may not support the required versions of any of the available backends."); - ImGuiTextCenter("\nA clean reinstall of Dusklight may help. For further assistance, please visit #tech-support on the Twilit Realm Discord server."); + ImGuiTextCenter("\nDusklight requires Vulkan 1.1+, or Direct X 12.0."); + ImGuiTextCenter("\nTry updating your Operating System and GPU drivers."); const auto& style = ImGui::GetStyle(); const auto retrySize = ImGui::CalcTextSize("Retry (Auto backend)"); const auto quitSize = ImGui::CalcTextSize("Quit"); @@ -360,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/ImGuiSaveEditor.cpp b/src/dusk/imgui/ImGuiSaveEditor.cpp index b08ae2860b..c09267bc27 100644 --- a/src/dusk/imgui/ImGuiSaveEditor.cpp +++ b/src/dusk/imgui/ImGuiSaveEditor.cpp @@ -713,7 +713,7 @@ namespace dusk { transformLevel++; } } - if (ImGui::SliderInt("Transform Level", &transformLevel, 0, 3)) { + if (ImGui::SliderInt("Transform Level", &transformLevel, 0, 4)) { u8 newFlags = 0; for (int i = 0; i < transformLevel; i++) { newFlags |= (1 << i); 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 aa91e2e255..e9768e9250 100644 --- a/src/dusk/settings.cpp +++ b/src/dusk/settings.cpp @@ -10,10 +10,11 @@ UserSettings g_userSettings = { .lockAspectRatio {"video.lockAspectRatio", false}, .enableFpsOverlay {"game.enableFpsOverlay", false}, .fpsOverlayCorner {"game.fpsOverlayCorner", 0}, + .maxFrameRate {"video.maxFrameRate", 240}, }, .audio = { - .masterVolume {"audio.masterVolume", 80}, + .masterVolume {"audio.masterVolume", 60}, .mainMusicVolume {"audio.mainMusicVolume", 100}, .subMusicVolume {"audio.subMusicVolume", 100}, .soundEffectsVolume {"audio.soundEffectsVolume", 100}, @@ -52,14 +53,17 @@ 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}, .bloomMultiplier {"game.bloomMultiplier", 1.0f}, .disableWaterRefraction {"game.disableWaterRefraction", false}, - .enableFrameInterpolation {"game.enableFrameInterpolation", false}, + .enableTextureReplacements {"game.enableTextureReplacements", true}, + .enableFrameInterpolation {"game.enableFrameInterpolation", FrameInterpMode::Off}, .internalResolutionScale {"game.internalResolutionScale", 0}, .shadowResolutionMultiplier {"game.shadowResolutionMultiplier", 1}, + .resampler {"game.resampler", Resampler::Bilinear}, .enableDepthOfField {"game.enableDepthOfField", true}, .enableMapBackground {"game.enableMapBackground", true}, .disableCutscenePillarboxing {"game.disableCutscenePillarboxing", false}, @@ -92,6 +96,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,12 +110,14 @@ 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}, @@ -118,6 +125,8 @@ UserSettings g_userSettings = { .showSpeedrunRTATimer {"game.showSpeedrunRTATimer", true}, .recordingMode {"game.recordingMode", false}, .removeQuestMapMarkers {"game.removeQuestMapMarkers", false}, + .showInputViewer {"game.showInputViewer", false}, + .showInputViewerGyro {"game.showInputViewerGyro", false} }, .backend = { @@ -130,6 +139,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}, + }, } }; @@ -144,6 +181,7 @@ void registerSettings() { Register(g_userSettings.video.lockAspectRatio); Register(g_userSettings.video.enableFpsOverlay); Register(g_userSettings.video.fpsOverlayCorner); + Register(g_userSettings.video.maxFrameRate); // Audio Register(g_userSettings.audio.masterVolume); @@ -181,10 +219,13 @@ 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); + Register(g_userSettings.game.enableTextureReplacements); Register(g_userSettings.game.internalResolutionScale); + Register(g_userSettings.game.resampler); Register(g_userSettings.game.shadowResolutionMultiplier); Register(g_userSettings.game.enableDepthOfField); Register(g_userSettings.game.enableMapBackground); @@ -201,14 +242,18 @@ 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.removeQuestMapMarkers); + 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); @@ -217,6 +262,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); @@ -242,6 +288,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..a0b2b6baa9 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 { @@ -34,7 +37,7 @@ Rml::String current_controller_name(int port) { Rml::String controller_index_name(u32 index) { const char* name = PADGetNameForControllerIndex(index); if (name == nullptr) { - return fmt::format("Controller {}", index + 1); + return fmt::format("Device {}", index + 1); } return name; } @@ -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); @@ -183,7 +124,7 @@ Rml::String native_axis_name(const PADAxisMapping& mapping, SDL_Gamepad* gamepad return native_button_name(gamepad, static_cast(mapping.nativeButton)); } - return "Not bound"; + return "Not Bound"; } bool is_dpad_button(PADButton button) { @@ -221,7 +162,7 @@ bool keyboard_escape_pressed() { Rml::String keyboard_key_name(s32 scancode) { if (scancode == PAD_KEY_INVALID) { - return "Not bound"; + return "Not Bound"; } switch (scancode) { case PAD_KEY_MOUSE_LEFT: @@ -362,11 +303,12 @@ void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) { }); }; - addPageButton(Page::Controller, "Controller", [port] { return current_controller_name(port); }, [] { return false; }); + addPageButton(Page::Controller, "Device", [port] { return current_controller_name(port); }, [] { return false; }); addPageButton(Page::Buttons, "Buttons", [] { return Rml::String(">"); }, [] { return false; }); 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{ @@ -407,7 +349,14 @@ void ControllerConfigWindow::build_port_tab(Rml::Element* content, int port) { rightPane, [](Pane& pane) { pane.add_text("Treat analog trigger movement as digital L and R button input."); }); - + leftPane.register_control(leftPane.add_button("Restore Default Controls").on_pressed([this, port] { + mDoAud_seStartMenu(kSoundClick); + PADRestoreDefaultMapping(port); + }), + rightPane, [](Pane& pane) { + pane.clear(); + pane.add_text("Restores all binding configurations for the currently selected device to their defaults."); + }); render_page(rightPane, port, mPage); } @@ -423,11 +372,12 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { [port] { return PADGetIndexForPort(port) < 0 && !keyboard_active(port); }, }) .on_pressed([this, port] { - mDoAud_seStartMenu(kSoundItemChange); + mDoAud_seStartMenu(kSoundClick); cancel_pending_binding(); PADClearPort(port); PADSetKeyboardActive(static_cast(port), FALSE); PADSerializeMappings(); + ClearAllActionBindings(port); }); pane.add_button({ @@ -435,16 +385,17 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { .isSelected = [port] { return keyboard_active(port); }, }) .on_pressed([this, port] { - mDoAud_seStartMenu(kSoundItemChange); + mDoAud_seStartMenu(kSoundClick); cancel_pending_binding(); PADClearPort(port); PADSetKeyboardActive(static_cast(port), TRUE); PADSerializeMappings(); + ClearAllActionBindings(port); }); const u32 controllerCount = PADCount(); if (controllerCount == 0) { - pane.add_text("No controllers detected"); + pane.add_text("No Device Detected"); break; } @@ -456,11 +407,12 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { [port, i] { return PADGetIndexForPort(port) == static_cast(i); }, }) .on_pressed([this, port, i] { - mDoAud_seStartMenu(kSoundItemChange); + mDoAud_seStartMenu(kSoundClick); cancel_pending_binding(); PADSetKeyboardActive(static_cast(port), FALSE); PADSetPortForIndex(i, port); PADSerializeMappings(); + ClearAllActionBindings(port); }); } break; @@ -480,17 +432,18 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { PADKeyButtonBinding* bindings = PADGetKeyButtonBindings(static_cast(port), &count); if (bindings == nullptr) { - return Rml::String("Not bound"); + return Rml::String("Not Bound"); } for (u32 i = 0; i < PAD_BUTTON_COUNT; ++i) { if (bindings[i].padButton == button) { return keyboard_key_name(bindings[i].scancode); } } - return Rml::String("Not bound"); + return Rml::String("Not Bound"); }, }) .on_pressed([this, port, button] { + mDoAud_seStartMenu(kSoundClick); cancel_pending_binding(); mPendingPort = port; mPendingBindingArmed = false; @@ -517,7 +470,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { u32 buttonCount = 0; PADButtonMapping* mappings = PADGetButtonMappings(port, &buttonCount); if (mappings == nullptr) { - pane.add_text("No controller selected"); + pane.add_text("No Device Selected"); break; } @@ -541,6 +494,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { }, }) .on_pressed([this, port, &mapping] { + mDoAud_seStartMenu(kSoundClick); cancel_pending_binding(); mPendingPort = port; mPendingBindingArmed = false; @@ -567,6 +521,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { }, }) .on_pressed([this, port, &mapping] { + mDoAud_seStartMenu(kSoundClick); cancel_pending_binding(); mPendingPort = port; mPendingBindingArmed = false; @@ -590,17 +545,18 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { PADKeyButtonBinding* bindings = PADGetKeyButtonBindings(static_cast(port), &count); if (bindings == nullptr) { - return Rml::String("Not bound"); + return Rml::String("Not Bound"); } for (u32 i = 0; i < PAD_BUTTON_COUNT; ++i) { if (bindings[i].padButton == button) { return keyboard_key_name(bindings[i].scancode); } } - return Rml::String("Not bound"); + return Rml::String("Not Bound"); }, }) .on_pressed([this, port, button] { + mDoAud_seStartMenu(kSoundClick); cancel_pending_binding(); mPendingPort = port; mPendingBindingArmed = false; @@ -621,17 +577,18 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { PADKeyAxisBinding* bindings = PADGetKeyAxisBindings(static_cast(port), &count); if (bindings == nullptr) { - return Rml::String("Not bound"); + return Rml::String("Not Bound"); } for (u32 i = 0; i < PAD_AXIS_COUNT; ++i) { if (bindings[i].padAxis == axis) { return keyboard_key_name(bindings[i].scancode); } } - return Rml::String("Not bound"); + return Rml::String("Not Bound"); }, }) .on_pressed([this, port, axis] { + mDoAud_seStartMenu(kSoundClick); cancel_pending_binding(); mPendingPort = port; mPendingBindingArmed = false; @@ -654,7 +611,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { u32 buttonCount = 0; PADButtonMapping* buttons = PADGetButtonMappings(port, &buttonCount); if (axes == nullptr && buttons == nullptr) { - pane.add_text("No controller selected"); + pane.add_text("No Device Selected"); break; } @@ -678,6 +635,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { }, }) .on_pressed([this, port, &mapping] { + mDoAud_seStartMenu(kSoundClick); cancel_pending_binding(); mPendingPort = port; mPendingBindingArmed = false; @@ -686,30 +644,33 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { } } - pane.add_section("Digital"); - if (buttons != nullptr) { - for (u32 i = 0; i < buttonCount; ++i) { - PADButtonMapping& mapping = buttons[i]; - if (mapping.padButton != PAD_TRIGGER_L && mapping.padButton != PAD_TRIGGER_R) { - continue; + if (getSettings().backend.enableAdvancedSettings) { + pane.add_section("Digital"); + if (buttons != nullptr) { + for (u32 i = 0; i < buttonCount; ++i) { + PADButtonMapping& mapping = buttons[i]; + if (mapping.padButton != PAD_TRIGGER_L && mapping.padButton != PAD_TRIGGER_R) { + continue; + } + pane.add_select_button({ + .key = PADGetButtonName(mapping.padButton), + .getValue = + [this, &mapping, gamepad] { + if (mPendingButtonMapping == &mapping) { + return pending_button_label(); + } + return native_button_name( + gamepad, mapping.nativeButton); + }, + }) + .on_pressed([this, port, &mapping] { + mDoAud_seStartMenu(kSoundClick); + cancel_pending_binding(); + mPendingPort = port; + mPendingBindingArmed = false; + mPendingButtonMapping = &mapping; + }); } - pane.add_select_button({ - .key = PADGetButtonName(mapping.padButton), - .getValue = - [this, &mapping, gamepad] { - if (mPendingButtonMapping == &mapping) { - return pending_button_label(); - } - return native_button_name( - gamepad, mapping.nativeButton); - }, - }) - .on_pressed([this, port, &mapping] { - cancel_pending_binding(); - mPendingPort = port; - mPendingBindingArmed = false; - mPendingButtonMapping = &mapping; - }); } } @@ -761,17 +722,18 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { PADKeyAxisBinding* bindings = PADGetKeyAxisBindings(static_cast(port), &count); if (bindings == nullptr) { - return Rml::String("Not bound"); + return Rml::String("Not Bound"); } for (u32 i = 0; i < PAD_AXIS_COUNT; ++i) { if (bindings[i].padAxis == axis) { return keyboard_key_name(bindings[i].scancode); } } - return Rml::String("Not bound"); + return Rml::String("Not Bound"); }, }) .on_pressed([this, port, axis] { + mDoAud_seStartMenu(kSoundClick); cancel_pending_binding(); mPendingPort = port; mPendingBindingArmed = false; @@ -796,7 +758,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { u32 axisCount = 0; PADAxisMapping* axes = PADGetAxisMappings(port, &axisCount); if (axes == nullptr) { - pane.add_text("No controller selected"); + pane.add_text("No Device Selected"); break; } @@ -817,6 +779,7 @@ void ControllerConfigWindow::render_page(Pane& pane, int port, Page page) { }, }) .on_pressed([this, port, &mapping] { + mDoAud_seStartMenu(kSoundClick); cancel_pending_binding(); mPendingPort = port; mPendingBindingArmed = false; @@ -946,6 +909,79 @@ 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] { + mDoAud_seStartMenu(kSoundClick); + 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 Device 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] { + mDoAud_seStartMenu(kSoundClick); + cancel_pending_binding(); + mPendingPort = port; + mPendingBindingArmed = false; + mPendingActionBinding = actionBind; + }); + }; + + for (auto& [configVars, actionName] : getActionBinds() | std::views::values) { + addActionBinding(&configVars->at(port), actionName); + } + break; + } } } @@ -997,6 +1033,12 @@ void ControllerConfigWindow::poll_pending_binding() { const s32 nativeButton = PADGetNativeButtonPressed(mPendingPort); if (nativeButton != -1) { const int completedPort = mPendingPort; + if (mPendingButtonMapping->nativeButton == static_cast(nativeButton) && + (mPendingButtonMapping->padButton != PAD_BUTTON_A && + mPendingButtonMapping->padButton != PAD_BUTTON_B)) { + unmap_pending_binding(); + return; + } mPendingButtonMapping->nativeButton = static_cast(nativeButton); finish_pending_binding(completedPort); } @@ -1007,6 +1049,10 @@ void ControllerConfigWindow::poll_pending_binding() { const PADSignedNativeAxis nativeAxis = PADGetNativeAxisPulled(mPendingPort); if (nativeAxis.nativeAxis != -1) { const int completedPort = mPendingPort; + if (mPendingAxisMapping->nativeAxis.nativeAxis == nativeAxis.nativeAxis) { + unmap_pending_binding(); + return; + } mPendingAxisMapping->nativeAxis = nativeAxis; mPendingAxisMapping->nativeButton = -1; finish_pending_binding(completedPort); @@ -1020,12 +1066,36 @@ 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; + if (mPendingActionBinding->getValue() == button) { + unmap_pending_binding(); + return; + } + mPendingActionBinding->setValue(button); + config::Save(); + finish_pending_binding(completedPort); + } + return; } } void ControllerConfigWindow::finish_pending_binding(int completedPort) { + mDoAud_seStartMenu(kSoundBindingChanged); mPendingButtonMapping = nullptr; mPendingAxisMapping = nullptr; + mPendingActionBinding = nullptr; mPendingPort = -1; mPendingBindingArmed = false; mSuppressNavigationUntilNeutral = true; @@ -1035,7 +1105,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 +1118,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 +1134,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 { @@ -1072,21 +1145,22 @@ bool ControllerConfigWindow::pending_input_neutral() const { } Rml::String ControllerConfigWindow::pending_button_label() const { - return mPendingBindingArmed ? "Press a button..." : "Waiting..."; + return mPendingBindingArmed ? "Press a Key or Button..." : "Waiting..."; } Rml::String ControllerConfigWindow::pending_axis_label() const { - return mPendingBindingArmed ? "Move axis or press a button..." : "Waiting..."; + return mPendingBindingArmed ? "Move Axis or press a Key or Button..." : "Waiting..."; } 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; @@ -1104,7 +1178,7 @@ void ControllerConfigWindow::finish_pending_key_binding() { } Rml::String ControllerConfigWindow::pending_key_label() const { - return mPendingBindingArmed ? "Press a key or mouse button..." : "Waiting..."; + return mPendingBindingArmed ? "Press a Key or Mouse Button..." : "Waiting..."; } void ControllerConfigWindow::stop_rumble_test() { @@ -1118,4 +1192,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..196c9bf719 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 ""; @@ -1502,14 +1501,14 @@ EditorWindow::EditorWindow() { .getValue = [] { return std::popcount(static_cast( - get_player_status_b()->mTransformLevelFlag & 0x7)); + get_player_status_b()->mTransformLevelFlag & 0xF)); }, .setValue = [](int value) { get_player_status_b()->mTransformLevelFlag = static_cast((1u << value) - 1u); }, - .max = 3, + .max = 4, }), rightPane, {}); leftPane.register_control( 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/graphics_tuner.cpp b/src/dusk/ui/graphics_tuner.cpp index 4f68d4a078..0b75b27a89 100644 --- a/src/dusk/ui/graphics_tuner.cpp +++ b/src/dusk/ui/graphics_tuner.cpp @@ -3,6 +3,7 @@ #include "Z2AudioLib/Z2SeMgr.h" #include "m_Do/m_Do_audio.h" +#include #include #include #include @@ -43,6 +44,8 @@ int get_value(GraphicsOption option) { return getSettings().game.internalResolutionScale.getValue(); case GraphicsOption::ShadowResolution: return getSettings().game.shadowResolutionMultiplier.getValue(); + case GraphicsOption::Resampler: + return static_cast(getSettings().game.resampler.getValue()); case GraphicsOption::BloomMode: return static_cast(getSettings().game.bloomMode.getValue()); case GraphicsOption::BloomMultiplier: @@ -62,6 +65,22 @@ void set_value(GraphicsOption option, int value) { case GraphicsOption::ShadowResolution: getSettings().game.shadowResolutionMultiplier.setValue(value); break; + case GraphicsOption::Resampler: { + const auto sampler = static_cast(std::clamp(value, + static_cast(Resampler::Bilinear), + static_cast(Resampler::Area))); + getSettings().game.resampler.setValue(sampler); + switch (sampler) { + case Resampler::Area: + aurora_set_resampler(SAMPLER_AREA); + break; + case Resampler::Bilinear: + default: + aurora_set_resampler(SAMPLER_BILINEAR); + break; + } + break; + } case GraphicsOption::BloomMode: getSettings().game.bloomMode.setValue(static_cast(std::clamp( value, static_cast(BloomMode::Off), static_cast(BloomMode::Dusk)))); @@ -177,6 +196,14 @@ Rml::String format_graphics_setting_value(GraphicsOption option, int value) { } case GraphicsOption::ShadowResolution: return fmt::format("{}×", value); + case GraphicsOption::Resampler: + switch (static_cast(value)) { + case Resampler::Bilinear: + return "Bilinear"; + case Resampler::Area: + return "Area"; + } + break; case GraphicsOption::BloomMode: switch (static_cast(value)) { case BloomMode::Off: @@ -184,7 +211,7 @@ Rml::String format_graphics_setting_value(GraphicsOption option, int value) { case BloomMode::Classic: return "Classic"; case BloomMode::Dusk: - return "Dusk"; + return "Dusklight"; } break; case GraphicsOption::BloomMultiplier: @@ -211,7 +238,7 @@ GraphicsTuner::GraphicsTuner(GraphicsTunerProps props, bool prelaunch) SteppedCarousel::Props{ .min = mValueMin, .max = mValueMax, - .step = 1, + .step = props.step, .getValue = [this] { return get_value(mOption); }, .onChange = [this](int value) { set_value(mOption, value); }, .formatValue = diff --git a/src/dusk/ui/graphics_tuner.hpp b/src/dusk/ui/graphics_tuner.hpp index 254aada22b..6f8f113d13 100644 --- a/src/dusk/ui/graphics_tuner.hpp +++ b/src/dusk/ui/graphics_tuner.hpp @@ -42,6 +42,7 @@ private: enum class GraphicsOption { InternalResolution, ShadowResolution, + Resampler, BloomMode, BloomMultiplier, }; @@ -55,6 +56,7 @@ struct GraphicsTunerProps { int valueMin = 0; int valueMax = 0; int defaultValue = 0; + int step = 1; }; class GraphicsTuner : public Document { diff --git a/src/dusk/ui/input.cpp b/src/dusk/ui/input.cpp index cdaa2360c0..2628aeb682 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); } @@ -438,10 +454,18 @@ bool touch_moved_too_far( return delta.SquaredMagnitude() > threshold * threshold; } -void dispatch_menu_key(Rml::Context& context) noexcept { +void emit_key_press(Rml::Context& context, Rml::Input::KeyIdentifier key) noexcept { context.ProcessMouseLeave(); - context.ProcessKeyDown(Rml::Input::KI_F1, 0); - context.ProcessKeyUp(Rml::Input::KI_F1, 0); + context.ProcessKeyDown(key, 0); +} + +void emit_key_tap(Rml::Context& context, Rml::Input::KeyIdentifier key) noexcept { + emit_key_press(context, key); + context.ProcessKeyUp(key, 0); +} + +void dispatch_menu_key(Rml::Context& context) noexcept { + emit_key_tap(context, Rml::Input::KI_F1); } bool handle_touch_menu_tap(Rml::Context& context, const SDL_Event& event) noexcept { @@ -611,7 +635,9 @@ void process_axis_direction( if (repeat->held) { if (released) { - if (!repeat->pending) { + if (repeat->pending) { + emit_key_tap(context, repeat->key); + } else { context.ProcessKeyUp(repeat->key, 0); } set_pad_button_held(port, heldPadButton, false); @@ -631,7 +657,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; } @@ -642,8 +668,7 @@ void process_axis_direction( } begin_gamepad_key(*repeat, key); - context.ProcessMouseLeave(); - context.ProcessKeyDown(key, 0); + emit_key_press(context, key); } } // namespace @@ -719,7 +744,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) { @@ -731,8 +756,7 @@ void handle_event(const SDL_Event& event) noexcept { } } if (!deferred) { - context->ProcessMouseLeave(); - context->ProcessKeyDown(key, 0); + emit_key_press(*context, key); } } } else { @@ -744,7 +768,9 @@ void handle_event(const SDL_Event& event) noexcept { if (repeat != nullptr) { *repeat = {}; } - if (!wasPending) { + if (wasPending) { + emit_key_tap(*context, key); + } else { context->ProcessKeyUp(key, 0); } } @@ -771,8 +797,7 @@ void update_input() noexcept { repeat.pressedAt = now; repeat.nextRepeatAt = repeat.repeatable ? now + kGamepadRepeatInitialDelay : 0.0; - context->ProcessMouseLeave(); - context->ProcessKeyDown(repeat.key, 0); + emit_key_press(*context, repeat.key); continue; } diff --git a/src/dusk/ui/menu_bar.cpp b/src/dusk/ui/menu_bar.cpp index 5ee7e68a88..1be5c7abb0 100644 --- a/src/dusk/ui/menu_bar.cpp +++ b/src/dusk/ui/menu_bar.cpp @@ -18,6 +18,7 @@ #include "modal.hpp" #include "settings.hpp" #include "ui.hpp" +#include "warp.hpp" #include "window.hpp" #include @@ -51,11 +52,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..69c262720e 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" @@ -101,13 +103,13 @@ Rml::Element* create_controller_warning(Rml::Element* parent) { auto* heading = append(elem, "heading"); auto* title = append(heading, "span"); - title->SetInnerRML("No controller assigned"); + title->SetInnerRML("No Device Assigned"); auto* icon = append(heading, "icon"); icon->SetClass("warning", true); auto* message = append(elem, "message"); auto* content = append(message, "span"); - content->SetInnerRML("Configure controller port 1 in Settings."); + content->SetInnerRML("Configure Port 1 in Settings."); return elem; } @@ -145,19 +147,29 @@ Rml::String back_button_name() { #if defined(TARGET_ANDROID) || (defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST) constexpr auto kMenuNotificationPrefix = "3-finger tap or"; #else -constexpr auto kMenuNotificationPrefix = "Press F1 or"; +constexpr auto kMenuNotificationPrefix = "Press F1 or"; #endif 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/preset.cpp b/src/dusk/ui/preset.cpp index 1f96cc0593..e5af8f2d50 100644 --- a/src/dusk/ui/preset.cpp +++ b/src/dusk/ui/preset.cpp @@ -40,7 +40,7 @@ void applyPresetDusk() { s.game.enableQuickTransform.setValue(true); s.game.instantSaves.setValue(true); s.game.midnasLamentNonStop.setValue(true); - s.game.enableFrameInterpolation.setValue(true); + s.game.enableFrameInterpolation.setValue(FrameInterpMode::Unlimited); s.game.sunsSong.setValue(true); s.game.bloomMode.setValue(BloomMode::Dusk); s.game.internalResolutionScale.setValue(0); @@ -83,7 +83,7 @@ PresetWindow::PresetWindow() : WindowSmall("modal", "modal-dialog") { "Enhancements disabled to match the GameCube version. " "Good for speedrunning or simple nostalgia!", applyPresetClassic}, - {"Dusk", + {"Dusklight", "Graphics & quality of life tweaks, including some from the Wii U version. " "Our recommended way to play!", applyPresetDusk}, diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp index 1650afe993..6187f97d4b 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" @@ -54,6 +59,12 @@ constexpr std::array kFpsOverlayCornerNames = { "Bottom Right", }; +constexpr std::array kInterpolationModes = { + "Off", + "Capped", + "Unlimited", +}; + constexpr std::array kGyroInputModeLabels = { "Sensor", "Mouse", @@ -152,8 +163,8 @@ std::vector available_backends() { size_t backendCount = 0; const AuroraBackend* raw = aurora_get_available_backends(&backendCount); for (size_t i = 0; i < backendCount; ++i) { - // Do not expose NULL or D3D11 - if (raw[i] != BACKEND_NULL && raw[i] != BACKEND_D3D11) { + // Do not expose NULL + if (raw[i] != BACKEND_NULL) { backends.emplace_back(raw[i]); } } @@ -182,6 +193,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 +207,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 +306,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 = @@ -308,13 +357,15 @@ const Rml::String kInternalResolutionHelpText = const Rml::String kShadowResolutionHelpText = "Configure the shadow-map resolution. Higher values improve shadow quality but increase GPU " "and memory usage."; +const Rml::String kResamplerHelpText = + "Configure the sampling method used when scaling the internal resolution for final presentation."; const Rml::String kBloomHelpText = "Configure the post-processing bloom effect. Classic uses the original bloom pass; Dusklight uses " "a higher-quality bloom pass."; const Rml::String kBloomBrightnessHelpText = "Configure bloom intensity. Higher values make bright areas glow more strongly."; const Rml::String kUnlockFramerateHelpText = - "Uses inter-frame interpolation to enable higher frame rates.

May introduce minor " + "
Uses inter-frame interpolation to enable higher frame rates.

May introduce minor " "visual artifacts or animation glitches."; int float_setting_percent(ConfigVar& var) { @@ -397,6 +448,31 @@ SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar& var, + Rml::String key, Rml::String helpText, int min, int max, int step = 5, + std::function isDisabled = {}, std::string suffix = "") { + auto& button = leftPane.add_child(NumberButton::Props{ + .key = std::move(key), + .getValue = [&var] { return var; }, + .setValue = + [&var, min, max](int value) { + var.setValue(std::clamp(value, min, max)); + config::Save(); + }, + .isDisabled = std::move(isDisabled), + .isModified = [&var] { return var.getValue() != var.getDefaultValue(); }, + .min = min, + .max = max, + .step = step, + .suffix = suffix, + }); + leftPane.register_control(button, rightPane, [helpText = std::move(helpText)](Pane& pane) { + pane.clear(); + pane.add_text(helpText); + }); + return button; +} + template void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, ConfigVar& var, const GraphicsTunerProps& props, bool prelaunch) { @@ -710,7 +786,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, @@ -732,6 +807,15 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .valueMax = 8, .defaultValue = 1, }, mPrelaunch); + graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.resampler, + GraphicsTunerProps{ + .option = GraphicsOption::Resampler, + .title = "Output Resampling", + .helpText = kResamplerHelpText, + .valueMin = static_cast(Resampler::Bilinear), + .valueMax = static_cast(Resampler::Area), + .defaultValue = static_cast(Resampler::Bilinear), + }, mPrelaunch); leftPane.add_section("Post-Processing"); graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMode, @@ -751,14 +835,49 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { .valueMin = 0, .valueMax = 100, .defaultValue = 100, + .step = 10, }, mPrelaunch); leftPane.add_section("Rendering"); - config_bool_select(leftPane, rightPane, getSettings().game.enableFrameInterpolation, + config_bool_select(leftPane, rightPane, getSettings().game.enableTextureReplacements, { - .key = "Unlock Framerate", - .helpText = kUnlockFramerateHelpText, + .key = "Use Texture Pack", + .helpText = "Enable installed texture replacements.", + .onChange = [](bool value) { aurora_set_texture_replacements_enabled(value); }, }); + leftPane.register_control( + leftPane.add_select_button({ + .key = "Unlock Framerate", + .getValue = + [] { + return kInterpolationModes[static_cast(getSettings().game.enableFrameInterpolation.getValue())]; + }, + .isModified = + [] { + return getSettings().game.enableFrameInterpolation.getValue() != + getSettings().game.enableFrameInterpolation.getDefaultValue(); + }, + }), + rightPane, [](Pane& pane) { + for (int i = 0; i < kInterpolationModes.size(); i++) { + pane.add_button({ + .text = kInterpolationModes[i], + .isSelected = + [i] { + return getSettings().game.enableFrameInterpolation.getValue() == static_cast(i); + }, + }) + .on_pressed([i] { + mDoAud_seStartMenu(kSoundItemChange); + getSettings().game.enableFrameInterpolation.setValue(static_cast(i)); + config::Save(); + }); + } + pane.add_rml(kUnlockFramerateHelpText); + }); + config_int_select(leftPane, rightPane, getSettings().video.maxFrameRate, + "Framerate Cap", "Limit the framerate to the specified value.", 30, 540, 1, + [] { return getSettings().game.enableFrameInterpolation.getValue() != FrameInterpMode::Capped; }); config_bool_select(leftPane, rightPane, getSettings().game.enableDepthOfField, { .key = "Enable Depth of Field", @@ -787,18 +906,18 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { }); }; - leftPane.add_section("Controller"); - leftPane.register_control(leftPane.add_button("Configure Controller").on_pressed([this] { + leftPane.add_section("Inputs"); + leftPane.register_control(leftPane.add_button("Configure Inputs").on_pressed([this] { push(std::make_unique(mPrelaunch)); }), rightPane, [](Pane& pane) { pane.clear(); - pane.add_text("Open controller binding configuration."); + pane.add_text("Open input binding configuration."); }); config_bool_select(leftPane, rightPane, getSettings().game.allowBackgroundInput, { - .key = "Allow Background Input", - .helpText = "Allow controller input even when the game window is not focused.", + .key = "Allow Background Inputs", + .helpText = "Allow inputs even when the game window is not focused.", .onChange = [](bool value) { aurora_set_background_input(value); }, }); @@ -815,9 +934,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 +1011,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 +1030,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 = [] { @@ -1025,8 +1147,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { addOption("Faster Tears of Light", getSettings().game.fastTears, "Tears of Light dropped by Shadow Insects pop out faster like the HD version."); addSpeedrunDisabledOption("Autosave", getSettings().game.autoSave, - "Autosaves the game when going to a new area, opening a dungeon door, " - "or getting a new item."); + "Autosaves the game when going to a new area or opening a dungeon door."); addOption("Instant Saves", getSettings().game.instantSaves, "Skips the delay when writing to the Memory Card."); addOption("Hold B for Instant Text", getSettings().game.instantText, @@ -1105,6 +1226,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, @@ -1122,7 +1244,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { addCheat("Always Greatspin", getSettings().game.alwaysGreatspin, "Allows the Great Spin attack without requiring full health."); addCheat("Fast Iron Boots", getSettings().game.enableFastIronBoots, - "Speeds up movement while wearing the Iron Boots."); + "Speeds up movement while heavy, including wearing the Iron Boots, holding the Ball and Chain, wearing Magic Armor without rupees, etc."); addCheat("Can Transform Anywhere", getSettings().game.canTransformAnywhere, "Allows transforming even if NPCs are looking."); addCheat("Fast Roll", getSettings().game.fastRoll, @@ -1131,6 +1253,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) { @@ -1202,7 +1326,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) { }); pane.add_button( { - .text = "Controller", + .text = "Missing Device", .isSelected = [] { return getSettings().game.enableControllerToasts.getValue(); }, }) @@ -1251,6 +1375,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 Dusklight to integrate with Discord Rich Presence. This allows Discord to show your status in-game.", + .onChange = [](bool enabled) { + if (enabled) { + dusk::discord::initialize(); + } else { + dusk::discord::shutdown(); + } + }, + }); +#endif config_bool_select(leftPane, rightPane, getSettings().backend.enableAdvancedSettings, { .key = "Enable Advanced Settings", @@ -1269,6 +1407,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/ui.cpp b/src/dusk/ui/ui.cpp index fc2d5d6691..98c9ca5782 100644 --- a/src/dusk/ui/ui.cpp +++ b/src/dusk/ui/ui.cpp @@ -130,7 +130,7 @@ void handle_event(const SDL_Event& event) noexcept { if (getSettings().game.enableControllerToasts) { const char* name = SDL_GetGamepadName(gamepad); Rml::String content = fmt::format("{}", name ? name : "[Unknown]"); - Rml::String title = "Controller connected"; + Rml::String title = "Device Connected"; if (const char* icon = connection_state_icon(SDL_GetGamepadConnectionState(gamepad))) { title = fmt::format( "{} &#x{};", title, @@ -163,7 +163,7 @@ void handle_event(const SDL_Event& event) noexcept { const char* name = SDL_GetGamepadNameForID(event.gdevice.which); push_toast({ .type = "controller", - .title = "Controller disconnected", + .title = "Device Disconnected", .content = name ? name : "[Unknown]", .duration = std::chrono::seconds(4), }); diff --git a/src/dusk/ui/ui.hpp b/src/dusk/ui/ui.hpp index 4a27ac7aac..cbfe3dcc9d 100644 --- a/src/dusk/ui/ui.hpp +++ b/src/dusk/ui/ui.hpp @@ -26,6 +26,8 @@ struct Toast { constexpr u32 kSoundClick = Z2SE_SY_CURSOR_OK; // "Play" button clicked/pressed constexpr u32 kSoundPlay = Z2SE_SY_ITEM_COMBINE_ON; +// Input binding changed +constexpr u32 kSoundBindingChanged = Z2SE_SY_ITEM_SET_X; // Menu button pressed (open/close menu bar or hide/show the active window) constexpr u32 kSoundMenuOpen = Z2SE_SY_MENU_SUB_IN; @@ -49,6 +51,8 @@ constexpr u32 kSoundItemDisable = Z2SE_SUBJ_VIEW_OUT; // Achievement unlocked constexpr u32 kSoundAchievementUnlock = Z2SE_NAVI_FLY; +// Warning prompt +constexpr u32 kSoundWarning = Z2SE_SY_COW_GET_IN; struct Insets { float top = 0.0f; 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 55ef94f8a3..72219e98dd 100644 --- a/src/f_ap/f_ap_game.cpp +++ b/src/f_ap/f_ap_game.cpp @@ -792,6 +792,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); @@ -803,7 +807,7 @@ static void duskExecute() { } if (dusk::getSettings().game.infiniteRupees) { - dComIfGs_setRupee(9999); + dComIfGs_setRupee(dComIfGs_getRupeeMax()); } if (dusk::getSettings().game.infiniteOxygen) { 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_ext.cpp b/src/m_Do/m_Do_ext.cpp index fc8952e91d..16158815cd 100644 --- a/src/m_Do/m_Do_ext.cpp +++ b/src/m_Do/m_Do_ext.cpp @@ -2410,7 +2410,7 @@ void mDoExt_3DlineMat0_c::draw() { } #if TARGET_PC - if (!dusk::getSettings().game.enableFrameInterpolation) + if (!dusk::frame_interp::is_enabled()) #endif { field_0x16 ^= (u8)1; @@ -2740,7 +2740,7 @@ void mDoExt_3DlineMat1_c::draw() { } GXSetTexCoordScaleManually(GX_TEXCOORD0, 0, 0, 0); #if TARGET_PC - if (!dusk::getSettings().game.enableFrameInterpolation) + if (!dusk::frame_interp::is_enabled()) #endif { mIsDrawn ^= (u8)1; @@ -2822,7 +2822,7 @@ void mDoExt_3DlineMat1_c::update(int param_0, f32 param_1, GXColor& param_2, u16 } #if TARGET_PC - const cXyz& lineEye = (presentationEye != nullptr && dusk::getSettings().game.enableFrameInterpolation) ? *presentationEye : sp_3c->lookat.eye; + const cXyz& lineEye = (presentationEye != nullptr && dusk::frame_interp::is_enabled()) ? *presentationEye : sp_3c->lookat.eye; sp_13c = *local_r27 - lineEye; #else sp_13c = *local_r27 - sp_3c->lookat.eye; @@ -2982,7 +2982,7 @@ void mDoExt_3DlineMat1_c::update(int param_0, GXColor& param_2, dKy_tevstr_c* pa local_r27 = sp_38[0].field_0x0; size_p = sp_38->field_0x4; #if TARGET_PC - if (presentationEye != nullptr && dusk::getSettings().game.enableFrameInterpolation && size_p == NULL) { + if (presentationEye != nullptr && dusk::frame_interp::is_enabled() && size_p == NULL) { sp_38 += 1; continue; } @@ -3001,7 +3001,7 @@ void mDoExt_3DlineMat1_c::update(int param_0, GXColor& param_2, dKy_tevstr_c* pa local_f30 = sp_130.abs(); local_f31 += local_f30 * 0.1f; #if TARGET_PC - const cXyz& lineEye = (presentationEye != nullptr && dusk::getSettings().game.enableFrameInterpolation) ? *presentationEye : stack_3c->lookat.eye; + const cXyz& lineEye = (presentationEye != nullptr && dusk::frame_interp::is_enabled()) ? *presentationEye : stack_3c->lookat.eye; sp_13c = local_r27[0] - lineEye; #else sp_13c = local_r27[0] - stack_3c->lookat.eye; @@ -3077,7 +3077,7 @@ void mDoExt_3DlineMat1_c::update(int param_0, GXColor& param_2, dKy_tevstr_c* pa #if TARGET_PC void mDoExt_3DlineMat1_c::refreshGeometryForPresentationEye(const cXyz& eye) { - if (!dusk::getSettings().game.enableFrameInterpolation) { + if (!dusk::frame_interp::is_enabled()) { return; } if (mInterpLineKind == 1) { diff --git a/src/m_Do/m_Do_graphic.cpp b/src/m_Do/m_Do_graphic.cpp index c400b22266..65a2b5b95a 100644 --- a/src/m_Do/m_Do_graphic.cpp +++ b/src/m_Do/m_Do_graphic.cpp @@ -2063,7 +2063,7 @@ static void captureScreenPerspDrawInfo(JPADrawInfo& info) { static void drawItem3D() { ZoneScoped; #ifdef TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { // FRAME INTERP NOTE: Title screen needs 0.0f while everything else that runs through this is -100.0f. if (fopAcM_SearchByName(fpcNm_TITLE_e) != nullptr) { dMenu_Collect3D_c::setViewPortOffsetY(0.0f); @@ -2241,7 +2241,7 @@ int mDoGph_Painter() { #endif dKy_setLight(); #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { dKy_setLight_again(); } #endif @@ -2296,7 +2296,7 @@ int mDoGph_Painter() { } #if TARGET_PC - if (dusk::getSettings().game.enableFrameInterpolation) { + if (dusk::frame_interp::is_enabled()) { // FRAME INTERP NOTE: Currently only recalculating points for Epona's reins. Need a more global solution. if (daHorse_c* horse = dComIfGp_getHorseActor()) { horse->lerpControlPoints(dusk::frame_interp::get_interpolation_step()); diff --git a/src/m_Do/m_Do_lib.cpp b/src/m_Do/m_Do_lib.cpp index 921681d464..3a32c4c342 100644 --- a/src/m_Do/m_Do_lib.cpp +++ b/src/m_Do/m_Do_lib.cpp @@ -96,8 +96,8 @@ void mDoLib_project(Vec* src, Vec* dst) { xSize = FB_WIDTH; } else { #if TARGET_PC - xOffset = mDoGph_gInf_c::getSafeMinXF(); - xSize = viewPort->width * mDoGph_gInf_c::hudAspectScaleUp; + xOffset = mDoGph_gInf_c::getMinXF(); + xSize = mDoGph_gInf_c::getWidthF(); #else xOffset = viewPort->x_orig; xSize = viewPort->width; diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp index 8f0a02ad1d..490c0b8980 100644 --- a/src/m_Do/m_Do_main.cpp +++ b/src/m_Do/m_Do_main.cpp @@ -28,6 +28,7 @@ #include "d/d_s_logo.h" #include "d/d_s_menu.h" #include "d/d_s_play.h" +#include "dusk/time.h" #include "f_ap/f_ap_game.h" #include "f_op/f_op_msg.h" #include "m_Do/m_Do_MemCard.h" @@ -71,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" @@ -79,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" @@ -118,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 { @@ -276,8 +280,9 @@ void main01(void) { const auto pacing = dusk::game_clock::advance_main_loop(); if (pacing.is_interpolating) { if (pacing.sim_ticks_to_run > 0) { - dusk::frame_interp::begin_frame(true, true, 0.0f); + dusk::frame_interp::begin_frame(dusk::getSettings().game.enableFrameInterpolation, true, 0.0f); dusk::frame_interp::set_ui_tick_pending(true); + for (int sim_tick = 0; sim_tick < pacing.sim_ticks_to_run; ++sim_tick) { dusk::frame_interp::begin_sim_tick(); mDoCPd_c::read(); @@ -288,7 +293,7 @@ void main01(void) { } } - dusk::frame_interp::begin_frame(true, false, + dusk::frame_interp::begin_frame(dusk::getSettings().game.enableFrameInterpolation, false, dusk::game_clock::sample_interpolation_step()); dusk::frame_interp::interpolate(); dusk::frame_interp::begin_presentation_camera(); @@ -298,7 +303,7 @@ void main01(void) { dusk::frame_interp::end_presentation_camera(); dusk::frame_interp::set_ui_tick_pending(false); } else { - dusk::frame_interp::begin_frame(false, true, 0.0f); + dusk::frame_interp::begin_frame(dusk::FrameInterpMode::Off, true, 0.0f); dusk::frame_interp::set_ui_tick_pending(true); // Game Inputs @@ -312,8 +317,26 @@ void main01(void) { mDoAud_Execute(); } + static Limiter main_loop_limiter; + static double last_fps_setting = 0.0; + static Limiter::duration_t target_ns = 0; + + if (dusk::getSettings().game.enableFrameInterpolation.getValue() == dusk::FrameInterpMode::Capped && !dusk::getTransientSettings().skipFrameRateLimit) { + double current_fps = dusk::getSettings().video.maxFrameRate.getValue(); + if (current_fps != last_fps_setting) { + last_fps_setting = current_fps; + target_ns = static_cast(1'000'000'000.0 / current_fps); + } + + Limiter::duration_t sleepTime = main_loop_limiter.Sleep(target_ns); + dusk::frameUsagePct = 100.0f * (1.0f - static_cast(sleepTime) / static_cast(target_ns)); + } else { + main_loop_limiter.Reset(); + } + aurora_end_frame(); + FrameMark; #ifdef DUSK_DISCORD @@ -461,6 +484,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 // ========================================================================= @@ -485,7 +513,7 @@ int game_main(int argc, char* argv[]) { ("h,help", "Print usage") ("console", "Show the Windows console window for logs", cxxopts::value()->default_value("false")->implicit_value("true")) ("dvd", "Path to DVD image file", cxxopts::value()) - ("backend", "Graphics API backend to use (auto, d3d12, metal, vulkan, null)", cxxopts::value()) + ("backend", "Graphics API backend to use (auto, d3d12, d3d11, metal, vulkan, null)", cxxopts::value()) ("cvar", "Override configuration variables without modifying config", cxxopts::value>()); arg_options.parse_positional({"dvd"}); @@ -507,10 +535,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? @@ -524,11 +559,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; @@ -543,13 +583,15 @@ int game_main(int argc, char* argv[]) { config.allowJoystickBackgroundEvents = dusk::getSettings().game.allowBackgroundInput; config.pauseOnFocusLost = dusk::getSettings().game.pauseOnFocusLost; config.imGuiInitCallback = &aurora_imgui_init_callback; - config.allowTextureReplacements = true; + config.allowTextureReplacements = dusk::getSettings().game.enableTextureReplacements; config.allowTextureDumps = false; auroraInfo = aurora_initialize(argc, argv, &config); } #ifdef DUSK_DISCORD - dusk::discord::initialize(); + if (dusk::getSettings().game.enableDiscordPresence) { + dusk::discord::initialize(); + } #endif VISetWindowTitle( @@ -562,8 +604,17 @@ int game_main(int argc, char* argv[]) { AuroraSetViewportPolicy(AURORA_VIEWPORT_STRETCH); } VISetFrameBufferScale(dusk::getSettings().game.internalResolutionScale.getValue()); + switch (dusk::getSettings().game.resampler.getValue()) { + case dusk::Resampler::Area: + aurora_set_resampler(SAMPLER_AREA); + break; + case dusk::Resampler::Bilinear: + default: + aurora_set_resampler(SAMPLER_BILINEAR); + break; + } - 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;