diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 37679e3f56..37a0849fd0 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -8,9 +8,9 @@ on:
pull_request:
env:
- # SCCACHE_GHA_ENABLED: "true"
+ SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
- SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
+ # SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
jobs:
build-linux:
@@ -22,7 +22,7 @@ jobs:
matrix:
include:
- name: GCC x86_64
- runner: [self-hosted, Linux]
+ runner: ubuntu-latest
preset: gcc
artifact_arch: x86_64
# - name: GCC aarch64
@@ -41,7 +41,6 @@ jobs:
submodules: recursive
- name: Install dependencies
- if: 'false' # disabled for self-hosted
run: |
sudo apt-get update
sudo apt-get -y install ninja-build clang lld openssl libcurl4-openssl-dev \
@@ -51,8 +50,7 @@ jobs:
libxss-dev libfuse2 libusb-1.0-0-dev libdecor-0-dev libpipewire-0.3-dev libunwind-dev
- name: Setup sccache
- if: 'false' # disabled for self-hosted
- uses: mozilla-actions/sccache-action@v0.0.9
+ uses: mozilla-actions/sccache-action@v0.0.10
- name: Print sccache stats
run: sccache --show-stats
@@ -67,7 +65,6 @@ jobs:
run: ci/build-appimage.sh
- name: Upload artifacts
- if: startsWith(github.event.ref, 'refs/tags/v')
uses: actions/upload-artifact@v7
with:
name: dusk-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}}
@@ -77,7 +74,7 @@ jobs:
build-apple:
name: Build Apple (${{matrix.name}})
- runs-on: [self-hosted, macOS]
+ runs-on: macos-latest
strategy:
fail-fast: false
matrix:
@@ -86,14 +83,14 @@ jobs:
platform: macos
preset: x-macos-ci-arm64
artifact_name: macos-appleclang-arm64
- # - name: AppleClang macOS x86_64
- # platform: macos
- # preset: x-macos-ci-x86_64
- # artifact_name: macos-appleclang-x86_64
- # - name: AppleClang iOS arm64
- # platform: ios
- # preset: x-ios-ci
- # artifact_name: ios-appleclang-arm64
+ - name: AppleClang macOS x86_64
+ platform: macos
+ preset: x-macos-ci-x86_64
+ artifact_name: macos-appleclang-x86_64
+ - name: AppleClang iOS arm64
+ platform: ios
+ preset: x-ios-ci
+ artifact_name: ios-appleclang-arm64
# - name: AppleClang tvOS arm64
# platform: tvos
# preset: x-tvos-ci
@@ -106,7 +103,6 @@ jobs:
submodules: recursive
- name: Install dependencies
- if: 'false'
run: brew install cmake ninja
- name: Install Rust iOS target
@@ -128,7 +124,7 @@ jobs:
rustup target add x86_64-apple-darwin
- name: Setup sccache
- uses: mozilla-actions/sccache-action@v0.0.9
+ uses: mozilla-actions/sccache-action@v0.0.10
- name: Configure CMake
run: cmake --preset ${{matrix.preset}}
@@ -137,7 +133,6 @@ jobs:
run: cmake --build --preset ${{matrix.preset}}
- name: Upload artifacts
- if: startsWith(github.event.ref, 'refs/tags/v')
uses: actions/upload-artifact@v7
with:
name: dusk-${{env.DUSK_VERSION}}-${{matrix.artifact_name}}
@@ -145,6 +140,73 @@ jobs:
build/install/Dusk.app
build/install/debug.tar.*
+ build-android:
+ name: Build Android (${{matrix.name}})
+ runs-on: ubuntu-latest
+
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - name: Clang arm64-v8a
+ preset: x-android-ci-arm64
+ abi: arm64-v8a
+ artifact_arch: arm64
+ rust_target: aarch64-linux-android
+
+ env:
+ ANDROID_NDK_VERSION: "29.0.14206865"
+
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ fetch-depth: 0
+ submodules: recursive
+
+ - name: Install dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get -y install ninja-build
+
+ - name: Setup Java
+ uses: actions/setup-java@v5
+ with:
+ distribution: temurin
+ java-version: 17
+
+ - name: Setup Android SDK
+ uses: android-actions/setup-android@v4
+
+ - name: Install Android SDK packages
+ run: sdkmanager "platforms;android-36" "build-tools;36.1.0" "ndk;${ANDROID_NDK_VERSION}"
+
+ - name: Install Rust Android target
+ run: |
+ rustup toolchain install stable
+ rustup target add ${{matrix.rust_target}}
+
+ - name: Setup sccache
+ uses: mozilla-actions/sccache-action@v0.0.10
+
+ - name: Configure CMake
+ run: cmake --preset ${{matrix.preset}}
+
+ - name: Build native library
+ run: cmake --build --preset ${{matrix.preset}} --target dusk
+
+ - name: Stage stripped JNI library
+ run: ANDROID_STAGE_ABIS="${{matrix.abi}}" platforms/android/scripts/stage-jni-libs.sh
+
+ - name: Build APK
+ working-directory: platforms/android
+ run: ./gradlew :app:assembleRelease --rerun-tasks
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v7
+ with:
+ name: dusk-${{env.DUSK_VERSION}}-android-${{matrix.artifact_arch}}
+ path: platforms/android/app/build/outputs/apk/release/app-${{matrix.abi}}-release-unsigned.apk
+
build-windows:
name: Build Windows (${{matrix.name}})
runs-on: ${{matrix.runner}}
@@ -154,7 +216,7 @@ jobs:
matrix:
include:
- name: MSVC x86_64
- runner: [self-hosted, Windows]
+ runner: windows-latest
preset: msvc
msvc_arch: amd64
vcpkg_arch: x64
@@ -191,7 +253,6 @@ jobs:
uses: mozilla-actions/sccache-action@v0.0.9
- name: Install dependencies
- if: 'false' # disabled for self-hosted
run: |
choco install ninja
vcpkg install freetype:${{matrix.vcpkg_arch}}-windows-static zstd:${{matrix.vcpkg_arch}}-windows-static
@@ -203,7 +264,6 @@ jobs:
run: cmake --build --preset x-windows-ci-${{matrix.preset}}
- name: Upload artifacts
- if: startsWith(github.event.ref, 'refs/tags/v')
uses: actions/upload-artifact@v7
with:
name: dusk-${{env.DUSK_VERSION}}-win32-msvc-${{matrix.artifact_arch}}
diff --git a/CMakeLists.txt b/CMakeLists.txt
index b00aa1201f..6290972f96 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -48,13 +48,15 @@ else ()
message(STATUS "Unable to find git, commit information will not be available")
endif ()
-if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)(-([0-9]+).*)?$")
+if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$")
set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
- if (CMAKE_MATCH_5)
- set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${CMAKE_MATCH_5}")
- else ()
- set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.0")
+ set(DUSK_VERSION_TWEAK "0")
+ if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$")
+ set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
+ elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$")
+ set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
endif ()
+ set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}")
else ()
set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION")
set(DUSK_VERSION_STRING "0.0.0.0")
@@ -113,26 +115,27 @@ add_subdirectory(libs/freeverb)
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)
-if (ANDROID)
- set(DUSK_ENABLE_UPDATE_CHECKER_DEFAULT OFF)
-else ()
- set(DUSK_ENABLE_UPDATE_CHECKER_DEFAULT ON)
-endif ()
-option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking support" ${DUSK_ENABLE_UPDATE_CHECKER_DEFAULT})
-option(DUSK_TPHD "Enable Twilight Princess HD asset support" ON)
+option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking support" ON)
+option(DUSK_TPHD "Enable TPHD asset support" ON)
if(ANDROID)
set(DUSK_MOVIE_SUPPORT OFF)
- set(NOD_COMPRESS_BZIP2 OFF CACHE BOOL "" FORCE)
- set(NOD_COMPRESS_LZMA OFF CACHE BOOL "" FORCE)
- set(NOD_COMPRESS_ZLIB OFF CACHE BOOL "" FORCE)
- set(NOD_COMPRESS_ZSTD OFF CACHE BOOL "" FORCE)
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")
+# Edit & Continue
+if (MSVC)
+ if ("${CMAKE_MSVC_DEBUG_INFORMATION_FORMAT}" STREQUAL "" AND CMAKE_BUILD_TYPE STREQUAL "Debug")
+ set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "EditAndContinue")
+ endif ()
+ if (CMAKE_MSVC_DEBUG_INFORMATION_FORMAT STREQUAL "EditAndContinue")
+ add_link_options("/INCREMENTAL")
+ endif ()
+endif ()
+
if (DUSK_MOVIE_SUPPORT)
find_package(libjpeg-turbo 3.0 CONFIG QUIET)
if (libjpeg-turbo_FOUND)
@@ -162,6 +165,7 @@ if (DUSK_MOVIE_SUPPORT)
CMAKE_C_COMPILER_LAUNCHER
CMAKE_MAKE_PROGRAM
CMAKE_MSVC_RUNTIME_LIBRARY
+ CMAKE_MSVC_DEBUG_INFORMATION_FORMAT
CMAKE_OSX_ARCHITECTURES
DEPLOYMENT_TARGET
ENABLE_ARC
@@ -346,6 +350,10 @@ if (DUSK_ENABLE_UPDATE_CHECKER)
list(APPEND GAME_LIBS winhttp)
list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_WINHTTP=1)
message(STATUS "dusk: Enabled update checker (WinHTTP)")
+ elseif (ANDROID)
+ set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/android.cpp)
+ list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_ANDROID=1)
+ message(STATUS "dusk: Enabled update checker (Android)")
elseif (APPLE)
find_library(FOUNDATION_FRAMEWORK Foundation REQUIRED)
set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/url_session.mm)
@@ -387,16 +395,6 @@ if (DUSK_ENABLE_DISCORD AND NOT ANDROID AND NOT IOS AND NOT TVOS)
list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD=1)
endif ()
-# Edit & Continue
-if (MSVC)
- if ("${CMAKE_MSVC_DEBUG_INFORMATION_FORMAT}" STREQUAL "" AND CMAKE_BUILD_TYPE STREQUAL "Debug")
- set(CMAKE_MSVC_DEBUG_INFORMATION_FORMAT "EditAndContinue")
- endif ()
- if (CMAKE_MSVC_DEBUG_INFORMATION_FORMAT STREQUAL "EditAndContinue")
- add_link_options("/INCREMENTAL")
- endif ()
-endif ()
-
if(ANDROID)
list(APPEND GAME_COMPILE_DEFS TARGET_ANDROID=1)
endif ()
diff --git a/CMakePresets.json b/CMakePresets.json
index 92799249a9..ea58a72243 100644
--- a/CMakePresets.json
+++ b/CMakePresets.json
@@ -30,7 +30,7 @@
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache",
"DUSK_ENABLE_SENTRY_NATIVE": {
"type": "BOOL",
- "value": true
+ "value": false
},
"DUSK_SENTRY_DSN": "$env{SENTRY_DSN}",
"DUSK_SENTRY_ENVIRONMENT": "production"
@@ -352,6 +352,25 @@
"ANDROID_ABI": "x86_64"
}
},
+ {
+ "name": "x-android-ci",
+ "hidden": true,
+ "inherits": [
+ "android-base",
+ "ci"
+ ]
+ },
+ {
+ "name": "x-android-ci-arm64",
+ "binaryDir": "${sourceDir}/build/android-arm64",
+ "inherits": [
+ "x-android-ci"
+ ],
+ "cacheVariables": {
+ "ANDROID_ABI": "arm64-v8a",
+ "Rust_CARGO_TARGET": "aarch64-linux-android"
+ }
+ },
{
"name": "x-linux-ci",
"hidden": true,
@@ -412,6 +431,7 @@
"x-macos-ci"
],
"cacheVariables": {
+ "AURORA_DAWN_PROVIDER": "vendor",
"CMAKE_OSX_ARCHITECTURES": "x86_64",
"Rust_CARGO_TARGET": "x86_64-apple-darwin"
}
@@ -555,6 +575,15 @@
"dusk"
]
},
+ {
+ "name": "x-android-ci-arm64",
+ "configurePreset": "x-android-ci-arm64",
+ "description": "(Internal) Android CI arm64-v8a",
+ "displayName": "(Internal) Android CI arm64-v8a",
+ "targets": [
+ "dusk"
+ ]
+ },
{
"name": "windows-msvc-debug",
"configurePreset": "windows-msvc-debug",
diff --git a/README.md b/README.md
index 5e7412e6aa..652eae2538 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
Official Website
•
- Discord
+ Discord
@@ -19,6 +19,9 @@ It aims to be as accurate as possible to the original while also providing new o
> [!IMPORTANT]
> Dusk does *not* provide any copyrighted assets. You must provide your own copy of the original game.
+> [!IMPORTANT]
+> At a minimum, Dusk 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.
+
### 1. Verify your dump
First, make sure your dump of the game is clean and supported by Dusk. You can do this by checking the SHA-1 hash of your dump against this list of supported versions:
@@ -28,20 +31,31 @@ First, make sure your dump of the game is clean and supported by Dusk. You can d
| GameCube USA | `75edd3ddff41f125d1b4ce1a40378f1b565519e7` |
| GameCube EUR | `2601822a488eeb86fb89db16ca8f29c2c953e1ca` |
+*Support for other versions of the game is planned in the future.
+
### 2. Download [Dusk](https://github.com/TwilitRealm/dusk/releases)
### 3. Setup the game
-
+**Windows / macOS / Linux**
- Extract the .zip file
- Launch Dusk
-- Press **Select Disc Image** and provide the path to your supported game dump.
+- Press **Select Disc Image** and provide the path to your supported game dump
+- Press **Play**!
+
+**iOS**
+- Follow the [iOS setup guide](docs/ios-install-altstore.md)
+
+**Android**
+- Install the Dusk apk
+- Launch Dusk
+- Press **Select Disc Image** and provide the path to your supported game dump
- Press **Play**!
# Building
If you'd like to build Dusk from source, please read the [build instructions](docs/building.md).
-Pull requests are welcomed! Note that we do not accept contributions that are primarily AI-generated and will close your PR if we suspect as much.
+Pull requests are welcomed! Note that we do not accept contributions that are primarily AI-generated and will close your PR if we suspect as much. Please also see the [code conventions](docs/code-conventions.md).
# Credits
diff --git a/ci/build-appimage.sh b/ci/build-appimage.sh
index 078d6cabf0..cef4ddfaf0 100755
--- a/ci/build-appimage.sh
+++ b/ci/build-appimage.sh
@@ -1,18 +1,26 @@
#!/bin/bash -ex
-shopt -s extglob
+
+if [[ -n "${GITHUB_WORKSPACE:-}" ]]; then
+ cd "$GITHUB_WORKSPACE"
+fi
+
+build_dir="$PWD/build"
+linuxdeploy="$build_dir/linuxdeploy-$(uname -m).AppImage"
# Get linuxdeploy
-cd "$RUNNER_WORKSPACE"
-curl -fOL https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-$(uname -m).AppImage
-chmod +x linuxdeploy-$(uname -m).AppImage
+mkdir -p "$build_dir"
+curl -fL "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-$(uname -m).AppImage" -o "$linuxdeploy"
+chmod +x "$linuxdeploy"
# Build AppImage
-cd "$GITHUB_WORKSPACE"
mkdir -p build/appdir/usr/{bin,share/{applications,icons/hicolor}}
-cp -r build/install/!(*.*) build/appdir/usr/bin
+for install_path in build/install/*; do
+ [[ "$(basename "$install_path")" == *.* ]] && continue
+ cp -r "$install_path" build/appdir/usr/bin
+done
cp -r platforms/freedesktop/{16x16,32x32,48x48,64x64,128x128,256x256,512x512,1024x1024} build/appdir/usr/share/icons/hicolor
cp platforms/freedesktop/dusk.desktop build/appdir/usr/share/applications
cd build/install
-VERSION="$DUSK_VERSION" NO_STRIP=1 "$RUNNER_WORKSPACE"/linuxdeploy-$(uname -m).AppImage \
- -l /usr/lib/x86_64-linux-gnu/libusb-1.0.so --appdir "$GITHUB_WORKSPACE"/build/appdir --output appimage
+VERSION="$DUSK_VERSION" NO_STRIP=1 "$linuxdeploy" \
+ -l /usr/lib/x86_64-linux-gnu/libusb-1.0.so --appdir "$build_dir/appdir" --output appimage
diff --git a/docs/code-conventions.md b/docs/code-conventions.md
new file mode 100644
index 0000000000..9c2659ac02
--- /dev/null
+++ b/docs/code-conventions.md
@@ -0,0 +1,13 @@
+# Code conventions for Dusk
+
+## Upstream when appropriate
+
+Bug fixes, documentation improvements, code cleanup, etc that also apply to the [original decompilation project](https://github.com/zeldaret/tp) should preferably be PR'd there.
+
+## Properly indicate Dusk-modified code
+
+When modifying original game code (i.e. in decomp) for Dusk's purposes, please clearly delineate such code as being Dusk-specific. Generally, this can be done by using `#if TARGET_PC` and keeping the original code in place. Use `#if AVOID_UB` for Undefined Behavior fixes to the original codebase.
+
+## Miscellaneous things
+
+* The original codebase makes heavy use of global `operator new` and similar overloads to allocate into a strict tree of heaps. This would cause many linkage headaches for us, so effectively all uses of `new` or `delete` in the original game code have been replaced with `JKR_NEW`, `JKR_DELETE`, or similar macros. See `JKRHeap.h` for the full list.
diff --git a/docs/ios-install-altstore.md b/docs/ios-install-altstore.md
new file mode 100644
index 0000000000..fdfc7689d6
--- /dev/null
+++ b/docs/ios-install-altstore.md
@@ -0,0 +1,46 @@
+# Installing Dusk on iOS via AltStore
+
+## Prerequisites
+
+- Mac with Homebrew installed
+- iPhone connected via USB
+- Dusk IPA file (download the latest `Dusk-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)
+
+## 1. Install AltServer
+
+```sh
+brew install altserver
+open -a AltServer
+```
+
+AltServer will appear in your menu bar.
+
+## 2. Enable Developer Mode (iOS 16+)
+
+- On your iPhone, go to **Settings > Privacy & Security > Developer Mode**
+- Toggle it on and restart when prompted
+
+## 3. Install AltStore 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**
+
+## 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:
+
+- **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
diff --git a/flake.nix b/flake.nix
index cd703bda64..c8e38a849b 100644
--- a/flake.nix
+++ b/flake.nix
@@ -5,9 +5,73 @@
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=";
+ };
+ # Dusk Actual
dusk = pkgs.stdenv.mkDerivation {
name = "dusk";
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 dusk $out/bin/dusk
+ cp -r ./res $out/bin/res
+ '';
nativeBuildInputs = [
pkgs.cmake
pkgs.pkg-config
@@ -25,6 +89,13 @@
pkgs.libjpeg8
pkgs.libxkbcommon
pkgs.libglvnd
+ pkgs.cxxopts
+ pkgs.abseil-cpp
+ pkgs.sdl3
+ pkgs.fmt
+ pkgs.tracy
+ pkgs.freetype
+ pkgs.zstd
];
};
in {
diff --git a/include/d/actor/d_a_alink.h b/include/d/actor/d_a_alink.h
index 9980ab776f..107afc1ed3 100644
--- a/include/d/actor/d_a_alink.h
+++ b/include/d/actor/d_a_alink.h
@@ -4552,6 +4552,18 @@ public:
void handleWolfHowl();
void handleQuickTransform();
bool checkGyroAimContext();
+
+ void onIronBallChainInterpCallback();
+
+ static const int IRON_BALL_CHAIN_COUNT = 102;
+ cXyz mIBChainInterpPrevPos[IRON_BALL_CHAIN_COUNT];
+ cXyz mIBChainInterpCurrPos[IRON_BALL_CHAIN_COUNT];
+ csXyz mIBChainInterpPrevAngle[IRON_BALL_CHAIN_COUNT];
+ csXyz mIBChainInterpCurrAngle[IRON_BALL_CHAIN_COUNT];
+ cXyz mIBChainInterpPrevHandRoot;
+ cXyz mIBChainInterpCurrHandRoot;
+ bool mIBChainInterpPrevValid;
+ bool mIBChainInterpCurrValid;
#endif
}; // Size: 0x385C
diff --git a/include/d/actor/d_a_e_db.h b/include/d/actor/d_a_e_db.h
index a95722a6d1..4f10715443 100644
--- a/include/d/actor/d_a_e_db.h
+++ b/include/d/actor/d_a_e_db.h
@@ -80,6 +80,12 @@ public:
/* 0x125C */ u32 field_0x125c;
/* 0x1260 */ u8 field_0x1260[0x126C - 0x1260];
/* 0x126C */ u8 HIOInit;
+#if TARGET_PC
+ cXyz mStalkLineInterpPrev[12];
+ cXyz mStalkLineInterpCurr[12];
+ bool mStalkLineInterpPrevValid;
+ bool mStalkLineInterpCurrValid;
+#endif
};
STATIC_ASSERT(sizeof(e_db_class) == 0x1270);
diff --git a/include/d/actor/d_a_e_hb.h b/include/d/actor/d_a_e_hb.h
index a7e0241007..3069bcd325 100644
--- a/include/d/actor/d_a_e_hb.h
+++ b/include/d/actor/d_a_e_hb.h
@@ -73,6 +73,12 @@ public:
/* 0x124C */ f32 field_0x124c;
/* 0x1250 */ u8 field_0x1250[0x1264 - 0x1250];
/* 0x1264 */ u8 HIOInit;
+#if TARGET_PC
+ cXyz mStalkLineInterpPrev[12];
+ cXyz mStalkLineInterpCurr[12];
+ bool mStalkLineInterpPrevValid;
+ bool mStalkLineInterpCurrValid;
+#endif
};
STATIC_ASSERT(sizeof(e_hb_class) == 0x1268);
diff --git a/include/d/actor/d_a_e_s1.h b/include/d/actor/d_a_e_s1.h
index a631ba47ac..5cbae84696 100644
--- a/include/d/actor/d_a_e_s1.h
+++ b/include/d/actor/d_a_e_s1.h
@@ -81,6 +81,15 @@ public:
/* 0x306D */ u8 field_0x306D[0x307C - 0x306D];
/* 0x307C */ u32 mBodyEffEmtrID;
/* 0x3080 */ u8 mInitHIO;
+
+#if TARGET_PC
+ static const int HAIR_STRAND_COUNT = 22;
+ static const int HAIR_SEGMENT_COUNT = 16;
+ cXyz mHairInterpPrev[HAIR_STRAND_COUNT * HAIR_SEGMENT_COUNT];
+ cXyz mHairInterpCurr[HAIR_STRAND_COUNT * HAIR_SEGMENT_COUNT];
+ bool mHairInterpPrevValid;
+ bool mHairInterpCurrValid;
+#endif
};
STATIC_ASSERT(sizeof(e_s1_class) == 0x3084);
diff --git a/include/d/actor/d_a_e_yd.h b/include/d/actor/d_a_e_yd.h
index 188e435ba5..44d6036600 100644
--- a/include/d/actor/d_a_e_yd.h
+++ b/include/d/actor/d_a_e_yd.h
@@ -74,6 +74,12 @@ public:
/* 0x1250 */ f32 field_0x1250;
/* 0x1254 */ u8 field_0x1254[0x1268 - 0x1254];
/* 0x1268 */ u8 field_0x1268;
+#if TARGET_PC
+ cXyz mLineMatInterpPrev[12];
+ cXyz mLineMatInterpCurr[12];
+ bool mLineMatInterpPrevValid;
+ bool mLineMatInterpCurrValid;
+#endif
};
STATIC_ASSERT(sizeof(e_yd_class) == 0x126c);
diff --git a/include/d/actor/d_a_e_yg.h b/include/d/actor/d_a_e_yg.h
index 69a85430a8..6be19933cb 100644
--- a/include/d/actor/d_a_e_yg.h
+++ b/include/d/actor/d_a_e_yg.h
@@ -63,6 +63,15 @@ public:
/* 0x0BB4 */ yg_ke_s mYgKes[13];
/* 0x1880 */ mDoExt_3DlineMat0_c mLineMat;
/* 0x189C */ u8 mIsFirstSpawn;
+
+#if TARGET_PC
+ static const int TENTACLE_STRAND_COUNT = 13;
+ static const int TENTACLE_SEGMENT_COUNT = 10;
+ cXyz mTentacleInterpPrev[TENTACLE_STRAND_COUNT * TENTACLE_SEGMENT_COUNT];
+ cXyz mTentacleInterpCurr[TENTACLE_STRAND_COUNT * TENTACLE_SEGMENT_COUNT];
+ bool mTentacleInterpPrevValid;
+ bool mTentacleInterpCurrValid;
+#endif
};
STATIC_ASSERT(sizeof(e_yg_class) == 0x18a0);
diff --git a/include/d/actor/d_a_e_yh.h b/include/d/actor/d_a_e_yh.h
index 519e5e2779..63e8ac1882 100644
--- a/include/d/actor/d_a_e_yh.h
+++ b/include/d/actor/d_a_e_yh.h
@@ -77,6 +77,12 @@ public:
/* 0x1260 */ u32 field_0x1260;
/* 0x1260 */ u8 field_0x1264[0x1270 - 0x1264];
/* 0x1270 */ bool mIsHIOOwner;
+#if TARGET_PC
+ cXyz mLineInterpPrev[12];
+ cXyz mLineInterpCurr[12];
+ bool mLineInterpPrevValid;
+ bool mLineInterpCurrValid;
+#endif
};
STATIC_ASSERT(sizeof(e_yh_class) == 0x1274);
diff --git a/include/d/actor/d_a_obj_fchain.h b/include/d/actor/d_a_obj_fchain.h
index 1bd7b9a810..7c4ae74faf 100644
--- a/include/d/actor/d_a_obj_fchain.h
+++ b/include/d/actor/d_a_obj_fchain.h
@@ -31,6 +31,10 @@ public:
csXyz* getAngle() { return field_0x8a4; }
J3DModelData* getModelData() { return mModelData; }
+#if TARGET_PC
+ void onInterpCallback();
+#endif
+
private:
/* 0x568 */ request_of_phase_process_class mPhase;
/* 0x570 */ J3DModelData* mModelData;
@@ -42,6 +46,14 @@ private:
/* 0x694 */ cXyz field_0x694[22];
/* 0x79C */ cXyz field_0x79c[22];
/* 0x8A4 */ csXyz field_0x8a4[22];
+
+#if TARGET_PC
+ static const int CHAIN_COUNT = 22;
+ cXyz mChainInterpPrev[CHAIN_COUNT];
+ cXyz mChainInterpCurr[CHAIN_COUNT];
+ bool mChainInterpPrevValid;
+ bool mChainInterpCurrValid;
+#endif
};
STATIC_ASSERT(sizeof(daObjFchain_c) == 0x928);
diff --git a/include/dusk/gyro.h b/include/dusk/gyro.h
index 279aeae16e..a206100739 100644
--- a/include/dusk/gyro.h
+++ b/include/dusk/gyro.h
@@ -12,6 +12,7 @@ void rollgoalTableOffset(s16& out_ax, s16& out_az);
extern bool s_sensor_keep_alive;
bool get_sensor_keep_alive();
void set_sensor_keep_alive(bool value);
+bool rollgoal_gyro_enabled();
} // namespace dusk::gyro
#endif
diff --git a/include/dusk/io.hpp b/include/dusk/io.hpp
index fb71a77d68..2efc4a8d3b 100644
--- a/include/dusk/io.hpp
+++ b/include/dusk/io.hpp
@@ -1,6 +1,7 @@
#ifndef DUSK_IO_HPP
#define DUSK_IO_HPP
+#include
#include
// I can't believe it's 2026 and neither SDL (no error codes) nor
@@ -15,7 +16,7 @@ namespace dusk::io {
* Methods on this class throw appropriate C++ exceptions when an error occurs.
*/
class FileStream {
- void* file;
+ FILE* file;
public:
FileStream() noexcept;
@@ -23,7 +24,7 @@ public:
/**
* \brief Take ownership of a FILE* handle.
*/
- explicit FileStream(void* file);
+ explicit FileStream(FILE* file);
FileStream(const FileStream& other) = delete;
FileStream(FileStream&& other) noexcept;
@@ -34,6 +35,11 @@ public:
*/
static FileStream OpenRead(const char* utf8Path);
+ /**
+ * \brief Open a file for reading at the given path.
+ */
+ static FileStream OpenRead(const std::filesystem::path& path);
+
/**
* \brief Create a file for writing.
*
@@ -41,16 +47,33 @@ public:
*/
static FileStream Create(const char* utf8Path);
+ /**
+ * \brief Create a file for writing.
+ *
+ * If there is an existing file, its contents are demolished.
+ */
+ static FileStream Create(const std::filesystem::path& path);
+
/**
* \brief Read the byte contents of a file directly into a vector.
*/
static std::vector ReadAllBytes(const char* utf8Path);
+ /**
+ * \brief Read the byte contents of a file directly into a vector.
+ */
+ static std::vector ReadAllBytes(const std::filesystem::path& path);
+
/**
* \brief Read the byte contents of a file directly into a vector.
*/
static void WriteAllText(const char* utf8Path, std::string_view text);
+ /**
+ * \brief Read the byte contents of a file directly into a vector.
+ */
+ static void WriteAllText(const std::filesystem::path& path, std::string_view text);
+
/**
* \brief Read the remaining contents of the file directly into a vector.
*/
@@ -59,17 +82,24 @@ public:
/**
* Get direct access to the underlying FILE* handle.
*/
- [[nodiscard]] void* GetFileHandle() const noexcept {
- return file;
- }
+ [[nodiscard]] void* GetFileHandle() const noexcept { return file; }
/**
* Write data to the file.
*/
void Write(const char* data, size_t dataLen);
+
+ FILE* ToInner();
};
+/**
+ * Converts a std::filesystem::path to a std::string, UTF-8, without exploding on Windows.
+ */
+inline std::string fs_path_to_string(const std::filesystem::path& path) {
+ const auto u8str = path.u8string();
+ return {reinterpret_cast(u8str.c_str())};
}
+} // namespace dusk::io
#endif // DUSK_IO_HPP
diff --git a/include/dusk/main.h b/include/dusk/main.h
index d6b9c9927f..c5c4fb27d1 100644
--- a/include/dusk/main.h
+++ b/include/dusk/main.h
@@ -7,11 +7,18 @@
#include
+#if defined(_WIN32) || \
+ (defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_TV && !TARGET_OS_MACCATALYST) || \
+ (defined(__linux__) && !defined(__ANDROID__))
+#define DUSK_CAN_OPEN_DATA_FOLDER 1
+#else
+#define DUSK_CAN_OPEN_DATA_FOLDER 0
+#endif
+
namespace dusk {
extern bool IsRunning;
extern bool IsShuttingDown;
extern bool IsGameLaunched;
- extern bool IsFocusPaused;
extern bool RestartRequested;
extern std::filesystem::path ConfigPath;
@@ -23,6 +30,7 @@ namespace dusk {
#endif
void RequestRestart() noexcept;
+ bool OpenDataFolder();
}
#endif // DUSK_MAIN_H
diff --git a/include/dusk/settings.h b/include/dusk/settings.h
index 8885c9e3ef..5622b64f96 100644
--- a/include/dusk/settings.h
+++ b/include/dusk/settings.h
@@ -27,6 +27,11 @@ enum class DiscVerificationState : u8 {
HashMismatch,
};
+enum class GyroMode : u8 {
+ Sensor = 0,
+ Mouse = 1,
+};
+
namespace config {
template <>
struct ConfigEnumRange {
@@ -45,6 +50,12 @@ struct ConfigEnumRange {
static constexpr auto min = DiscVerificationState::Unknown;
static constexpr auto max = DiscVerificationState::HashMismatch;
};
+
+template <>
+struct ConfigEnumRange {
+ static constexpr auto min = GyroMode::Sensor;
+ static constexpr auto max = GyroMode::Mouse;
+};
}
// Persistent user settings
@@ -102,8 +113,8 @@ struct UserSettings {
ConfigVar minimalHUD;
ConfigVar pauseOnFocusLost;
ConfigVar enableLinkDollRotation;
- ConfigVar enableAchievementNotifications;
-
+ ConfigVar enableAchievementToasts;
+ ConfigVar enableControllerToasts;
// Graphics
ConfigVar bloomMode;
@@ -120,6 +131,7 @@ struct UserSettings {
ConfigVar midnasLamentNonStop;
// Input
+ ConfigVar gyroMode;
ConfigVar enableGyroAim;
ConfigVar enableGyroRollgoal;
ConfigVar gyroSensitivityX;
@@ -135,6 +147,7 @@ struct UserSettings {
ConfigVar freeCameraSensitivity;
ConfigVar debugFlyCam;
ConfigVar debugFlyCamLockEvents;
+ ConfigVar allowBackgroundInput;
// Cheats
ConfigVar infiniteHearts;
diff --git a/platforms/android/README.md b/platforms/android/README.md
index c7ecd684c1..f51b37db73 100644
--- a/platforms/android/README.md
+++ b/platforms/android/README.md
@@ -66,12 +66,11 @@ Output APK:
You can pass command-line args through the activity intent:
```bash
-adb shell am start -n com.twilitrealm.dusk/.DuskActivity \
- --es dusk_args "'/sdcard/Download/The Legend of Zelda: Twilight Princess (USA).iso'"
+adb shell am start -n dev.twilitrealm.dusk/.DuskActivity \
+ --es dusk_args "--backend vulkan"
```
Supported extras:
- `dusk_args`: single shell-like argument string
- `dusk_argv`: string-array argv
-- `dusk_disc`: compatibility shortcut (single ISO path)
diff --git a/platforms/android/app/build.gradle b/platforms/android/app/build.gradle
index 9375d06910..b7775df8e0 100644
--- a/platforms/android/app/build.gradle
+++ b/platforms/android/app/build.gradle
@@ -2,12 +2,22 @@ plugins {
id 'com.android.application'
}
+def duskRepoDir = rootProject.projectDir.parentFile.parentFile
+def duskGeneratedAssetsDir = layout.buildDirectory.dir('generated/assets/dusk').get().asFile
+def syncDuskAssets = tasks.register('syncDuskAssets', Sync) {
+ from(new File(duskRepoDir, 'res')) {
+ into 'res'
+ exclude '**/.DS_Store'
+ }
+ into duskGeneratedAssetsDir
+}
+
android {
- namespace 'com.twilitrealm.dusk'
+ namespace 'dev.twilitrealm.dusk'
compileSdk 36
defaultConfig {
- applicationId 'com.twilitrealm.dusk'
+ applicationId 'dev.twilitrealm.dusk'
minSdk 26
targetSdk 36
versionCode 1
@@ -27,7 +37,7 @@ android {
sourceSets {
main {
jniLibs.srcDirs = ['src/main/jniLibs']
- assets.srcDirs = ['../../assets']
+ assets.srcDirs = [duskGeneratedAssetsDir]
}
}
@@ -48,3 +58,10 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
+
+tasks.configureEach { task ->
+ if ((task.name.startsWith('merge') && task.name.endsWith('Assets')) ||
+ task.name.toLowerCase().contains('lint')) {
+ task.dependsOn(syncDuskAssets)
+ }
+}
diff --git a/platforms/android/app/proguard-rules.pro b/platforms/android/app/proguard-rules.pro
index 952161ca8b..72b7ca16fa 100644
--- a/platforms/android/app/proguard-rules.pro
+++ b/platforms/android/app/proguard-rules.pro
@@ -1,2 +1,4 @@
# Keep SDL activity and related JNI bridge methods.
-keep class org.libsdl.app.** { *; }
+-keep class dev.twilitrealm.dusk.DuskHttpClient { *; }
+-keep class dev.twilitrealm.dusk.DuskHttpClient$Response { *; }
diff --git a/platforms/android/app/src/main/AndroidManifest.xml b/platforms/android/app/src/main/AndroidManifest.xml
index e8c2bb29a8..f5a0365856 100644
--- a/platforms/android/app/src/main/AndroidManifest.xml
+++ b/platforms/android/app/src/main/AndroidManifest.xml
@@ -10,6 +10,7 @@
+
+
+
+
+
+
+
out = new ArrayList<>();
StringBuilder current = new StringBuilder();
@@ -61,18 +71,46 @@ public class DuskActivity extends SDLActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ hideSystemBars();
+ }
+ @Override
+ protected void onResume() {
+ super.onResume();
+ hideSystemBars();
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasFocus) {
+ super.onWindowFocusChanged(hasFocus);
+ if (hasFocus) {
+ hideSystemBars();
+ }
+ }
+
+ private void hideSystemBars() {
+ Window window = getWindow();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
- getWindow().getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars());
- }else {
- View decorView = getWindow().getDecorView();
- // Hide the status bar.
- int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
+ window.setDecorFitsSystemWindows(false);
+ WindowInsetsController ctrl = window.getDecorView().getWindowInsetsController();
+ if (ctrl != null) {
+ ctrl.setSystemBarsBehavior(
+ WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
+ ctrl.hide(WindowInsets.Type.systemBars());
+ }
+ } else {
+ View decorView = window.getDecorView();
+ int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
+ View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(uiOptions);
- // Remember that you should never show the action bar if the
- // status bar is hidden, so hide that too if necessary.
ActionBar actionBar = getActionBar();
- actionBar.hide();
+ if (actionBar != null) {
+ actionBar.hide();
+ }
}
}
@@ -100,12 +138,96 @@ public class DuskActivity extends SDLActivity {
return splitArgs(trimmed);
}
}
-
- String discPath = intent.getStringExtra("dusk_disc");
- if (discPath != null && !discPath.isEmpty()) {
- return new String[] { discPath };
- }
}
return new String[0];
}
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if (resultCode == RESULT_OK) {
+ persistUriPermissions(data);
+ }
+ super.onActivityResult(requestCode, resultCode, data);
+ }
+
+ private void persistUriPermissions(Intent data) {
+ if (data == null) {
+ return;
+ }
+
+ int permissionFlags =
+ data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ if (permissionFlags == 0) {
+ return;
+ }
+
+ Uri uri = data.getData();
+ if (uri != null) {
+ persistUriPermission(uri, permissionFlags);
+ }
+
+ ClipData clipData = data.getClipData();
+ if (clipData == null) {
+ return;
+ }
+ for (int i = 0; i < clipData.getItemCount(); ++i) {
+ Uri itemUri = clipData.getItemAt(i).getUri();
+ if (itemUri != null) {
+ persistUriPermission(itemUri, permissionFlags);
+ }
+ }
+ }
+
+ private void persistUriPermission(Uri uri, int permissionFlags) {
+ if ((permissionFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
+ persistUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION, "read");
+ }
+ if ((permissionFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
+ persistUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, "write");
+ }
+ }
+
+ private void persistUriPermission(Uri uri, int permissionFlag, String permissionName) {
+ try {
+ getContentResolver().takePersistableUriPermission(uri, permissionFlag);
+ } catch (SecurityException | IllegalArgumentException e) {
+ Log.w(TAG, "Unable to persist " + permissionName + " URI permission for " + uri, e);
+ }
+ }
+
+ public String getDisplayNameForUri(String uriString) {
+ if (uriString == null || uriString.isEmpty()) {
+ return "";
+ }
+
+ Uri uri = Uri.parse(uriString);
+ if ("content".equals(uri.getScheme())) {
+ try (Cursor cursor = getContentResolver().query(
+ uri, new String[] { OpenableColumns.DISPLAY_NAME }, null, null, null))
+ {
+ if (cursor != null && cursor.moveToFirst()) {
+ int displayNameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+ if (displayNameColumn >= 0) {
+ String displayName = cursor.getString(displayNameColumn);
+ if (displayName != null && !displayName.isEmpty()) {
+ return displayName;
+ }
+ }
+ }
+ } catch (SecurityException | IllegalArgumentException e) {
+ Log.w(TAG, "Unable to query display name for " + uri, e);
+ }
+ } else if ("file".equals(uri.getScheme())) {
+ String path = uri.getPath();
+ if (path != null && !path.isEmpty()) {
+ String name = new File(path).getName();
+ if (!name.isEmpty()) {
+ return name;
+ }
+ }
+ }
+
+ String lastSegment = uri.getLastPathSegment();
+ return lastSegment != null ? lastSegment : "";
+ }
}
diff --git a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskDocumentsProvider.java b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskDocumentsProvider.java
new file mode 100644
index 0000000000..79fa8894d0
--- /dev/null
+++ b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskDocumentsProvider.java
@@ -0,0 +1,413 @@
+package dev.twilitrealm.dusk;
+
+import android.content.ContentResolver;
+import android.content.res.AssetFileDescriptor;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+import android.provider.DocumentsContract;
+import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsContract.Root;
+import android.provider.DocumentsProvider;
+import android.webkit.MimeTypeMap;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+public class DuskDocumentsProvider extends DocumentsProvider {
+ public static final String AUTHORITY = "dev.twilitrealm.dusk.documents";
+
+ private static final String ROOT_ID = "dusk";
+ private static final String ROOT_DOCUMENT_ID = "root";
+ private static final String DIRECTORY_MIME_TYPE = Document.MIME_TYPE_DIR;
+
+ private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
+ Root.COLUMN_ROOT_ID,
+ Root.COLUMN_FLAGS,
+ Root.COLUMN_TITLE,
+ Root.COLUMN_DOCUMENT_ID,
+ Root.COLUMN_ICON,
+ Root.COLUMN_AVAILABLE_BYTES,
+ Root.COLUMN_SUMMARY
+ };
+
+ private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
+ Document.COLUMN_DOCUMENT_ID,
+ Document.COLUMN_DISPLAY_NAME,
+ Document.COLUMN_FLAGS,
+ Document.COLUMN_MIME_TYPE,
+ Document.COLUMN_LAST_MODIFIED,
+ Document.COLUMN_SIZE
+ };
+
+ @Override
+ public boolean onCreate() {
+ ensureUserDirectories();
+ return true;
+ }
+
+ @Override
+ public Cursor queryRoots(String[] projection) throws FileNotFoundException {
+ final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
+ final File root = getRootDirectory();
+ final MatrixCursor.RowBuilder row = result.newRow();
+
+ row.add(Root.COLUMN_ROOT_ID, ROOT_ID);
+ row.add(Root.COLUMN_FLAGS,
+ Root.FLAG_LOCAL_ONLY |
+ Root.FLAG_SUPPORTS_CREATE |
+ Root.FLAG_SUPPORTS_IS_CHILD);
+ row.add(Root.COLUMN_TITLE, getContext().getString(R.string.app_name));
+ row.add(Root.COLUMN_DOCUMENT_ID, ROOT_DOCUMENT_ID);
+ row.add(Root.COLUMN_ICON, R.mipmap.icon);
+ row.add(Root.COLUMN_AVAILABLE_BYTES, root.getFreeSpace());
+ row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.documents_provider_summary));
+
+ return result;
+ }
+
+ @Override
+ public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
+ final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
+ includeDocument(result, documentId, getFileForDocumentId(documentId));
+ return result;
+ }
+
+ @Override
+ public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
+ throws FileNotFoundException
+ {
+ return queryChildDocumentsInternal(parentDocumentId, projection);
+ }
+
+ @Override
+ public Cursor queryChildDocuments(String parentDocumentId, String[] projection, Bundle queryArgs)
+ throws FileNotFoundException
+ {
+ return queryChildDocumentsInternal(parentDocumentId, projection);
+ }
+
+ private Cursor queryChildDocumentsInternal(String parentDocumentId, String[] projection)
+ throws FileNotFoundException
+ {
+ final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
+ final File parent = getFileForDocumentId(parentDocumentId);
+ final File[] files = parent.listFiles();
+ result.setNotificationUri(getContext().getContentResolver(), getChildDocumentsUri(parentDocumentId));
+
+ if (files == null) {
+ return result;
+ }
+
+ for (File file : files) {
+ includeDocument(result, getDocumentIdForFile(file), file);
+ }
+
+ return result;
+ }
+
+ @Override
+ public boolean isChildDocument(String parentDocumentId, String documentId) {
+ try {
+ final File parent = getFileForDocumentId(parentDocumentId);
+ final File child = getFileForDocumentId(documentId);
+ return isInside(parent, child);
+ } catch (FileNotFoundException e) {
+ return false;
+ }
+ }
+
+ @Override
+ public String createDocument(String parentDocumentId, String mimeType, String displayName)
+ throws FileNotFoundException
+ {
+ final File parent = getFileForDocumentId(parentDocumentId);
+ if (!parent.isDirectory()) {
+ throw new FileNotFoundException("Parent is not a directory: " + parentDocumentId);
+ }
+
+ final String safeDisplayName = sanitizeDisplayName(displayName);
+ final File file = buildUniqueFile(parent, safeDisplayName);
+ final boolean created;
+ if (DIRECTORY_MIME_TYPE.equals(mimeType)) {
+ created = file.mkdir();
+ } else {
+ try {
+ created = file.createNewFile();
+ } catch (IOException e) {
+ throw asFileNotFound("Unable to create document", e);
+ }
+ }
+
+ if (!created) {
+ throw new FileNotFoundException("Unable to create document: " + displayName);
+ }
+
+ notifyChildrenChanged(parentDocumentId);
+ return getDocumentIdForFile(file);
+ }
+
+ @Override
+ public String renameDocument(String documentId, String displayName) throws FileNotFoundException {
+ final File file = getFileForDocumentId(documentId);
+ if (ROOT_DOCUMENT_ID.equals(documentId)) {
+ throw new FileNotFoundException("Cannot rename root document");
+ }
+
+ final File target = buildUniqueFile(file.getParentFile(), sanitizeDisplayName(displayName));
+ final String parentDocumentId = getDocumentIdForFile(file.getParentFile());
+ if (!file.renameTo(target)) {
+ throw new FileNotFoundException("Unable to rename document: " + documentId);
+ }
+ notifyDocumentChanged(documentId);
+ notifyDocumentChanged(getDocumentIdForFile(target));
+ notifyChildrenChanged(parentDocumentId);
+ return getDocumentIdForFile(target);
+ }
+
+ @Override
+ public void deleteDocument(String documentId) throws FileNotFoundException {
+ if (ROOT_DOCUMENT_ID.equals(documentId)) {
+ throw new FileNotFoundException("Cannot delete root document");
+ }
+
+ final File file = getFileForDocumentId(documentId);
+ final String parentDocumentId = getDocumentIdForFile(file.getParentFile());
+ deleteRecursively(file);
+ notifyDocumentChanged(documentId);
+ notifyChildrenChanged(parentDocumentId);
+ }
+
+ @Override
+ public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal)
+ throws FileNotFoundException
+ {
+ return ParcelFileDescriptor.open(getFileForDocumentId(documentId), modeToParcelMode(mode));
+ }
+
+ @Override
+ public AssetFileDescriptor openDocumentThumbnail(String documentId, android.graphics.Point sizeHint,
+ CancellationSignal signal) throws FileNotFoundException
+ {
+ throw new FileNotFoundException("Thumbnails are not supported");
+ }
+
+ private void includeDocument(MatrixCursor result, String documentId, File file) throws FileNotFoundException {
+ final MatrixCursor.RowBuilder row = result.newRow();
+ final boolean isDirectory = file.isDirectory();
+ final String displayName = ROOT_DOCUMENT_ID.equals(documentId)
+ ? getContext().getString(R.string.documents_provider_root_name)
+ : file.getName();
+
+ int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME;
+ if (isDirectory) {
+ flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
+ } else if (file.canWrite()) {
+ flags |= Document.FLAG_SUPPORTS_WRITE;
+ }
+ if (ROOT_DOCUMENT_ID.equals(documentId)) {
+ flags &= ~(Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME);
+ }
+
+ row.add(Document.COLUMN_DOCUMENT_ID, documentId);
+ row.add(Document.COLUMN_DISPLAY_NAME, displayName);
+ row.add(Document.COLUMN_FLAGS, flags);
+ row.add(Document.COLUMN_MIME_TYPE, isDirectory ? DIRECTORY_MIME_TYPE : getMimeType(file));
+ row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
+ row.add(Document.COLUMN_SIZE, isDirectory ? null : file.length());
+ }
+
+ private File getRootDirectory() throws FileNotFoundException {
+ final File root = getContext().getFilesDir();
+ if (root == null) {
+ throw new FileNotFoundException("Dusk files directory is unavailable");
+ }
+ return root;
+ }
+
+ private File getFileForDocumentId(String documentId) throws FileNotFoundException {
+ final File root = getRootDirectory();
+ if (ROOT_DOCUMENT_ID.equals(documentId)) {
+ return root;
+ }
+ if (!documentId.startsWith(ROOT_DOCUMENT_ID + "/")) {
+ throw new FileNotFoundException("Invalid document id: " + documentId);
+ }
+
+ final String relativePath = documentId.substring(ROOT_DOCUMENT_ID.length() + 1);
+ final File file = new File(root, relativePath);
+ if (!isInside(root, file)) {
+ throw new FileNotFoundException("Document escapes Dusk files directory: " + documentId);
+ }
+ if (!file.exists()) {
+ throw new FileNotFoundException("Document does not exist: " + documentId);
+ }
+ return file;
+ }
+
+ private String getDocumentIdForFile(File file) throws FileNotFoundException {
+ final File root = getRootDirectory();
+ if (sameFile(root, file)) {
+ return ROOT_DOCUMENT_ID;
+ }
+ if (!isInside(root, file)) {
+ throw new FileNotFoundException("File escapes Dusk files directory: " + file);
+ }
+
+ final String rootPath = canonicalPath(root);
+ final String filePath = canonicalPath(file);
+ return ROOT_DOCUMENT_ID + "/" + filePath.substring(rootPath.length() + 1);
+ }
+
+ private void ensureUserDirectories() {
+ final File root = getContext().getFilesDir();
+ if (root == null) {
+ return;
+ }
+ new File(root, "texture_replacements").mkdirs();
+ new File(root, "USA/Card A").mkdirs();
+ new File(root, "EUR/Card A").mkdirs();
+ }
+
+ private static String[] resolveRootProjection(String[] projection) {
+ return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
+ }
+
+ private static String[] resolveDocumentProjection(String[] projection) {
+ return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
+ }
+
+ private static String sanitizeDisplayName(String displayName) throws FileNotFoundException {
+ if (displayName == null) {
+ throw new FileNotFoundException("Document name is empty");
+ }
+
+ final String sanitized = displayName.trim();
+ if (sanitized.isEmpty() || ".".equals(sanitized) || "..".equals(sanitized) ||
+ sanitized.contains("/") || sanitized.contains("\\"))
+ {
+ throw new FileNotFoundException("Invalid document name: " + displayName);
+ }
+ return sanitized;
+ }
+
+ private static File buildUniqueFile(File parent, String displayName) {
+ File file = new File(parent, displayName);
+ if (!file.exists()) {
+ return file;
+ }
+
+ final int dot = displayName.lastIndexOf('.');
+ final String baseName = dot > 0 ? displayName.substring(0, dot) : displayName;
+ final String extension = dot > 0 ? displayName.substring(dot) : "";
+ for (int i = 1; i < 100; ++i) {
+ file = new File(parent, baseName + " (" + i + ")" + extension);
+ if (!file.exists()) {
+ return file;
+ }
+ }
+ return new File(parent, baseName + " (" + System.currentTimeMillis() + ")" + extension);
+ }
+
+ private static int modeToParcelMode(String mode) {
+ if ("r".equals(mode)) {
+ return ParcelFileDescriptor.MODE_READ_ONLY;
+ }
+ if ("w".equals(mode) || "wt".equals(mode)) {
+ return ParcelFileDescriptor.MODE_WRITE_ONLY |
+ ParcelFileDescriptor.MODE_CREATE |
+ ParcelFileDescriptor.MODE_TRUNCATE;
+ }
+ if ("wa".equals(mode)) {
+ return ParcelFileDescriptor.MODE_WRITE_ONLY |
+ ParcelFileDescriptor.MODE_CREATE |
+ ParcelFileDescriptor.MODE_APPEND;
+ }
+ if ("rw".equals(mode)) {
+ return ParcelFileDescriptor.MODE_READ_WRITE |
+ ParcelFileDescriptor.MODE_CREATE;
+ }
+ if ("rwt".equals(mode)) {
+ return ParcelFileDescriptor.MODE_READ_WRITE |
+ ParcelFileDescriptor.MODE_CREATE |
+ ParcelFileDescriptor.MODE_TRUNCATE;
+ }
+ return ParcelFileDescriptor.MODE_READ_ONLY;
+ }
+
+ private static String getMimeType(File file) {
+ final int dot = file.getName().lastIndexOf('.');
+ if (dot >= 0) {
+ final String extension = file.getName().substring(dot + 1).toLowerCase();
+ final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
+ if (mimeType != null) {
+ return mimeType;
+ }
+ }
+ return "application/octet-stream";
+ }
+
+ private Uri getChildDocumentsUri(String parentDocumentId) {
+ return DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId);
+ }
+
+ private void notifyChildrenChanged(String parentDocumentId) {
+ final ContentResolver resolver = getContext().getContentResolver();
+ resolver.notifyChange(getChildDocumentsUri(parentDocumentId), null, false);
+ }
+
+ private void notifyDocumentChanged(String documentId) {
+ final ContentResolver resolver = getContext().getContentResolver();
+ resolver.notifyChange(DocumentsContract.buildDocumentUri(AUTHORITY, documentId), null, false);
+ }
+
+ private static void deleteRecursively(File file) throws FileNotFoundException {
+ if (file.isDirectory()) {
+ final File[] children = file.listFiles();
+ if (children != null) {
+ for (File child : children) {
+ deleteRecursively(child);
+ }
+ }
+ }
+ if (!file.delete()) {
+ throw new FileNotFoundException("Unable to delete document: " + file);
+ }
+ }
+
+ private static boolean isInside(File parent, File child) {
+ try {
+ final String parentPath = canonicalPath(parent);
+ final String childPath = canonicalPath(child);
+ return childPath.equals(parentPath) || childPath.startsWith(parentPath + File.separator);
+ } catch (FileNotFoundException e) {
+ return false;
+ }
+ }
+
+ private static boolean sameFile(File a, File b) {
+ try {
+ return canonicalPath(a).equals(canonicalPath(b));
+ } catch (FileNotFoundException e) {
+ return false;
+ }
+ }
+
+ private static String canonicalPath(File file) throws FileNotFoundException {
+ try {
+ return file.getCanonicalPath();
+ } catch (IOException e) {
+ throw asFileNotFound("Unable to resolve path", e);
+ }
+ }
+
+ private static FileNotFoundException asFileNotFound(String message, IOException cause) {
+ final FileNotFoundException exception = new FileNotFoundException(message + ": " + cause.getMessage());
+ exception.initCause(cause);
+ return exception;
+ }
+}
diff --git a/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskHttpClient.java b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskHttpClient.java
new file mode 100644
index 0000000000..be160d6a2d
--- /dev/null
+++ b/platforms/android/app/src/main/java/com/twilitrealm/dusk/DuskHttpClient.java
@@ -0,0 +1,237 @@
+package dev.twilitrealm.dusk;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.SocketTimeoutException;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import javax.net.ssl.HttpsURLConnection;
+
+public final class DuskHttpClient {
+ public static final int ERROR_NONE = 0;
+ public static final int ERROR_INVALID_URL = 1;
+ public static final int ERROR_UNSUPPORTED_SCHEME = 2;
+ public static final int ERROR_TIMEOUT = 3;
+ public static final int ERROR_TOO_LARGE = 4;
+ public static final int ERROR_NETWORK = 5;
+
+ private static final int MAX_REDIRECTS = 5;
+
+ public static final class Response {
+ public int error;
+ public String message;
+ public int statusCode;
+ public String[] headerNames;
+ public String[] headerValues;
+ public byte[] body;
+
+ Response(int error, String message, int statusCode, String[] headerNames,
+ String[] headerValues, byte[] body) {
+ this.error = error;
+ this.message = message;
+ this.statusCode = statusCode;
+ this.headerNames = headerNames != null ? headerNames : new String[0];
+ this.headerValues = headerValues != null ? headerValues : new String[0];
+ this.body = body != null ? body : new byte[0];
+ }
+ }
+
+ private DuskHttpClient() {
+ }
+
+ public static Response get(String url, String[] headerNames, String[] headerValues,
+ int timeoutMs, long maxBodyBytes) {
+ if (url == null || url.isEmpty()) {
+ return fail(ERROR_INVALID_URL, "URL is empty");
+ }
+
+ try {
+ URL currentUrl = new URL(url);
+ if (!isHttps(currentUrl)) {
+ return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported");
+ }
+
+ for (int redirect = 0; redirect <= MAX_REDIRECTS; ++redirect) {
+ HttpsURLConnection connection =
+ (HttpsURLConnection) currentUrl.openConnection();
+ try {
+ connection.setRequestMethod("GET");
+ connection.setConnectTimeout(timeoutMs);
+ connection.setReadTimeout(timeoutMs);
+ connection.setUseCaches(false);
+ connection.setInstanceFollowRedirects(false);
+ applyHeaders(connection, headerNames, headerValues);
+
+ int statusCode = connection.getResponseCode();
+ if (isRedirect(statusCode)) {
+ String location = connection.getHeaderField("Location");
+ if (location == null || location.isEmpty()) {
+ return fail(ERROR_NETWORK, "Redirect response did not include Location",
+ statusCode, connection, new byte[0]);
+ }
+
+ URL nextUrl = new URL(currentUrl, location);
+ if (!isHttps(nextUrl)) {
+ return fail(ERROR_UNSUPPORTED_SCHEME,
+ "Only https:// redirects are supported", statusCode,
+ connection, new byte[0]);
+ }
+ currentUrl = nextUrl;
+ continue;
+ }
+
+ byte[] body = readBody(connection, statusCode, maxBodyBytes);
+ return success(statusCode, connection, body);
+ } catch (ResponseTooLargeException e) {
+ return fail(ERROR_TOO_LARGE, "Response body exceeded the configured limit",
+ safeStatusCode(connection), connection, e.partialBody);
+ } finally {
+ connection.disconnect();
+ }
+ }
+
+ return fail(ERROR_NETWORK, "Too many redirects");
+ } catch (MalformedURLException e) {
+ return fail(ERROR_INVALID_URL, "Failed to parse URL");
+ } catch (SocketTimeoutException e) {
+ return fail(ERROR_TIMEOUT, "Request timed out");
+ } catch (IOException e) {
+ String message = e.getMessage();
+ return fail(ERROR_NETWORK, message != null ? message : e.toString());
+ } catch (ClassCastException e) {
+ return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported");
+ }
+ }
+
+ private static void applyHeaders(HttpsURLConnection connection, String[] names,
+ String[] values) {
+ if (names == null || values == null) {
+ return;
+ }
+
+ int count = Math.min(names.length, values.length);
+ for (int i = 0; i < count; ++i) {
+ if (names[i] != null && values[i] != null) {
+ connection.setRequestProperty(names[i], values[i]);
+ }
+ }
+ }
+
+ private static boolean isHttps(URL url) {
+ return "https".equalsIgnoreCase(url.getProtocol());
+ }
+
+ private static boolean isRedirect(int statusCode) {
+ return statusCode == HttpURLConnection.HTTP_MOVED_PERM ||
+ statusCode == HttpURLConnection.HTTP_MOVED_TEMP ||
+ statusCode == HttpURLConnection.HTTP_SEE_OTHER ||
+ statusCode == 307 ||
+ statusCode == 308;
+ }
+
+ private static byte[] readBody(HttpsURLConnection connection, int statusCode,
+ long maxBodyBytes) throws IOException,
+ ResponseTooLargeException {
+ InputStream stream = statusCode >= HttpURLConnection.HTTP_BAD_REQUEST ?
+ connection.getErrorStream() : connection.getInputStream();
+ if (stream == null) {
+ return new byte[0];
+ }
+
+ try (InputStream bodyStream = stream;
+ ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+ byte[] buffer = new byte[8192];
+ long total = 0;
+ while (true) {
+ int read = bodyStream.read(buffer);
+ if (read < 0) {
+ return out.toByteArray();
+ }
+ if (read == 0) {
+ continue;
+ }
+ if (read > maxBodyBytes || total > maxBodyBytes - read) {
+ throw new ResponseTooLargeException(out.toByteArray());
+ }
+ out.write(buffer, 0, read);
+ total += read;
+ }
+ }
+ }
+
+ private static int safeStatusCode(HttpsURLConnection connection) {
+ try {
+ return connection.getResponseCode();
+ } catch (IOException e) {
+ return 0;
+ }
+ }
+
+ private static Response success(int statusCode, HttpsURLConnection connection, byte[] body) {
+ HeaderLists headers = readHeaders(connection);
+ return new Response(ERROR_NONE, "", statusCode, headers.names, headers.values, body);
+ }
+
+ private static Response fail(int error, String message) {
+ return new Response(error, message, 0, null, null, null);
+ }
+
+ private static Response fail(int error, String message, int statusCode,
+ HttpsURLConnection connection, byte[] body) {
+ HeaderLists headers = readHeaders(connection);
+ return new Response(error, message, statusCode, headers.names, headers.values, body);
+ }
+
+ private static HeaderLists readHeaders(HttpsURLConnection connection) {
+ List names = new ArrayList<>();
+ List values = new ArrayList<>();
+
+ Map> headerFields = connection.getHeaderFields();
+ if (headerFields == null) {
+ return new HeaderLists(new String[0], new String[0]);
+ }
+
+ for (Map.Entry> entry : headerFields.entrySet()) {
+ String name = entry.getKey();
+ if (name == null) {
+ continue;
+ }
+ List entryValues = entry.getValue();
+ if (entryValues == null || entryValues.isEmpty()) {
+ names.add(name);
+ values.add("");
+ continue;
+ }
+ for (String value : entryValues) {
+ names.add(name);
+ values.add(value != null ? value : "");
+ }
+ }
+
+ return new HeaderLists(names.toArray(new String[0]), values.toArray(new String[0]));
+ }
+
+ private static final class HeaderLists {
+ final String[] names;
+ final String[] values;
+
+ HeaderLists(String[] names, String[] values) {
+ this.names = names;
+ this.values = values;
+ }
+ }
+
+ private static final class ResponseTooLargeException extends Exception {
+ final byte[] partialBody;
+
+ ResponseTooLargeException(byte[] partialBody) {
+ this.partialBody = partialBody;
+ }
+ }
+}
diff --git a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java
index b91a8211b1..1fb2bfb4a7 100644
--- a/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java
+++ b/platforms/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java
@@ -256,6 +256,7 @@ public class HIDDeviceManager {
0x24c6, // PowerA
0x2c22, // Qanba
0x2dc8, // 8BitDo
+ 0x37d7, // Flydigi
0x9886, // ASTRO Gaming
};
diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java
index 9548c529c0..42f5a911f7 100644
--- a/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java
+++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLActivity.java
@@ -61,7 +61,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
private static final String TAG = "SDL";
private static final int SDL_MAJOR_VERSION = 3;
private static final int SDL_MINOR_VERSION = 4;
- private static final int SDL_MICRO_VERSION = 2;
+ private static final int SDL_MICRO_VERSION = 4;
/*
// Display InputType.SOURCE/CLASS of events and devices
//
@@ -2032,7 +2032,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
try {
ParcelFileDescriptor pfd = mSingleton.getContentResolver().openFileDescriptor(Uri.parse(uri), mode);
return pfd != null ? pfd.detachFd() : -1;
- } catch (FileNotFoundException e) {
+ } catch (FileNotFoundException | SecurityException e) {
e.printStackTrace();
return -1;
}
@@ -2227,4 +2227,3 @@ class SDLClipboardHandler implements
SDLActivity.onNativeClipboardChanged();
}
}
-
diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLInputConnection.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLInputConnection.java
index 027b8fd3e5..fdc2994c15 100644
--- a/platforms/android/app/src/main/java/org/libsdl/app/SDLInputConnection.java
+++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLInputConnection.java
@@ -65,17 +65,15 @@ class SDLInputConnection extends BaseInputConnection
@Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
- if (Build.VERSION.SDK_INT <= 29 /* Android 10.0 (Q) */) {
- // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection
- // and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
- if (beforeLength > 0 && afterLength == 0) {
- // backspace(s)
- while (beforeLength-- > 0) {
- nativeGenerateScancodeForUnichar('\b');
- }
- return true;
- }
- }
+ // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection
+ // and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
+ if (beforeLength > 0 && afterLength == 0) {
+ // backspace(s)
+ while (beforeLength-- > 0) {
+ nativeGenerateScancodeForUnichar('\b');
+ }
+ return true;
+ }
if (!super.deleteSurroundingText(beforeLength, afterLength)) {
return false;
diff --git a/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java b/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java
index 1579b73345..5ed335ac39 100644
--- a/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java
+++ b/platforms/android/app/src/main/java/org/libsdl/app/SDLSurface.java
@@ -35,6 +35,8 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
View.OnApplyWindowInsetsListener, View.OnKeyListener, View.OnTouchListener,
SensorEventListener, ScaleGestureDetector.OnScaleGestureListener {
+ private static native void auroraNativeSetSurfaceReady(boolean ready);
+
// Sensors
protected SensorManager mSensorManager;
protected Display mDisplay;
@@ -96,6 +98,7 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.v("SDL", "surfaceCreated()");
+ auroraNativeSetSurfaceReady(false);
SDLActivity.onNativeSurfaceCreated();
}
@@ -103,6 +106,7 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.v("SDL", "surfaceDestroyed()");
+ auroraNativeSetSurfaceReady(false);
// Transition to pause, if needed
SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
@@ -192,6 +196,7 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
/* Surface is ready */
mIsSurfaceReady = true;
+ auroraNativeSetSurfaceReady(true);
SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
SDLActivity.handleNativeState();
diff --git a/platforms/android/app/src/main/res/values/strings.xml b/platforms/android/app/src/main/res/values/strings.xml
index 79d8812b82..752dae76bf 100644
--- a/platforms/android/app/src/main/res/values/strings.xml
+++ b/platforms/android/app/src/main/res/values/strings.xml
@@ -1,4 +1,6 @@
Dusk
+ Dusk Data
+ Saves, texture packs, settings, and logs
diff --git a/platforms/android/gradlew b/platforms/android/gradlew
old mode 100644
new mode 100755
diff --git a/platforms/android/scripts/stage-jni-libs.sh b/platforms/android/scripts/stage-jni-libs.sh
old mode 100644
new mode 100755
index ae25522e4d..42b08c13e6
--- a/platforms/android/scripts/stage-jni-libs.sh
+++ b/platforms/android/scripts/stage-jni-libs.sh
@@ -14,9 +14,22 @@ if [[ -z "$ANDROID_NDK_VER" ]] && [[ -d "$ANDROID_HOME_DIR/ndk" ]]; then
fi
if [[ -n "$ANDROID_NDK_VER" ]]; then
- TOOLCHAIN_BIN="$ANDROID_HOME_DIR/ndk/$ANDROID_NDK_VER/toolchains/llvm/prebuilt/linux-x86_64/bin"
- if [[ -x "$TOOLCHAIN_BIN/llvm-strip" ]]; then
- STRIP_TOOL="$TOOLCHAIN_BIN/llvm-strip"
+ case "$(uname -s)" in
+ Darwin) HOST_TAG="darwin-x86_64" ;;
+ Linux) HOST_TAG="linux-x86_64" ;;
+ *) HOST_TAG="" ;;
+ esac
+
+ PREBUILT_DIR="$ANDROID_HOME_DIR/ndk/$ANDROID_NDK_VER/toolchains/llvm/prebuilt"
+ if [[ -n "$HOST_TAG" && -x "$PREBUILT_DIR/$HOST_TAG/bin/llvm-strip" ]]; then
+ STRIP_TOOL="$PREBUILT_DIR/$HOST_TAG/bin/llvm-strip"
+ else
+ for candidate in "$PREBUILT_DIR"/*/bin/llvm-strip; do
+ if [[ -x "$candidate" ]]; then
+ STRIP_TOOL="$candidate"
+ break
+ fi
+ done
fi
fi
@@ -25,29 +38,35 @@ copy_lib() {
local src="$2"
local dst_dir="$APP_DIR/$abi"
local dst="$dst_dir/libmain.so"
+ local tmp="$dst_dir/.libmain.so.$$"
+ if [[ ! -f "$src" ]]; then
+ echo "Missing native library for $abi: $src" >&2
+ exit 1
+ fi
+
mkdir -p "$dst_dir"
- cp -f "$src" "$dst"
+ cp -f "$src" "$tmp"
if [[ "$ANDROID_STAGE_STRIP" != "0" ]] && [[ -n "$STRIP_TOOL" ]]; then
- "$STRIP_TOOL" --strip-debug "$dst"
- echo "Staged and stripped $src -> $dst"
+ "$STRIP_TOOL" --strip-unneeded "$tmp"
+ mv -f "$tmp" "$dst"
+ echo "Stripped and staged $src -> $dst"
else
+ mv -f "$tmp" "$dst"
echo "Staged $src -> $dst (strip disabled or strip tool unavailable)"
fi
}
-declare -A ABI_TO_LIB=(
- ["arm64-v8a"]="$ROOT_DIR/build/android-arm64/libmain.so"
- ["x86_64"]="$ROOT_DIR/build/android-x86_64/libmain.so"
-)
-
# Drop any previously staged ABI directories to avoid stale APK contents.
rm -rf "$APP_DIR/x86" "$APP_DIR/arm64-v8a" "$APP_DIR/x86_64"
for abi in $ANDROID_STAGE_ABIS; do
- src="${ABI_TO_LIB[$abi]:-}"
- if [[ -z "$src" ]]; then
- echo "Unsupported ABI '$abi'. Supported ABIs: arm64-v8a x86_64" >&2
- exit 1
- fi
+ case "$abi" in
+ arm64-v8a) src="$ROOT_DIR/build/android-arm64/libmain.so" ;;
+ x86_64) src="$ROOT_DIR/build/android-x86_64/libmain.so" ;;
+ *)
+ echo "Unsupported ABI '$abi'. Supported ABIs: arm64-v8a x86_64" >&2
+ exit 1
+ ;;
+ esac
copy_lib "$abi" "$src"
done
diff --git a/platforms/ios/Info.plist.in b/platforms/ios/Info.plist.in
index 881d5aff35..395b29b7d7 100644
--- a/platforms/ios/Info.plist.in
+++ b/platforms/ios/Info.plist.in
@@ -79,5 +79,11 @@
CADisableMinimumFrameDurationOnPhone
+ UIFileSharingEnabled
+
+ LSSupportsOpeningDocumentsInPlace
+
+ LSSupportsGameMode
+
diff --git a/platforms/macos/Info.plist.in b/platforms/macos/Info.plist.in
index b7bc7b706c..aee7393ee8 100644
--- a/platforms/macos/Info.plist.in
+++ b/platforms/macos/Info.plist.in
@@ -28,5 +28,7 @@
${MACOSX_BUNDLE_SHORT_VERSION_STRING}
NSHighResolutionCapable
+ LSSupportsGameMode
+
diff --git a/platforms/tvos/Info.plist.in b/platforms/tvos/Info.plist.in
index 841d120cc2..49ed85edd7 100644
--- a/platforms/tvos/Info.plist.in
+++ b/platforms/tvos/Info.plist.in
@@ -45,5 +45,7 @@
LaunchScreen
UIUserInterfaceStyle
Automatic
+ LSSupportsGameMode
+
diff --git a/res/rml/overlay.rcss b/res/rml/overlay.rcss
index 05c8440b55..3ce4d51068 100644
--- a/res/rml/overlay.rcss
+++ b/res/rml/overlay.rcss
@@ -163,30 +163,30 @@ icon {
}
icon.arrow-forward {
- width: 24dp;
- height: 24dp;
- font-size: 24dp;
+ width: 1.2em;
+ height: 1.2em;
+ font-size: 1.2em;
decorator: text("" center center);
}
icon.trophy {
- width: 24dp;
- height: 24dp;
- font-size: 24dp;
+ width: 1.2em;
+ height: 1.2em;
+ font-size: 1.2em;
decorator: text("" center center);
}
icon.controller {
- width: 24dp;
- height: 24dp;
- font-size: 24dp;
+ width: 1.2em;
+ height: 1.2em;
+ font-size: 1.2em;
decorator: text("" center center);
}
icon.warning {
- width: 24dp;
- height: 24dp;
- font-size: 24dp;
+ width: 1.2em;
+ height: 1.2em;
+ font-size: 1.2em;
decorator: text("" center center);
}
@@ -274,3 +274,18 @@ logo img.outer {
transform: rotate(360deg);
}
}
+
+@media (max-height: 640dp) {
+ toast {
+ top: 20dp;
+ right: 20dp;
+ }
+
+ toast.controller-warning {
+ bottom: 20dp;
+ }
+
+ toast.menu-notification {
+ top: 20dp;
+ }
+}
diff --git a/res/rml/prelaunch.rcss b/res/rml/prelaunch.rcss
index 9b83db4660..707d8b8234 100644
--- a/res/rml/prelaunch.rcss
+++ b/res/rml/prelaunch.rcss
@@ -275,7 +275,6 @@ body.mirrored version-info {
.update {
display: none;
- font-size: 16dp;
color: #A6A09B;
align-items: center;
justify-content: flex-end;
@@ -375,8 +374,8 @@ body.animate-in .intro-item {
}
menu {
- left: 20dp;
- right: 20dp;
+ left: 32dp;
+ right: 32dp;
width: auto;
min-width: 0;
max-width: none;
@@ -387,8 +386,8 @@ body.animate-in .intro-item {
}
body.mirrored menu {
- left: 20dp;
- right: 20dp;
+ left: 32dp;
+ right: 32dp;
flex-direction: row-reverse;
}
@@ -396,7 +395,7 @@ body.animate-in .intro-item {
flex: 1 1 0;
min-width: 0;
max-width: 48%;
-
+ margin-left: 32dp;
}
body.mirrored hero {
@@ -438,9 +437,61 @@ body.animate-in .intro-item {
decorator: horizontal-gradient(#FEE685FF #FEE68500);
}
- .eyebrow,
- disc-info,
- version-info {
+ .eyebrow {
display: none;
}
+
+ disc-info {
+ right: 32dp;
+ left: auto;
+ bottom: 32dp;
+ top: auto;
+ text-align: right;
+ font-size: 16dp;
+ }
+
+ #disc-status {
+ justify-content: flex-end;
+ }
+
+ #disc-status icon {
+ font-size: 20dp;
+ }
+
+ #disc-version {
+ font-size: 16dp;
+ }
+
+ version-info {
+ right: 32dp;
+ left: auto;
+ bottom: auto;
+ top: 32dp;
+ text-align: right;
+ font-size: 16dp;
+ }
+
+ .update {
+ font-size: 16dp;
+ }
+
+ body.mirrored disc-info {
+ right: auto;
+ left: 32dp;
+ bottom: 32dp;
+ top: auto;
+ text-align: left;
+ }
+
+ body.mirrored version-info {
+ right: auto;
+ left: 32dp;
+ bottom: auto;
+ top: 32dp;
+ text-align: left;
+ }
+
+ body.mirrored #disc-status {
+ justify-content: flex-start;
+ }
}
diff --git a/res/rml/window.rcss b/res/rml/window.rcss
index 8dd29b55e4..45473f312f 100644
--- a/res/rml/window.rcss
+++ b/res/rml/window.rcss
@@ -498,7 +498,6 @@ progress.verification-progress-bar {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
- justify-content: stretch;
align-items: stretch;
gap: 12dp;
width: 100%;
diff --git a/src/d/actor/d_a_alink.cpp b/src/d/actor/d_a_alink.cpp
index e14f598487..0204278ddc 100644
--- a/src/d/actor/d_a_alink.cpp
+++ b/src/d/actor/d_a_alink.cpp
@@ -51,6 +51,7 @@
#include "d/actor/d_a_ni.h"
#include "d/d_s_play.h"
+#include "dusk/frame_interpolation.h"
#include "dusk/settings.h"
#include "res/Object/Alink.h"
#include
@@ -14787,6 +14788,10 @@ void daAlink_c::deleteEquipItem(BOOL i_isPlaySound, BOOL i_isDeleteKantera) {
mIronBallChainPos = NULL;
mIronBallChainAngle = NULL;
field_0x3848 = NULL;
+#if TARGET_PC
+ mIBChainInterpPrevValid = false;
+ mIBChainInterpCurrValid = false;
+#endif
field_0x0774 = NULL;
field_0x0778 = NULL;
mpHookshotLinChk = NULL;
@@ -19717,6 +19722,27 @@ int daAlink_c::draw() {
)
{
dComIfGd_getOpaListDark()->entryImm(mpHookChain, 0);
+
+#if TARGET_PC
+ if (dusk::getSettings().game.enableFrameInterpolation &&
+ mEquipItem == dItemNo_IRONBALL_e &&
+ mIronBallChainPos != NULL && mIronBallChainAngle != NULL)
+ {
+ if (mIBChainInterpCurrValid) {
+ memcpy(mIBChainInterpPrevPos, mIBChainInterpCurrPos, IRON_BALL_CHAIN_COUNT * sizeof(cXyz));
+ memcpy(mIBChainInterpPrevAngle, mIBChainInterpCurrAngle, IRON_BALL_CHAIN_COUNT * sizeof(csXyz));
+ mIBChainInterpPrevHandRoot = mIBChainInterpCurrHandRoot;
+ mIBChainInterpPrevValid = true;
+ }
+
+ memcpy(mIBChainInterpCurrPos, mIronBallChainPos, IRON_BALL_CHAIN_COUNT * sizeof(cXyz));
+ memcpy(mIBChainInterpCurrAngle, mIronBallChainAngle, IRON_BALL_CHAIN_COUNT * sizeof(csXyz));
+ mIBChainInterpCurrHandRoot = mHookshotTopPos;
+ mIBChainInterpCurrValid = true;
+
+ dusk::frame_interp::add_interpolation_callback(&ironBallChainInterpCallback, this);
+ }
+#endif
}
}
diff --git a/src/d/actor/d_a_alink_hook.inc b/src/d/actor/d_a_alink_hook.inc
index c960d37a7b..36fba0c6d8 100644
--- a/src/d/actor/d_a_alink_hook.inc
+++ b/src/d/actor/d_a_alink_hook.inc
@@ -8,6 +8,8 @@
#include "d/actor/d_a_obj_swhang.h"
#include "d/actor/d_a_obj_chandelier.h"
#include "JSystem/J3DGraphBase/J3DMaterial.h"
+#include "dusk/frame_interpolation.h"
+#include "dusk/settings.h"
enum {
HS_MODE_NONE_e,
@@ -235,6 +237,31 @@ void daAlink_c::hsChainShape_c::draw() {
}
}
+#if TARGET_PC
+static void ironBallChainInterpCallback(bool isSimFrame, void* pUserWork) {
+ static_cast(pUserWork)->onIronBallChainInterpCallback();
+}
+
+void daAlink_c::onIronBallChainInterpCallback() {
+ if (!mIBChainInterpPrevValid || !mIBChainInterpCurrValid) {
+ return;
+ }
+ if (mIronBallChainPos == NULL || mIronBallChainAngle == NULL) {
+ return;
+ }
+
+ const f32 alpha = dusk::frame_interp::get_interpolation_step();
+
+ for (int i = 0; i < IRON_BALL_CHAIN_COUNT; i++) {
+ mIronBallChainPos[i] = mIBChainInterpPrevPos[i] + (mIBChainInterpCurrPos[i] - mIBChainInterpPrevPos[i]) * alpha;
+ mIronBallChainAngle[i].x = mIBChainInterpPrevAngle[i].x + (s16)((s16)(mIBChainInterpCurrAngle[i].x - mIBChainInterpPrevAngle[i].x) * alpha);
+ mIronBallChainAngle[i].y = mIBChainInterpPrevAngle[i].y + (s16)((s16)(mIBChainInterpCurrAngle[i].y - mIBChainInterpPrevAngle[i].y) * alpha);
+ mIronBallChainAngle[i].z = mIBChainInterpPrevAngle[i].z + (s16)((s16)(mIBChainInterpCurrAngle[i].z - mIBChainInterpPrevAngle[i].z) * alpha);
+ }
+ mHookshotTopPos = mIBChainInterpPrevHandRoot + (mIBChainInterpCurrHandRoot - mIBChainInterpPrevHandRoot) * alpha;
+}
+#endif
+
void daAlink_c::hookshotAtHitCallBack(dCcD_GObjInf* i_atObjInf, fopAc_ac_c* i_tgActor,
dCcD_GObjInf* i_tgObjInf) {
if (i_tgActor != NULL && fopAcM_IsActor(i_tgActor) && !i_tgObjInf->ChkTgHookshotThrough()) {
diff --git a/src/d/actor/d_a_e_db.cpp b/src/d/actor/d_a_e_db.cpp
index cbd21176b9..4d60fbd6f6 100644
--- a/src/d/actor/d_a_e_db.cpp
+++ b/src/d/actor/d_a_e_db.cpp
@@ -10,6 +10,10 @@
#include "f_op/f_op_kankyo_mng.h"
#include "f_op/f_op_actor_enemy.h"
+#if TARGET_PC
+#include "dusk/frame_interpolation.h"
+#endif
+
class daE_DB_HIO_c : public JORReflexible {
public:
daE_DB_HIO_c();
@@ -66,6 +70,22 @@ static BOOL leaf_anm_init(e_db_class* i_this, int i_anm, f32 i_morf, u8 i_mode,
return FALSE;
}
+#if TARGET_PC
+static void daE_DB_interp_callback(bool isSimFrame, void* pUserWork) {
+ e_db_class* i_this = (e_db_class*)pUserWork;
+ if (!i_this->mStalkLineInterpPrevValid || !i_this->mStalkLineInterpCurrValid) {
+ return;
+ }
+ const f32 alpha = dusk::frame_interp::get_interpolation_step();
+ cXyz* dst = i_this->stalkLine.getPos(0);
+ for (int i = 0; i < 12; i++) {
+ const cXyz& p0 = i_this->mStalkLineInterpPrev[i];
+ const cXyz& p1 = i_this->mStalkLineInterpCurr[i];
+ dst[i] = p0 + (p1 - p0) * alpha;
+ }
+}
+#endif
+
static int daE_DB_Draw(e_db_class* i_this) {
fopAc_ac_c* actor = &i_this->enemy;
@@ -95,6 +115,17 @@ static int daE_DB_Draw(e_db_class* i_this) {
static GXColor l_color = {0x14, 0x0F, 0x00, 0xFF};
i_this->stalkLine.update(12, l_color, &actor->tevStr);
dComIfGd_set3DlineMat(&i_this->stalkLine);
+#if TARGET_PC
+ if (dusk::getSettings().game.enableFrameInterpolation) {
+ if (i_this->mStalkLineInterpCurrValid) {
+ memcpy(i_this->mStalkLineInterpPrev, i_this->mStalkLineInterpCurr, sizeof(i_this->mStalkLineInterpCurr));
+ i_this->mStalkLineInterpPrevValid = true;
+ }
+ memcpy(i_this->mStalkLineInterpCurr, i_this->stalkLine.getPos(0), 12 * sizeof(cXyz));
+ i_this->mStalkLineInterpCurrValid = true;
+ dusk::frame_interp::add_interpolation_callback(&daE_DB_interp_callback, i_this);
+ }
+#endif
for (int i = 1; i < 11; i++) {
if (i_this->thornModel[i] != NULL) {
diff --git a/src/d/actor/d_a_e_hb.cpp b/src/d/actor/d_a_e_hb.cpp
index d3177ba0f6..8ed9058c6f 100644
--- a/src/d/actor/d_a_e_hb.cpp
+++ b/src/d/actor/d_a_e_hb.cpp
@@ -9,6 +9,10 @@
#include "d/actor/d_a_e_hb_leaf.h"
#include "f_op/f_op_actor_enemy.h"
+#if TARGET_PC
+#include "dusk/frame_interpolation.h"
+#endif
+
enum daE_HB_ACTION {
ACTION_STAY,
ACTION_APPEAR,
@@ -64,6 +68,22 @@ static BOOL leaf_anm_init(e_hb_class* i_this, int i_anm, f32 i_morf, u8 i_mode,
return FALSE;
}
+#if TARGET_PC
+static void daE_HB_interp_callback(bool isSimFrame, void* pUserWork) {
+ e_hb_class* i_this = (e_hb_class*)pUserWork;
+ if (!i_this->mStalkLineInterpPrevValid || !i_this->mStalkLineInterpCurrValid) {
+ return;
+ }
+ const f32 alpha = dusk::frame_interp::get_interpolation_step();
+ cXyz* dst = i_this->stalkLine.getPos(0);
+ for (int i = 0; i < 12; i++) {
+ const cXyz& p0 = i_this->mStalkLineInterpPrev[i];
+ const cXyz& p1 = i_this->mStalkLineInterpCurr[i];
+ dst[i] = p0 + (p1 - p0) * alpha;
+ }
+}
+#endif
+
static int daE_HB_Draw(e_hb_class* i_this) {
fopAc_ac_c* actor = &i_this->enemy;
@@ -82,6 +102,17 @@ static int daE_HB_Draw(e_hb_class* i_this) {
static GXColor l_color = {0x14, 0x0F, 0x00, 0xFF};
i_this->stalkLine.update(12, l_color, &actor->tevStr);
dComIfGd_set3DlineMat(&i_this->stalkLine);
+#if TARGET_PC
+ if (dusk::getSettings().game.enableFrameInterpolation) {
+ if (i_this->mStalkLineInterpCurrValid) {
+ memcpy(i_this->mStalkLineInterpPrev, i_this->mStalkLineInterpCurr, sizeof(i_this->mStalkLineInterpCurr));
+ i_this->mStalkLineInterpPrevValid = true;
+ }
+ memcpy(i_this->mStalkLineInterpCurr, i_this->stalkLine.getPos(0), 12 * sizeof(cXyz));
+ i_this->mStalkLineInterpCurrValid = true;
+ dusk::frame_interp::add_interpolation_callback(&daE_HB_interp_callback, i_this);
+ }
+#endif
for (int i = 1; i < 11; i++) {
if (i_this->thornModel[i] != NULL) {
diff --git a/src/d/actor/d_a_e_s1.cpp b/src/d/actor/d_a_e_s1.cpp
index 48cd7d6800..a5a05fce20 100644
--- a/src/d/actor/d_a_e_s1.cpp
+++ b/src/d/actor/d_a_e_s1.cpp
@@ -14,6 +14,8 @@
#include "d/d_s_play.h"
#include "f_op/f_op_actor_enemy.h"
#include "f_op/f_op_camera_mng.h"
+#include "dusk/frame_interpolation.h"
+#include "dusk/settings.h"
#include
class daE_S1_HIO_c {
@@ -99,6 +101,25 @@ static void anm_init(e_s1_class* i_this, int i_resNo, f32 i_morf, u8 i_attr, f32
i_this->mAnm = i_resNo;
}
+#if TARGET_PC
+static void daE_S1_interp_callback(bool isSimFrame, void* pUserWork) {
+ e_s1_class* i_this = (e_s1_class*)pUserWork;
+ if (!i_this->mHairInterpPrevValid || !i_this->mHairInterpCurrValid) {
+ return;
+ }
+ const f32 alpha = dusk::frame_interp::get_interpolation_step();
+ for (int s = 0; s < e_s1_class::HAIR_STRAND_COUNT; s++) {
+ cXyz* dst = i_this->mLineMat.getPos(s);
+ for (int i = 0; i < e_s1_class::HAIR_SEGMENT_COUNT; i++) {
+ int idx = s * e_s1_class::HAIR_SEGMENT_COUNT + i;
+ const cXyz& p0 = i_this->mHairInterpPrev[idx];
+ const cXyz& p1 = i_this->mHairInterpCurr[idx];
+ dst[i] = p0 + (p1 - p0) * alpha;
+ }
+ }
+}
+#endif
+
static int daE_S1_Draw(e_s1_class* i_this) {
if (i_this->field_0x306c != 0) {
return 1;
@@ -132,6 +153,22 @@ static int daE_S1_Draw(e_s1_class* i_this) {
i_this->mLineMat.update(16, line_color, &i_this->tevStr);
dComIfGd_set3DlineMatDark(&i_this->mLineMat);
+#if TARGET_PC
+ if (dusk::getSettings().game.enableFrameInterpolation) {
+ if (i_this->mHairInterpCurrValid) {
+ memcpy(i_this->mHairInterpPrev, i_this->mHairInterpCurr, sizeof(i_this->mHairInterpCurr));
+ i_this->mHairInterpPrevValid = true;
+ }
+ for (int s = 0; s < e_s1_class::HAIR_STRAND_COUNT; s++) {
+ cXyz* src = i_this->mLineMat.getPos(s);
+ memcpy(&i_this->mHairInterpCurr[s * e_s1_class::HAIR_SEGMENT_COUNT], src,
+ e_s1_class::HAIR_SEGMENT_COUNT * sizeof(cXyz));
+ }
+ i_this->mHairInterpCurrValid = true;
+ dusk::frame_interp::add_interpolation_callback(&daE_S1_interp_callback, i_this);
+ }
+#endif
+
dComIfGd_setList();
return 1;
}
@@ -2149,6 +2186,11 @@ static int daE_S1_Create(fopAc_ac_c* i_this) {
return cPhs_ERROR_e;
}
+#if TARGET_PC
+ a_this->mHairInterpPrevValid = false;
+ a_this->mHairInterpCurrValid = false;
+#endif
+
OS_REPORT("//////////////E_S1 SET 2 !!\n");
if (path_no != 0xFF) {
diff --git a/src/d/actor/d_a_e_yd.cpp b/src/d/actor/d_a_e_yd.cpp
index bdcd044eed..18809e05ea 100644
--- a/src/d/actor/d_a_e_yd.cpp
+++ b/src/d/actor/d_a_e_yd.cpp
@@ -12,6 +12,10 @@
#include "d/d_cc_uty.h"
#include "f_op/f_op_actor_enemy.h"
+#if TARGET_PC
+#include "dusk/frame_interpolation.h"
+#endif
+
class daE_YD_HIO_c {
public:
daE_YD_HIO_c();
@@ -73,6 +77,22 @@ static s32 leaf_anm_init(e_yd_class* i_this, int param_1, f32 param_2, u8 param_
return false;
}
+#if TARGET_PC
+static void daE_YD_interp_callback(bool isSimFrame, void* pUserWork) {
+ e_yd_class* i_this = (e_yd_class*)pUserWork;
+ if (!i_this->mLineMatInterpPrevValid || !i_this->mLineMatInterpCurrValid) {
+ return;
+ }
+ const f32 alpha = dusk::frame_interp::get_interpolation_step();
+ cXyz* dst = i_this->mLineMat.getPos(0);
+ for (int i = 0; i < 12; i++) {
+ const cXyz& p0 = i_this->mLineMatInterpPrev[i];
+ const cXyz& p1 = i_this->mLineMatInterpCurr[i];
+ dst[i] = p0 + (p1 - p0) * alpha;
+ }
+}
+#endif
+
static s32 daE_YD_Draw(e_yd_class* i_this) {
static GXColor l_color = { 0x14, 0x0F, 0x00, 0xFF };
@@ -86,6 +106,17 @@ static s32 daE_YD_Draw(e_yd_class* i_this) {
i_this->mpMorf->entryDL();
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 (i_this->mLineMatInterpCurrValid) {
+ memcpy(i_this->mLineMatInterpPrev, i_this->mLineMatInterpCurr, sizeof(i_this->mLineMatInterpCurr));
+ i_this->mLineMatInterpPrevValid = true;
+ }
+ memcpy(i_this->mLineMatInterpCurr, i_this->mLineMat.getPos(0), 12 * sizeof(cXyz));
+ i_this->mLineMatInterpCurrValid = true;
+ dusk::frame_interp::add_interpolation_callback(&daE_YD_interp_callback, i_this);
+ }
+#endif
for (s32 i = 1; i < 11; i++) {
if (i_this->field_0x77c[i] != 0) {
g_env_light.setLightTevColorType_MAJI(i_this->field_0x77c[i], &i_this->actor.tevStr);
diff --git a/src/d/actor/d_a_e_yg.cpp b/src/d/actor/d_a_e_yg.cpp
index 9ab6c80e6d..44a8aee249 100644
--- a/src/d/actor/d_a_e_yg.cpp
+++ b/src/d/actor/d_a_e_yg.cpp
@@ -10,6 +10,8 @@
#include "f_op/f_op_kankyo_mng.h"
#include "d/actor/d_a_obj_carry.h"
#include "Z2AudioLib/Z2Instances.h"
+#include "dusk/frame_interpolation.h"
+#include "dusk/settings.h"
#include "f_op/f_op_actor_enemy.h"
enum E_yg_RES_File_ID {
@@ -134,7 +136,26 @@ static BOOL pl_check(e_yg_class* i_this, f32 i_dist) {
return FALSE;
}
-static int daE_YG_Draw(e_yg_class* i_this) {
+#if TARGET_PC
+static void daE_YG_interp_callback(bool isSimFrame, void* pUserWork) {
+ e_yg_class* i_this = (e_yg_class*)pUserWork;
+ if (!i_this->mTentacleInterpPrevValid || !i_this->mTentacleInterpCurrValid) {
+ return;
+ }
+ const f32 alpha = dusk::frame_interp::get_interpolation_step();
+ for (int s = 0; s < e_yg_class::TENTACLE_STRAND_COUNT; s++) {
+ cXyz* dst = i_this->mLineMat.getPos(s);
+ for (int i = 0; i < e_yg_class::TENTACLE_SEGMENT_COUNT; i++) {
+ int idx = s * e_yg_class::TENTACLE_SEGMENT_COUNT + i;
+ const cXyz& p0 = i_this->mTentacleInterpPrev[idx];
+ const cXyz& p1 = i_this->mTentacleInterpCurr[idx];
+ dst[i] = p0 + (p1 - p0) * alpha;
+ }
+ }
+}
+#endif
+
+static int daE_YG_Draw(e_yg_class* i_this) {
if (i_this->mDispFlag) {
return 1;
}
@@ -160,6 +181,23 @@ static int daE_YG_Draw(e_yg_class* i_this) {
color.a = 0xFF;
i_this->mLineMat.update(10, color, &actor->tevStr);
dComIfGd_set3DlineMatDark(&i_this->mLineMat);
+
+#if TARGET_PC
+ if (dusk::getSettings().game.enableFrameInterpolation) {
+ if (i_this->mTentacleInterpCurrValid) {
+ memcpy(i_this->mTentacleInterpPrev, i_this->mTentacleInterpCurr, sizeof(i_this->mTentacleInterpCurr));
+ i_this->mTentacleInterpPrevValid = true;
+ }
+ for (int s = 0; s < e_yg_class::TENTACLE_STRAND_COUNT; s++) {
+ cXyz* src = i_this->mLineMat.getPos(s);
+ memcpy(&i_this->mTentacleInterpCurr[s * e_yg_class::TENTACLE_SEGMENT_COUNT], src,
+ e_yg_class::TENTACLE_SEGMENT_COUNT * sizeof(cXyz));
+ }
+ i_this->mTentacleInterpCurrValid = true;
+ dusk::frame_interp::add_interpolation_callback(&daE_YG_interp_callback, i_this);
+ }
+#endif
+
dComIfGd_setList();
return 1;
@@ -1378,6 +1416,11 @@ static cPhs_Step daE_YG_Create(fopAc_ac_c* actor) {
return cPhs_ERROR_e;
}
+#if TARGET_PC
+ i_this->mTentacleInterpPrevValid = false;
+ i_this->mTentacleInterpCurrValid = false;
+#endif
+
if (!hio_set) {
i_this->mIsFirstSpawn = 1;
hio_set = true;
diff --git a/src/d/actor/d_a_e_yh.cpp b/src/d/actor/d_a_e_yh.cpp
index bbbd3e5129..cf9955a7db 100644
--- a/src/d/actor/d_a_e_yh.cpp
+++ b/src/d/actor/d_a_e_yh.cpp
@@ -12,6 +12,10 @@
#include "f_op/f_op_actor_enemy.h"
#include "f_op/f_op_kankyo_mng.h"
+#if TARGET_PC
+#include "dusk/frame_interpolation.h"
+#endif
+
class daE_YH_HIO_c : public JORReflexible {
public:
daE_YH_HIO_c();
@@ -85,6 +89,22 @@ static BOOL leaf_anm_init(e_yh_class* i_this, int param_2, f32 param_3, u8 param
return FALSE;
}
+#if TARGET_PC
+static void daE_YH_interp_callback(bool isSimFrame, void* pUserWork) {
+ e_yh_class* i_this = (e_yh_class*)pUserWork;
+ if (!i_this->mLineInterpPrevValid || !i_this->mLineInterpCurrValid) {
+ return;
+ }
+ const f32 alpha = dusk::frame_interp::get_interpolation_step();
+ cXyz* dst = i_this->mLine.getPos(0);
+ for (int i = 0; i < 12; i++) {
+ const cXyz& p0 = i_this->mLineInterpPrev[i];
+ const cXyz& p1 = i_this->mLineInterpCurr[i];
+ dst[i] = p0 + (p1 - p0) * alpha;
+ }
+}
+#endif
+
static int daE_YH_Draw(e_yh_class* i_this) {
fopAc_ac_c* a_this = (fopAc_ac_c*)i_this;
@@ -114,6 +134,17 @@ 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 (i_this->mLineInterpCurrValid) {
+ memcpy(i_this->mLineInterpPrev, i_this->mLineInterpCurr, sizeof(i_this->mLineInterpCurr));
+ i_this->mLineInterpPrevValid = true;
+ }
+ memcpy(i_this->mLineInterpCurr, i_this->mLine.getPos(0), 12 * sizeof(cXyz));
+ i_this->mLineInterpCurrValid = true;
+ dusk::frame_interp::add_interpolation_callback(&daE_YH_interp_callback, i_this);
+ }
+#endif
for (int i = 1; i < 11; i++) {
if (i_this->mModels[i] != NULL) {
diff --git a/src/d/actor/d_a_mg_fshop.cpp b/src/d/actor/d_a_mg_fshop.cpp
index f0905efd5d..0d5b6dd2c9 100644
--- a/src/d/actor/d_a_mg_fshop.cpp
+++ b/src/d/actor/d_a_mg_fshop.cpp
@@ -729,10 +729,12 @@ static void koro2_game(fshop_class* i_this) {
cLib_addCalcAngleS2(&i_this->field_0x4020.z, 0, 2, 0x200);
case 2:
#if TARGET_PC
- if (dusk::getSettings().game.enableGyroRollgoal) {
+ if (dusk::gyro::rollgoal_gyro_enabled()) {
if (!dusk::gyro::get_sensor_keep_alive()) {
dusk::gyro::set_sensor_keep_alive(true);
}
+ } else if (dusk::gyro::get_sensor_keep_alive()) {
+ dusk::gyro::set_sensor_keep_alive(false);
}
#endif
@@ -753,7 +755,7 @@ static void koro2_game(fshop_class* i_this) {
old_stick_x = mDoCPd_c::getSubStickX(PAD_1);
cLib_addCalcAngleS2(&i_this->field_0x4060, i_this->field_0x4062, 4, 0x1000);
#if TARGET_PC
- if (dusk::getSettings().game.enableGyroRollgoal) {
+ if (dusk::gyro::rollgoal_gyro_enabled()) {
dusk::gyro::rollgoalTick(true, i_this->field_0x4060);
}
#endif
@@ -791,7 +793,7 @@ static void koro2_game(fshop_class* i_this) {
s16 gyro_ax = 0;
s16 gyro_az = 0;
#if TARGET_PC
- if (dusk::getSettings().game.enableGyroRollgoal) {
+ if (dusk::gyro::rollgoal_gyro_enabled()) {
dusk::gyro::rollgoalTableOffset(gyro_ax, gyro_az);
}
#endif
diff --git a/src/d/actor/d_a_midna.cpp b/src/d/actor/d_a_midna.cpp
index 2356b693be..3bc500991d 100644
--- a/src/d/actor/d_a_midna.cpp
+++ b/src/d/actor/d_a_midna.cpp
@@ -419,9 +419,30 @@ int daMidna_c::createHeap() {
return 0;
}
+#if TARGET_PC
+ if (mpDemoFCTongueBmd != NULL) {
+ if(!daAlink_c::initDemoBck(&mpDemoFCTmpBck, "demo00_Midna_cut00_FC_tmp.bck")) {
+ return 0;
+ }
+
+ // Update Midna's eye maxLOD to prevent the eyes from disappearing
+ J3DTexture* tex = mpDemoFCTongueBmd->getModelData()->getTexture();
+ JUTNameTab* nametable = mpDemoFCTongueBmd->getModelData()->getTextureName();
+ if (tex != nullptr && nametable != nullptr) {
+ for (u16 i = 0; i < tex->getNum(); i++) {
+ const char* tex_name = nametable->getName(i);
+ if (tex_name != NULL && strcmp(tex_name, "midona_eyeball") == 0) {
+ ResTIMG* timg = tex->getResTIMG(i);
+ timg->maxLOD = 0;
+ }
+ }
+ }
+ }
+#else
if (mpDemoFCTongueBmd != NULL && !daAlink_c::initDemoBck(&mpDemoFCTmpBck, "demo00_Midna_cut00_FC_tmp.bck")) {
return 0;
}
+#endif
modelData =
(J3DModelData*)dComIfG_getObjectRes(dStage_roomControl_c::getDemoArcName(), "demo00_Midna_cut00_BD_tmp.bmd");
@@ -433,6 +454,21 @@ int daMidna_c::createHeap() {
modelData->getMaterialNodePointer(2)->setMaterialAnm(mpEyeMatAnm[0]);
modelData->getMaterialNodePointer(3)->setMaterialAnm(mpEyeMatAnm[1]);
+
+#if TARGET_PC
+ // Update Midna's eye maxLOD to prevent the eyes from disappearing
+ J3DTexture* tex = modelData->getTexture();
+ JUTNameTab* nametable = modelData->getTextureName();
+ if (tex != nullptr && nametable != nullptr) {
+ for (u16 i = 0; i < tex->getNum(); i++) {
+ const char* tex_name = nametable->getName(i);
+ if (tex_name != NULL && strcmp(tex_name, "midona_eyeball") == 0) {
+ ResTIMG* timg = tex->getResTIMG(i);
+ timg->maxLOD = 0;
+ }
+ }
+ }
+#endif
}
if (!initDemoModel(&mpDemoBDMaskBmd, "demo00_Midna_cut00_BD_mask.bmd", 0)) {
diff --git a/src/d/actor/d_a_obj_fchain.cpp b/src/d/actor/d_a_obj_fchain.cpp
index e681fec4e7..024b4ecf8b 100644
--- a/src/d/actor/d_a_obj_fchain.cpp
+++ b/src/d/actor/d_a_obj_fchain.cpp
@@ -10,6 +10,8 @@
#include "JSystem/J3DGraphBase/J3DDrawBuffer.h"
#include "SSystem/SComponent/c_math.h"
#include "d/d_com_inf_game.h"
+#include "dusk/frame_interpolation.h"
+#include "dusk/settings.h"
#include
static char const l_arcName[] = "Fchain";
@@ -65,6 +67,10 @@ int daObjFchain_c::create() {
local_48++;
}
rv = cPhs_COMPLEATE_e;
+#if TARGET_PC
+ mChainInterpPrevValid = false;
+ mChainInterpCurrValid = false;
+#endif
break;
}
return rv;
@@ -289,6 +295,26 @@ void daObjFchain_shape_c::draw() {
}
}
+#if TARGET_PC
+static void fchain_interp_callback(bool isSimFrame, void* pUserWork) {
+ static_cast(pUserWork)->onInterpCallback();
+}
+
+void daObjFchain_c::onInterpCallback() {
+ if (!mChainInterpPrevValid || !mChainInterpCurrValid) {
+ return;
+ }
+
+ const f32 alpha = dusk::frame_interp::get_interpolation_step();
+
+ for (int i = 0; i < CHAIN_COUNT; i++) {
+ const cXyz& p0 = mChainInterpPrev[i];
+ const cXyz& p1 = mChainInterpCurr[i];
+ field_0x694[i] = p0 + (p1 - p0) * alpha;
+ }
+}
+#endif
+
int daObjFchain_c::draw() {
if (field_0x584 != 0) {
g_env_light.settingTevStruct(0, ¤t.pos, &tevStr);
@@ -297,6 +323,19 @@ int daObjFchain_c::draw() {
return 1;
}
dComIfGd_getOpaListDark()->entryImm(&mShape, 0);
+
+#if TARGET_PC
+ if (dusk::getSettings().game.enableFrameInterpolation) {
+ if (mChainInterpCurrValid) {
+ memcpy(mChainInterpPrev, mChainInterpCurr, sizeof(mChainInterpCurr));
+ mChainInterpPrevValid = true;
+ }
+
+ memcpy(mChainInterpCurr, field_0x694, sizeof(mChainInterpCurr));
+ mChainInterpCurrValid = true;
+ dusk::frame_interp::add_interpolation_callback(&fchain_interp_callback, this);
+ }
+#endif
}
return 1;
}
diff --git a/src/d/d_menu_letter.cpp b/src/d/d_menu_letter.cpp
index 589b2553c0..4bfda4b9cd 100644
--- a/src/d/d_menu_letter.cpp
+++ b/src/d/d_menu_letter.cpp
@@ -965,7 +965,8 @@ void dMenu_Letter_c::screenSetBase() {
}
if (field_0x374 > 1) {
J2DPane* pJVar6 = mpBaseScreen->search('pi_n');
- f32 dVar18 = field_0x1f0[1]->getBounds().i.x - field_0x1f0[0]->getBounds().i.x;
+ f32 x1 = field_0x1f0[1]->getBounds().i.x;
+ f32 dVar18 = x1 - field_0x1f0[0]->getBounds().i.x;
f32 dVar17 = dVar18 * (field_0x374 - 1);
f32 dVar16 = (pJVar6->getWidth() / 2) - (dVar17 / 2);
for (int i = 0; i < 9; i++) {
diff --git a/src/d/d_msg_object.cpp b/src/d/d_msg_object.cpp
index 7e3be2cb08..44b5c66571 100644
--- a/src/d/d_msg_object.cpp
+++ b/src/d/d_msg_object.cpp
@@ -427,14 +427,15 @@ static void dummyStrings() {
dMsgObject_HIO_c g_MsgObject_HIO_c;
int dMsgObject_c::_execute() {
-#if TARGET_PC
+// TODO: enabling wii message overrides fixes direction text, but gives wrong item control text
+/*#if TARGET_PC
if (dusk::getSettings().game.enableMirrorMode) {
// enable wii message index override
g_MsgObject_HIO_c.mMessageDisplay = 1;
} else if (!dusk::getSettings().game.enableMirrorMode && g_MsgObject_HIO_c.mMessageDisplay == 1) {
g_MsgObject_HIO_c.mMessageDisplay = 0;
}
-#endif
+#endif*/
field_0x4c7 = 0;
diff --git a/src/dusk/achievements.cpp b/src/dusk/achievements.cpp
index eefcda978f..28e66c891e 100644
--- a/src/dusk/achievements.cpp
+++ b/src/dusk/achievements.cpp
@@ -1030,7 +1030,7 @@ void AchievementSystem::load() {
return;
}
try {
- auto data = io::FileStream::ReadAllBytes(filePath.string().c_str());
+ auto data = io::FileStream::ReadAllBytes(filePath);
auto j = json::parse(data);
if (!j.is_object()) {
return;
@@ -1067,7 +1067,7 @@ void AchievementSystem::save() {
}
try {
io::FileStream::WriteAllText(
- (dusk::ConfigPath / ACHIEVEMENTS_FILENAME).string().c_str(),
+ dusk::ConfigPath / ACHIEVEMENTS_FILENAME,
j.dump(2)
);
} catch (const std::exception&) {}
@@ -1113,7 +1113,7 @@ void AchievementSystem::processEntry(Entry& e) {
if (nowUnlocked) {
e.achievement.progress = e.achievement.isCounter ? e.achievement.goal : 1;
e.achievement.unlocked = true;
- if (getSettings().game.enableAchievementNotifications) {
+ if (getSettings().game.enableAchievementToasts) {
ui::push_toast({
.type = "achievement",
.title = "Achievement Unlocked!",
diff --git a/src/dusk/config.cpp b/src/dusk/config.cpp
index f4af7a2961..8225536d34 100644
--- a/src/dusk/config.cpp
+++ b/src/dusk/config.cpp
@@ -23,8 +23,8 @@ aurora::Module DuskConfigLog("dusk::config");
static absl::flat_hash_map RegisteredConfigVars;
static bool RegistrationDone = false;
-static std::string GetConfigJsonPath() {
- return (dusk::ConfigPath / ConfigFileName).string();
+static std::u8string GetConfigJsonPath() {
+ return (dusk::ConfigPath / ConfigFileName).u8string();
}
ConfigVarBase::ConfigVarBase(const char* name, const ConfigImplBase* impl) : name(name), registered(false), layer(ConfigVarLayer::Default), impl(impl) {
@@ -156,6 +156,7 @@ namespace dusk::config {
template class ConfigImpl;
template class ConfigImpl;
template class ConfigImpl;
+ template class ConfigImpl;
}
void dusk::config::Register(ConfigVarBase& configVar) {
@@ -188,7 +189,7 @@ void dusk::config::LoadFromUserPreferences() {
if (configJsonPath.empty()) {
return;
}
- LoadFromFileName(configJsonPath.c_str());
+ LoadFromFileName(reinterpret_cast(configJsonPath.c_str()));
}
static void LoadFromPath(const char* path) {
@@ -240,7 +241,9 @@ void dusk::config::Save() {
return;
}
- DuskConfigLog.info("Saving config to '{}'", configJsonPath);
+ DuskConfigLog.info(
+ "Saving config to '{}'",
+ reinterpret_cast(configJsonPath.c_str()));
json j;
@@ -250,7 +253,7 @@ void dusk::config::Save() {
}
}
- io::FileStream::WriteAllText(configJsonPath.c_str(), j.dump(4));
+ io::FileStream::WriteAllText(reinterpret_cast(configJsonPath.c_str()), j.dump(4));
}
ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) {
diff --git a/src/dusk/file_select.cpp b/src/dusk/file_select.cpp
index ec388f86cd..95df652940 100644
--- a/src/dusk/file_select.cpp
+++ b/src/dusk/file_select.cpp
@@ -1,9 +1,16 @@
#include "file_select.hpp"
#include
+#include
#include
#include
+#include
+
+#if defined(__ANDROID__) || defined(ANDROID)
+#include
+#include
+#endif
#if defined(__APPLE__)
#include
@@ -19,6 +26,92 @@
namespace dusk {
namespace {
+std::string fallback_display_name(std::string_view path) {
+ if (path.empty()) {
+ return {};
+ }
+
+ std::string pathString(path);
+ const std::size_t slash = pathString.find_last_of("/\\");
+ if (slash == std::string::npos || slash + 1 >= pathString.size()) {
+ return pathString;
+ }
+ return pathString.substr(slash + 1);
+}
+
+#if defined(__ANDROID__) || defined(ANDROID)
+bool clear_pending_exception(JNIEnv* env) {
+ if (env == nullptr || !env->ExceptionCheck()) {
+ return false;
+ }
+ env->ExceptionClear();
+ return true;
+}
+
+std::string to_string(JNIEnv* env, jstring value) {
+ if (env == nullptr || value == nullptr) {
+ return {};
+ }
+
+ const char* utf8 = env->GetStringUTFChars(value, nullptr);
+ if (utf8 == nullptr) {
+ clear_pending_exception(env);
+ return {};
+ }
+
+ std::string result(utf8);
+ env->ReleaseStringUTFChars(value, utf8);
+ return result;
+}
+
+std::string android_display_name(std::string_view path) {
+ auto* env = static_cast(SDL_GetAndroidJNIEnv());
+ if (env == nullptr) {
+ return {};
+ }
+
+ jobject activity = static_cast(SDL_GetAndroidActivity());
+ if (activity == nullptr || clear_pending_exception(env)) {
+ if (activity != nullptr) {
+ env->DeleteLocalRef(activity);
+ }
+ return {};
+ }
+
+ jclass activityClass = env->GetObjectClass(activity);
+ if (activityClass == nullptr || clear_pending_exception(env)) {
+ env->DeleteLocalRef(activity);
+ return {};
+ }
+
+ jmethodID getDisplayName = env->GetMethodID(
+ activityClass, "getDisplayNameForUri", "(Ljava/lang/String;)Ljava/lang/String;");
+ env->DeleteLocalRef(activityClass);
+ if (getDisplayName == nullptr || clear_pending_exception(env)) {
+ env->DeleteLocalRef(activity);
+ return {};
+ }
+
+ jstring uri = env->NewStringUTF(std::string(path).c_str());
+ if (uri == nullptr || clear_pending_exception(env)) {
+ env->DeleteLocalRef(activity);
+ return {};
+ }
+
+ auto* displayName =
+ static_cast(env->CallObjectMethod(activity, getDisplayName, uri));
+ env->DeleteLocalRef(uri);
+ env->DeleteLocalRef(activity);
+ if (displayName == nullptr || clear_pending_exception(env)) {
+ return {};
+ }
+
+ std::string result = to_string(env, displayName);
+ env->DeleteLocalRef(displayName);
+ return result;
+}
+#endif
+
#if USE_IOS_DIALOG
struct IOSDialogCallbackState {
FileCallback callback;
@@ -89,6 +182,18 @@ void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window,
#endif
}
+std::string display_name_for_path(std::string_view path) {
+#if defined(__ANDROID__) || defined(ANDROID)
+ if (path.starts_with("content:") || path.starts_with("file:")) {
+ std::string displayName = android_display_name(path);
+ if (!displayName.empty()) {
+ return displayName;
+ }
+ }
+#endif
+ return fallback_display_name(path);
+}
+
void ShowFolderSelect(FileCallback callback, void* userdata, SDL_Window* window,
const char* default_location) {
if (callback == nullptr) {
diff --git a/src/dusk/file_select.hpp b/src/dusk/file_select.hpp
index 97c739b67a..f523ce90a6 100644
--- a/src/dusk/file_select.hpp
+++ b/src/dusk/file_select.hpp
@@ -2,6 +2,9 @@
#include
+#include
+#include
+
struct SDL_Window;
namespace dusk {
@@ -9,8 +12,10 @@ namespace dusk {
using FileCallback = void (*)(void* userdata, const char* path, const char* error);
void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window,
- const SDL_DialogFileFilter* filters, int nfilters, const char* default_location,
- bool allow_many);
+ const SDL_DialogFileFilter* filters, int nfilters, const char* default_location,
+ bool allow_many);
+
+std::string display_name_for_path(std::string_view path);
void ShowFolderSelect(FileCallback callback, void* userdata, SDL_Window* window,
const char* default_location);
diff --git a/src/dusk/gyro.cpp b/src/dusk/gyro.cpp
index abe22909c1..680d500631 100644
--- a/src/dusk/gyro.cpp
+++ b/src/dusk/gyro.cpp
@@ -1,5 +1,9 @@
#include "dusk/gyro.h"
+#include "dusk/ui/ui.hpp"
#include "d/actor/d_a_alink.h"
+
+#include
+#include
#include
namespace dusk::gyro {
@@ -12,11 +16,14 @@ constexpr float kGravityEmaAlpha = 0.1f;
constexpr float kMinGravityProjection = 0.2f;
// Let roll contribute more strongly as the pad approaches an upright posture.
constexpr float kRollAimBoostMax = 2.0f;
+constexpr float kMousePixelToRad = 0.0025f;
bool s_sensor_enabled = false;
bool s_accel_enabled = false;
bool s_was_aiming = false;
bool s_have_gravity_baseline = false;
+bool s_mouse_enabled = false;
+bool s_mouse_relative = false;
float s_smooth_gx = 0.0f;
float s_smooth_gy = 0.0f;
float s_smooth_gz = 0.0f;
@@ -36,6 +43,7 @@ void reset_filter_state() {
s_baseline_gravity_y = s_baseline_gravity_z = 0.0f;
s_was_aiming = false;
s_have_gravity_baseline = false;
+ s_mouse_enabled = false;
s_yaw_rad = s_pitch_rad = s_roll_rad = 0.0f;
s_rollgoal_ax = s_rollgoal_az = 0;
}
@@ -46,14 +54,29 @@ float apply_deadband(float v, float deadband_rad_s) {
}
return v;
}
+
+void disable_pad_sensors() {
+ if (s_sensor_enabled) {
+ PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_GYRO, FALSE);
+ s_sensor_enabled = false;
+ }
+ if (s_accel_enabled) {
+ PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_ACCEL, FALSE);
+ s_accel_enabled = false;
+ }
+}
} // namespace
bool s_sensor_keep_alive = false;
bool get_sensor_keep_alive() { return s_sensor_keep_alive; }
void set_sensor_keep_alive(bool value) { s_sensor_keep_alive = value; }
+bool rollgoal_gyro_enabled() {
+ return getSettings().game.enableGyroRollgoal && getSettings().game.gyroMode.getValue() != GyroMode::Mouse;
+}
+
bool queryGyroAimContext() {
- if (!static_cast(dusk::getSettings().game.enableGyroAim)) {
+ if (!static_cast(getSettings().game.enableGyroAim)) {
return false;
}
@@ -71,15 +94,28 @@ void read(float dt) {
const bool aim_just_ended = !aim_active && s_was_aiming;
s_was_aiming = aim_active;
+ const bool mouse_mode = getSettings().game.gyroMode.getValue() == GyroMode::Mouse;
+ const bool mouse_gyro_active = !ui::any_document_visible() && mouse_mode && (aim_active || s_sensor_keep_alive);
+ SDL_Window* window = aurora::window::get_sdl_window();
+ if (window != nullptr && mouse_gyro_active != s_mouse_relative &&
+ SDL_SetWindowRelativeMouseMode(window, mouse_gyro_active))
+ {
+ s_mouse_relative = mouse_gyro_active;
+ }
+
+ if (mouse_gyro_active && !s_mouse_enabled && window != nullptr) {
+ const AuroraWindowSize sz = aurora::window::get_window_size();
+ const float cx = static_cast(sz.width) * 0.5f;
+ const float cy = static_cast(sz.height) * 0.5f;
+ SDL_WarpMouseInWindow(window, cx, cy);
+ float discard_x = 0.0f;
+ float discard_y = 0.0f;
+ SDL_GetRelativeMouseState(&discard_x, &discard_y);
+ }
+ s_mouse_enabled = mouse_gyro_active;
+
if (!s_sensor_keep_alive && !aim_active) {
- if (s_sensor_enabled) {
- PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_GYRO, FALSE);
- s_sensor_enabled = false;
- }
- if (s_accel_enabled) {
- PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_ACCEL, FALSE);
- s_accel_enabled = false;
- }
+ disable_pad_sensors();
reset_filter_state();
return;
}
@@ -90,6 +126,31 @@ void read(float dt) {
s_have_gravity_baseline = false;
}
+ if (mouse_mode && !mouse_gyro_active) {
+ s_pitch_rad = 0.0f;
+ s_yaw_rad = 0.0f;
+ s_roll_rad = 0.0f;
+ return;
+ }
+
+ if (mouse_mode) {
+ disable_pad_sensors();
+
+ float mx_rel = 0.0f;
+ float my_rel = 0.0f;
+ SDL_GetRelativeMouseState(&mx_rel, &my_rel);
+ // Convert pixels to radians
+ s_pitch_rad = my_rel * kMousePixelToRad * getSettings().game.gyroSensitivityY;
+ s_yaw_rad = -mx_rel * kMousePixelToRad * getSettings().game.gyroSensitivityX;
+ s_roll_rad = 0.0f;
+
+ s_pitch_rad = getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad;
+ s_yaw_rad = getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad;
+ s_yaw_rad = getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad;
+
+ return;
+ }
+
if (!s_sensor_enabled) {
if (!PADHasSensor(PAD_CHAN0, PAD_SENSOR_GYRO)) {
return;
@@ -112,8 +173,8 @@ void read(float dt) {
return;
}
- const float smooth_alpha = kGyroEmaAlphaMax + dusk::getSettings().game.gyroSmoothing * (kGyroEmaAlphaMin - kGyroEmaAlphaMax);
- const float deadband = dusk::getSettings().game.gyroDeadband;
+ const float smooth_alpha = kGyroEmaAlphaMax + getSettings().game.gyroSmoothing * (kGyroEmaAlphaMin - kGyroEmaAlphaMax);
+ const float deadband = getSettings().game.gyroDeadband;
s_smooth_gx += smooth_alpha * (gyro[0] - s_smooth_gx);
s_smooth_gy += smooth_alpha * (gyro[1] - s_smooth_gy);
@@ -123,8 +184,8 @@ void read(float dt) {
const float yaw_rate = apply_deadband(s_smooth_gy, deadband);
const float roll_rate = apply_deadband(s_smooth_gz, deadband);
- s_pitch_rad = -pitch_rate * dt * dusk::getSettings().game.gyroSensitivityX;
- s_roll_rad = roll_rate * dt * dusk::getSettings().game.gyroSensitivityX; // GYRO NOTE: Exposing Z sensitivity seems unusual, so I'm just using X
+ s_pitch_rad = -pitch_rate * dt * getSettings().game.gyroSensitivityY;
+ s_roll_rad = roll_rate * dt * getSettings().game.gyroSensitivityX; // GYRO NOTE: Exposing Z sensitivity seems unusual, so I'm just using X
float horizontal_rate = yaw_rate;
if (aim_active && s_accel_enabled) {
@@ -162,11 +223,11 @@ void read(float dt) {
}
}
- s_yaw_rad = horizontal_rate * dt * dusk::getSettings().game.gyroSensitivityY;
+ s_yaw_rad = horizontal_rate * dt * getSettings().game.gyroSensitivityX;
- s_pitch_rad = dusk::getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad;
- s_yaw_rad = dusk::getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad;
- s_yaw_rad = dusk::getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad;
+ s_pitch_rad = getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad;
+ s_yaw_rad = getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad;
+ s_yaw_rad = getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad;
}
void getAimDeltas(float& out_yaw, float& out_pitch) {
@@ -180,9 +241,9 @@ void rollgoalTick(bool play_active, s16 camera_yaw) {
return;
}
- float pitch_rad = -s_pitch_rad * dusk::getSettings().game.gyroSensitivityRollgoal;
- float roll_rad = s_roll_rad * dusk::getSettings().game.gyroSensitivityRollgoal;
- roll_rad = dusk::getSettings().game.enableMirrorMode ? -roll_rad : roll_rad;
+ float pitch_rad = -s_pitch_rad * getSettings().game.gyroSensitivityRollgoal;
+ float roll_rad = s_roll_rad * getSettings().game.gyroSensitivityRollgoal;
+ roll_rad = getSettings().game.enableMirrorMode ? -roll_rad : roll_rad;
s_rollgoal_az += cM_rad2s(roll_rad);
cXyz in(roll_rad, 0.0f, pitch_rad);
diff --git a/src/dusk/http/android.cpp b/src/dusk/http/android.cpp
new file mode 100644
index 0000000000..3f4eef8d84
--- /dev/null
+++ b/src/dusk/http/android.cpp
@@ -0,0 +1,402 @@
+#include "http.hpp"
+
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+namespace dusk::http {
+namespace {
+
+constexpr int JavaErrorNone = 0;
+constexpr int JavaErrorInvalidUrl = 1;
+constexpr int JavaErrorUnsupportedScheme = 2;
+constexpr int JavaErrorTimeout = 3;
+constexpr int JavaErrorTooLarge = 4;
+
+int timeout_ms(std::chrono::milliseconds timeout) {
+ const auto count = std::max(1, timeout.count());
+ return static_cast(
+ std::min(count, std::numeric_limits::max()));
+}
+
+jlong max_body_bytes(size_t maxBodyBytes) {
+ return static_cast(std::min(
+ maxBodyBytes, static_cast(std::numeric_limits::max())));
+}
+
+bool clear_pending_exception(JNIEnv* env) {
+ if (env == nullptr || !env->ExceptionCheck()) {
+ return false;
+ }
+ env->ExceptionClear();
+ return true;
+}
+
+std::string to_string(JNIEnv* env, jstring value) {
+ if (env == nullptr || value == nullptr) {
+ return {};
+ }
+
+ const char* utf8 = env->GetStringUTFChars(value, nullptr);
+ if (utf8 == nullptr) {
+ clear_pending_exception(env);
+ return {};
+ }
+
+ std::string result(utf8);
+ env->ReleaseStringUTFChars(value, utf8);
+ return result;
+}
+
+jstring to_jstring(JNIEnv* env, std::string_view value) {
+ if (env == nullptr) {
+ return nullptr;
+ }
+ return env->NewStringUTF(std::string(value).c_str());
+}
+
+Error map_java_error(int error) {
+ switch (error) {
+ case JavaErrorNone:
+ return Error::None;
+ case JavaErrorInvalidUrl:
+ return Error::InvalidUrl;
+ case JavaErrorUnsupportedScheme:
+ return Error::UnsupportedScheme;
+ case JavaErrorTimeout:
+ return Error::Timeout;
+ case JavaErrorTooLarge:
+ return Error::TooLarge;
+ default:
+ return Error::Network;
+ }
+}
+
+jclass load_dusk_class(JNIEnv* env, jobject activity, const char* className) {
+ jclass activityClass = env->GetObjectClass(activity);
+ if (activityClass == nullptr || clear_pending_exception(env)) {
+ return nullptr;
+ }
+
+ jmethodID getClassLoader =
+ env->GetMethodID(activityClass, "getClassLoader", "()Ljava/lang/ClassLoader;");
+ env->DeleteLocalRef(activityClass);
+ if (getClassLoader == nullptr || clear_pending_exception(env)) {
+ return nullptr;
+ }
+
+ jobject classLoader = env->CallObjectMethod(activity, getClassLoader);
+ if (classLoader == nullptr || clear_pending_exception(env)) {
+ return nullptr;
+ }
+
+ jclass classLoaderClass = env->FindClass("java/lang/ClassLoader");
+ if (classLoaderClass == nullptr || clear_pending_exception(env)) {
+ env->DeleteLocalRef(classLoader);
+ return nullptr;
+ }
+
+ jmethodID loadClass = env->GetMethodID(
+ classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
+ env->DeleteLocalRef(classLoaderClass);
+ if (loadClass == nullptr || clear_pending_exception(env)) {
+ env->DeleteLocalRef(classLoader);
+ return nullptr;
+ }
+
+ jstring javaClassName = env->NewStringUTF(className);
+ if (javaClassName == nullptr || clear_pending_exception(env)) {
+ env->DeleteLocalRef(classLoader);
+ return nullptr;
+ }
+
+ auto* loadedClass =
+ static_cast(env->CallObjectMethod(classLoader, loadClass, javaClassName));
+ env->DeleteLocalRef(javaClassName);
+ env->DeleteLocalRef(classLoader);
+ if (loadedClass == nullptr || clear_pending_exception(env)) {
+ return nullptr;
+ }
+
+ return loadedClass;
+}
+
+jobjectArray make_string_array(JNIEnv* env, const std::vector& headers, bool names) {
+ jclass stringClass = env->FindClass("java/lang/String");
+ if (stringClass == nullptr || clear_pending_exception(env)) {
+ return nullptr;
+ }
+
+ jobjectArray array =
+ env->NewObjectArray(static_cast(headers.size()), stringClass, nullptr);
+ env->DeleteLocalRef(stringClass);
+ if (array == nullptr || clear_pending_exception(env)) {
+ return nullptr;
+ }
+
+ for (jsize i = 0; i < static_cast(headers.size()); ++i) {
+ const std::string& value = names ? headers[static_cast(i)].name :
+ headers[static_cast(i)].value;
+ jstring javaValue = to_jstring(env, value);
+ if (javaValue == nullptr || clear_pending_exception(env)) {
+ env->DeleteLocalRef(array);
+ return nullptr;
+ }
+ env->SetObjectArrayElement(array, i, javaValue);
+ env->DeleteLocalRef(javaValue);
+ if (clear_pending_exception(env)) {
+ env->DeleteLocalRef(array);
+ return nullptr;
+ }
+ }
+
+ return array;
+}
+
+std::vector read_headers(JNIEnv* env, jobjectArray names, jobjectArray values) {
+ std::vector headers;
+ if (names == nullptr || values == nullptr) {
+ return headers;
+ }
+
+ const jsize count = std::min(env->GetArrayLength(names), env->GetArrayLength(values));
+ headers.reserve(static_cast(count));
+ for (jsize i = 0; i < count; ++i) {
+ auto* name = static_cast(env->GetObjectArrayElement(names, i));
+ auto* value = static_cast(env->GetObjectArrayElement(values, i));
+ if (clear_pending_exception(env)) {
+ if (name != nullptr) {
+ env->DeleteLocalRef(name);
+ }
+ if (value != nullptr) {
+ env->DeleteLocalRef(value);
+ }
+ headers.clear();
+ return headers;
+ }
+
+ if (name != nullptr) {
+ headers.push_back({
+ .name = to_string(env, name),
+ .value = to_string(env, value),
+ });
+ }
+
+ if (name != nullptr) {
+ env->DeleteLocalRef(name);
+ }
+ if (value != nullptr) {
+ env->DeleteLocalRef(value);
+ }
+ }
+
+ return headers;
+}
+
+std::string read_body(JNIEnv* env, jbyteArray body) {
+ if (body == nullptr) {
+ return {};
+ }
+
+ const jsize bodySize = env->GetArrayLength(body);
+ std::string result(static_cast(bodySize), '\0');
+ if (bodySize > 0) {
+ env->GetByteArrayRegion(body, 0, bodySize, reinterpret_cast(result.data()));
+ if (clear_pending_exception(env)) {
+ return {};
+ }
+ }
+ return result;
+}
+
+Result result_from_response(JNIEnv* env, jobject response) {
+ if (response == nullptr) {
+ return {
+ .error = Error::Network,
+ .message = "Android HTTP request did not return a response",
+ };
+ }
+
+ jclass responseClass = env->GetObjectClass(response);
+ if (responseClass == nullptr || clear_pending_exception(env)) {
+ return {
+ .error = Error::Network,
+ .message = "Failed to inspect Android HTTP response",
+ };
+ }
+
+ jfieldID errorField = env->GetFieldID(responseClass, "error", "I");
+ jfieldID messageField = env->GetFieldID(responseClass, "message", "Ljava/lang/String;");
+ jfieldID statusField = env->GetFieldID(responseClass, "statusCode", "I");
+ jfieldID headerNamesField =
+ env->GetFieldID(responseClass, "headerNames", "[Ljava/lang/String;");
+ jfieldID headerValuesField =
+ env->GetFieldID(responseClass, "headerValues", "[Ljava/lang/String;");
+ jfieldID bodyField = env->GetFieldID(responseClass, "body", "[B");
+ env->DeleteLocalRef(responseClass);
+ if (errorField == nullptr || messageField == nullptr || statusField == nullptr ||
+ headerNamesField == nullptr || headerValuesField == nullptr || bodyField == nullptr ||
+ clear_pending_exception(env))
+ {
+ return {
+ .error = Error::Network,
+ .message = "Android HTTP response shape was not recognized",
+ };
+ }
+
+ const int javaError = env->GetIntField(response, errorField);
+ auto* message = static_cast(env->GetObjectField(response, messageField));
+ auto* headerNames = static_cast(env->GetObjectField(response, headerNamesField));
+ auto* headerValues =
+ static_cast(env->GetObjectField(response, headerValuesField));
+ auto* body = static_cast(env->GetObjectField(response, bodyField));
+ if (clear_pending_exception(env)) {
+ return {
+ .error = Error::Network,
+ .message = "Failed to read Android HTTP response",
+ };
+ }
+
+ Response httpResponse{
+ .statusCode = static_cast(env->GetIntField(response, statusField)),
+ .headers = read_headers(env, headerNames, headerValues),
+ .body = read_body(env, body),
+ };
+
+ std::string messageString = to_string(env, message);
+
+ if (message != nullptr) {
+ env->DeleteLocalRef(message);
+ }
+ if (headerNames != nullptr) {
+ env->DeleteLocalRef(headerNames);
+ }
+ if (headerValues != nullptr) {
+ env->DeleteLocalRef(headerValues);
+ }
+ if (body != nullptr) {
+ env->DeleteLocalRef(body);
+ }
+
+ return {
+ .error = map_java_error(javaError),
+ .message = std::move(messageString),
+ .response = std::move(httpResponse),
+ };
+}
+
+} // namespace
+
+bool available() noexcept {
+ return true;
+}
+
+Backend backend() noexcept {
+ return Backend::Android;
+}
+
+const char* backend_name() noexcept {
+ return "Android";
+}
+
+Result get(const Request& request) {
+ if (request.url.empty()) {
+ return {
+ .error = Error::InvalidUrl,
+ .message = "URL is empty",
+ };
+ }
+ if (!request.url.starts_with("https://")) {
+ return {
+ .error = Error::UnsupportedScheme,
+ .message = "Only https:// URLs are supported",
+ };
+ }
+
+ auto* env = static_cast(SDL_GetAndroidJNIEnv());
+ if (env == nullptr) {
+ return {
+ .error = Error::Network,
+ .message = "Failed to access Android JNI environment",
+ };
+ }
+
+ jobject activity = static_cast(SDL_GetAndroidActivity());
+ if (activity == nullptr || clear_pending_exception(env)) {
+ if (activity != nullptr) {
+ env->DeleteLocalRef(activity);
+ }
+ return {
+ .error = Error::Network,
+ .message = "Failed to access Android activity",
+ };
+ }
+
+ jclass clientClass =
+ load_dusk_class(env, activity, "dev.twilitrealm.dusk.DuskHttpClient");
+ env->DeleteLocalRef(activity);
+ if (clientClass == nullptr) {
+ return {
+ .error = Error::Network,
+ .message = "Failed to load Android HTTP helper",
+ };
+ }
+
+ jmethodID getMethod = env->GetStaticMethodID(clientClass, "get",
+ "(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;IJ)"
+ "Ldev/twilitrealm/dusk/DuskHttpClient$Response;");
+ if (getMethod == nullptr || clear_pending_exception(env)) {
+ env->DeleteLocalRef(clientClass);
+ return {
+ .error = Error::Network,
+ .message = "Failed to find Android HTTP helper method",
+ };
+ }
+
+ jstring url = to_jstring(env, request.url);
+ jobjectArray headerNames = make_string_array(env, request.headers, true);
+ jobjectArray headerValues = make_string_array(env, request.headers, false);
+ if (url == nullptr || headerNames == nullptr || headerValues == nullptr ||
+ clear_pending_exception(env))
+ {
+ if (url != nullptr) {
+ env->DeleteLocalRef(url);
+ }
+ if (headerNames != nullptr) {
+ env->DeleteLocalRef(headerNames);
+ }
+ if (headerValues != nullptr) {
+ env->DeleteLocalRef(headerValues);
+ }
+ env->DeleteLocalRef(clientClass);
+ return {
+ .error = Error::Network,
+ .message = "Failed to prepare Android HTTP request",
+ };
+ }
+
+ jobject response = env->CallStaticObjectMethod(clientClass, getMethod, url, headerNames,
+ headerValues, timeout_ms(request.timeout), max_body_bytes(request.maxBodyBytes));
+ env->DeleteLocalRef(url);
+ env->DeleteLocalRef(headerNames);
+ env->DeleteLocalRef(headerValues);
+ env->DeleteLocalRef(clientClass);
+ if (clear_pending_exception(env)) {
+ return {
+ .error = Error::Network,
+ .message = "Android HTTP request failed with a Java exception",
+ };
+ }
+
+ Result result = result_from_response(env, response);
+ if (response != nullptr) {
+ env->DeleteLocalRef(response);
+ }
+ return result;
+}
+
+} // namespace dusk::http
diff --git a/src/dusk/http/http.hpp b/src/dusk/http/http.hpp
index d62b031fbc..54bef6eec1 100644
--- a/src/dusk/http/http.hpp
+++ b/src/dusk/http/http.hpp
@@ -13,6 +13,7 @@ enum class Backend {
WinHttp,
UrlSession,
LibCurl,
+ Android,
};
enum class Error {
diff --git a/src/dusk/imgui/ImGuiConsole.cpp b/src/dusk/imgui/ImGuiConsole.cpp
index e933e314e4..dca2a88d79 100644
--- a/src/dusk/imgui/ImGuiConsole.cpp
+++ b/src/dusk/imgui/ImGuiConsole.cpp
@@ -253,13 +253,6 @@ namespace dusk {
UpdateSettings();
- if (!fpcM_SearchByName(fpcNm_LOGO_SCENE_e) &&
- (ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) &&
- ImGui::IsKeyPressed(ImGuiKey_R))
- {
- JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
- }
-
if (ImGui::IsKeyPressed(ImGuiKey_F11)) {
getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
VISetWindowFullscreen(getSettings().video.enableFullscreen);
@@ -320,7 +313,9 @@ namespace dusk {
ImGui::PopFont();
}
ImGui::PushFont(ImGuiEngine::fontLarge);
- ImGuiTextCenter("Failed to initialize any graphics backend");
+ 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 Dusk may help. For further assistance, please visit #tech-support on the Twilit Realm Discord server.");
const auto& style = ImGui::GetStyle();
const auto retrySize = ImGui::CalcTextSize("Retry (Auto backend)");
const auto quitSize = ImGui::CalcTextSize("Quit");
@@ -383,18 +378,18 @@ namespace dusk {
}
// Hide mouse cursor if the F1 menu is not open and the cursor is idle for 3 seconds.
- ImGuiIO& io = ImGui::GetIO();
- if (showMenu) {
- mouseHideTimer = 0.0f;
- ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; // Imgui will re-show cursor.
- } else if (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f) {
- mouseHideTimer = 0.0f;
- ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; // Imgui will re-show cursor.
- } else if (mouseHideTimer <= 3.0f) {
- mouseHideTimer += ImGui::GetIO().DeltaTime;
- } else {
- ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;
- SDL_HideCursor();
+ if (dusk::getSettings().game.gyroMode.getValue() != GyroMode::Mouse)
+ {
+ ImGuiIO& io = ImGui::GetIO();
+ if (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f) {
+ mouseHideTimer = 0.0f;
+ ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; // Imgui will re-show cursor.
+ } else if (mouseHideTimer <= 3.0f) {
+ mouseHideTimer += ImGui::GetIO().DeltaTime;
+ } else {
+ ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;
+ SDL_HideCursor();
+ }
}
ShowToasts();
diff --git a/src/dusk/imgui/ImGuiConsole.hpp b/src/dusk/imgui/ImGuiConsole.hpp
index 6715846e7b..6362a146b6 100644
--- a/src/dusk/imgui/ImGuiConsole.hpp
+++ b/src/dusk/imgui/ImGuiConsole.hpp
@@ -2,11 +2,9 @@
#define DUSK_IMGUI_HPP
#include
-#include
#include
#include
-#include
#include
#include "ImGuiMenuGame.hpp"
@@ -73,24 +71,4 @@ bool ImGuiButtonCenter(std::string_view text);
float ImGuiScale();
} // namespace dusk
-#if defined(_WIN32) || \
- (defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_TV && !TARGET_OS_MACCATALYST) || \
- (defined(__linux__) && !defined(__ANDROID__))
-#define DUSK_CAN_OPEN_DATA_FOLDER 1
-
-namespace fs = std::filesystem;
-
-static void OpenDataFolder() {
- const std::string path = fs::absolute(dusk::ConfigPath).generic_string();
-#if defined(_WIN32)
- const std::string url = std::string("file:///") + path;
-#else
- const std::string url = std::string("file://") + path;
-#endif
- (void)SDL_OpenURL(url.c_str());
-}
-#else
-#define DUSK_CAN_OPEN_DATA_FOLDER 0
-#endif
-
#endif // DUSK_IMGUI_HPP
diff --git a/src/dusk/imgui/ImGuiMenuGame.cpp b/src/dusk/imgui/ImGuiMenuGame.cpp
index 79c032ee35..783bbbdeec 100644
--- a/src/dusk/imgui/ImGuiMenuGame.cpp
+++ b/src/dusk/imgui/ImGuiMenuGame.cpp
@@ -56,12 +56,12 @@ namespace dusk {
}
// L+R+A+Start to reset timer
- if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigStart(PAD_1)) {
+ if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigZ(PAD_1)) {
m_speedrunInfo.reset();
}
// L+R+A+Z to manually stop timer
- if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigZ(PAD_1)) {
+ if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigY(PAD_1)) {
if (m_speedrunInfo.m_isRunStarted) {
m_speedrunInfo.m_endTimestamp = OSGetTime() - m_speedrunInfo.m_startTimestamp;
m_speedrunInfo.m_isRunStarted = false;
diff --git a/src/dusk/imgui/ImGuiStateShare.cpp b/src/dusk/imgui/ImGuiStateShare.cpp
index a0bd450984..48849e2bc3 100644
--- a/src/dusk/imgui/ImGuiStateShare.cpp
+++ b/src/dusk/imgui/ImGuiStateShare.cpp
@@ -48,19 +48,18 @@ void ImGuiStateShare::onMergeFileSelected(void* userdata, const char* path, cons
-static std::string GetStatesFilePath() {
- return (dusk::ConfigPath / STATES_FILENAME).string();
+static std::filesystem::path GetStatesFilePath() {
+ return ConfigPath / STATES_FILENAME;
}
void ImGuiStateShare::loadStatesFile() {
m_loaded = true;
- const std::filesystem::path filePath = dusk::ConfigPath / STATES_FILENAME;
+ const std::filesystem::path filePath = GetStatesFilePath();
if (!std::filesystem::exists(filePath)) {
return;
}
try {
- const std::string pathStr = filePath.string();
- auto data = io::FileStream::ReadAllBytes(pathStr.c_str());
+ auto data = io::FileStream::ReadAllBytes(filePath);
auto j = json::parse(data);
if (!j.is_array()) {
return;
@@ -85,7 +84,7 @@ void ImGuiStateShare::saveStatesFile() {
j.push_back(json{{"name", s.name}, {"data", s.encoded}});
}
try {
- io::FileStream::WriteAllText(GetStatesFilePath().c_str(), j.dump(2));
+ io::FileStream::WriteAllText(GetStatesFilePath(), j.dump(2));
} catch (const std::exception& e) {
m_statusMsg = fmt::format("Failed to save states: {}", e.what());
}
diff --git a/src/dusk/io.cpp b/src/dusk/io.cpp
index 50b5cab5bd..3c87a66acf 100644
--- a/src/dusk/io.cpp
+++ b/src/dusk/io.cpp
@@ -1,7 +1,8 @@
-#include "dusk/io.hpp"
#include
#include
+#include "dusk/io.hpp"
+
using namespace dusk::io;
#if _WIN32
@@ -58,7 +59,7 @@ static FILE* OpenCore(const char* path, const MODE_TYPE* mode) {
FileStream::FileStream() noexcept : file(nullptr) {
}
-FileStream::FileStream(void* file) : file(file) {
+FileStream::FileStream(FILE* file) : file(file) {
if (!file) {
CRASH("Invalid file handle");
}
@@ -78,10 +79,18 @@ FileStream FileStream::OpenRead(const char* utf8Path) {
return FileStream(OpenCore(utf8Path, MODE("rb")));
}
+FileStream FileStream::OpenRead(const std::filesystem::path& path) {
+ return FileStream(OpenCore(path, MODE("rb")));
+}
+
FileStream FileStream::Create(const char* utf8Path) {
return FileStream(OpenCore(utf8Path, MODE("wb")));
}
+FileStream FileStream::Create(const std::filesystem::path& path) {
+ return FileStream(OpenCore(path, MODE("wb")));
+}
+
std::vector FileStream::ReadFull() {
const auto fileHandle = ThrowIfNotOpen(*this);
@@ -128,7 +137,11 @@ std::vector FileStream::ReadFull() {
}
std::vector FileStream::ReadAllBytes(const char* utf8Path) {
- auto handle = OpenRead(utf8Path);
+ return ReadAllBytes(reinterpret_cast(utf8Path));
+}
+
+std::vector FileStream::ReadAllBytes(const std::filesystem::path& path) {
+ auto handle = OpenRead(path);
return std::move(handle.ReadFull());
}
@@ -142,6 +155,16 @@ void FileStream::Write(const char* data, size_t dataLen) {
}
void FileStream::WriteAllText(const char* utf8Path, const std::string_view text) {
- auto handle = Create(utf8Path);
+ WriteAllText(reinterpret_cast(utf8Path), text);
+}
+
+void FileStream::WriteAllText(const std::filesystem::path& path, const std::string_view text) {
+ auto handle = Create(path);
handle.Write(text.data(), text.size());
}
+
+FILE* FileStream::ToInner() {
+ auto handle = file;
+ file = nullptr;
+ return handle;
+}
\ No newline at end of file
diff --git a/src/dusk/iso_validate.cpp b/src/dusk/iso_validate.cpp
index 4d9c48d826..00cd365ca9 100644
--- a/src/dusk/iso_validate.cpp
+++ b/src/dusk/iso_validate.cpp
@@ -9,6 +9,8 @@
#include
#include
+#include "dusk/logging.h"
+
namespace {
constexpr uint8_t hex_nibble_to_u8(char c) {
@@ -42,6 +44,18 @@ constexpr XXH128_hash_t parse_xxh128(std::string_view hex) {
};
}
+const char* verification_state_name(dusk::DiscVerificationState state) noexcept {
+ switch (state) {
+ case dusk::DiscVerificationState::Success:
+ return "verified";
+ case dusk::DiscVerificationState::HashMismatch:
+ return "hash mismatch";
+ case dusk::DiscVerificationState::Unknown:
+ default:
+ return "unknown";
+ }
+}
+
} // namespace
namespace dusk::iso {
@@ -248,4 +262,10 @@ bool isPal(const char* path) {
DiscInfo info{};
return inspect(path, info) == ValidationError::Success && info.isPal;
}
+
+void log_verification_state(std::string_view path, DiscVerificationState state) {
+ const std::string pathText = path.empty() ? "" : std::string(path);
+ DuskLog.info(
+ "Disc verification status: {} (path: {})", verification_state_name(state), pathText);
+}
} // namespace dusk::iso
diff --git a/src/dusk/iso_validate.hpp b/src/dusk/iso_validate.hpp
index c737a017d9..176284fd02 100644
--- a/src/dusk/iso_validate.hpp
+++ b/src/dusk/iso_validate.hpp
@@ -31,6 +31,7 @@ struct DiscInfo {
ValidationError inspect(const char* path, DiscInfo& info);
ValidationError validate(const char* path, VerificationStatus& status, DiscInfo& info);
bool isPal(const char* path);
+void log_verification_state(std::string_view path, DiscVerificationState state);
} // namespace dusk::iso
diff --git a/src/dusk/logging.cpp b/src/dusk/logging.cpp
index 8b96fcffd5..1c0a6ecbdb 100644
--- a/src/dusk/logging.cpp
+++ b/src/dusk/logging.cpp
@@ -8,6 +8,7 @@
#include
#include
+#include "dusk/io.hpp"
#include "tracy/Tracy.hpp"
#if TARGET_ANDROID
@@ -40,7 +41,7 @@ std::atomic g_logStateAlive(true);
struct LogState {
std::mutex mutex;
FILE* file = nullptr;
- std::string filePath;
+ std::u8string filePath;
~LogState() {
CloseFile();
@@ -207,19 +208,19 @@ void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraL
std::filesystem::create_directories(logsDir, ec);
if (ec) {
std::fprintf(stderr, "[WARNING | dusk] Failed to create log directory '%s': %s\n",
- logsDir.string().c_str(), ec.message().c_str());
+ io::fs_path_to_string(logsDir).c_str(), ec.message().c_str());
return;
}
const std::filesystem::path logPath = logsDir / MakeTimestampedLogName();
- g_logState.file = std::fopen(logPath.string().c_str(), "wb");
+ g_logState.file = io::FileStream::Create(logPath).ToInner();
if (g_logState.file == nullptr) {
std::fprintf(stderr, "[WARNING | dusk] Failed to open log file '%s'\n",
- logPath.string().c_str());
+ io::fs_path_to_string(logPath).c_str());
return;
}
- g_logState.filePath = logPath.string();
+ g_logState.filePath = logPath.u8string();
aurora::g_config.logCallback = &aurora_log_callback;
aurora::g_config.logLevel = logLevel;
WriteLogLine(g_logState.file, "INFO", "dusk", "File logging initialized", 24);
@@ -237,5 +238,6 @@ const char* dusk::GetLogFilePath() {
return nullptr;
}
std::lock_guard lock(g_logState.mutex);
- return g_logState.filePath.empty() ? nullptr : g_logState.filePath.c_str();
+ return reinterpret_cast(
+ g_logState.filePath.empty() ? nullptr : g_logState.filePath.c_str());
}
diff --git a/src/dusk/main.cpp b/src/dusk/main.cpp
index e1b2fd0b6e..d88594248a 100644
--- a/src/dusk/main.cpp
+++ b/src/dusk/main.cpp
@@ -6,6 +6,7 @@
#include
#include "dusk/main.h"
+#include "dusk/io.hpp"
#include
#include
@@ -91,7 +92,7 @@ bool RestartProcess(int argc, char* argv[]) {
std::vector args;
args.reserve(static_cast(std::max(argc, 1)));
- args.push_back(executablePath.string());
+ args.push_back(dusk::io::fs_path_to_string(executablePath));
for (int i = 1; i < argc; ++i) {
args.emplace_back(argv[i] != nullptr ? argv[i] : "");
}
diff --git a/src/dusk/settings.cpp b/src/dusk/settings.cpp
index c01ccf5c7c..64673893ad 100644
--- a/src/dusk/settings.cpp
+++ b/src/dusk/settings.cpp
@@ -50,7 +50,8 @@ UserSettings g_userSettings = {
.minimalHUD {"game.minimalHUD", false},
.pauseOnFocusLost {"game.pauseOnFocusLost", false},
.enableLinkDollRotation {"game.enableLinkDollRotation", false},
- .enableAchievementNotifications {"game.enableAchievementNotifications", true},
+ .enableAchievementToasts {"game.enableAchievementToasts", true},
+ .enableControllerToasts {"game.enableControllerToasts", true},
// Graphics
.bloomMode {"game.bloomMode", BloomMode::Dusk},
@@ -67,6 +68,7 @@ UserSettings g_userSettings = {
.midnasLamentNonStop {"game.midnasLamentNonStop", false},
// Input
+ .gyroMode {"game.gyroMode", GyroMode::Sensor},
.enableGyroAim {"game.enableGyroAim", false},
.enableGyroRollgoal {"game.enableGyroRollgoal", false},
.gyroSensitivityX {"game.gyroSensitivityX", 1.0f},
@@ -82,6 +84,7 @@ UserSettings g_userSettings = {
.freeCameraSensitivity {"game.freeCameraSensitivity", 1.0f},
.debugFlyCam {"game.debugFlyCam", false},
.debugFlyCamLockEvents {"game.debugFlyCamLockEvents", true},
+ .allowBackgroundInput {"game.allowBackgroundInput", true},
// Cheats
.infiniteHearts {"game.infiniteHearts", false},
@@ -186,7 +189,8 @@ void registerSettings() {
Register(g_userSettings.game.freeMagicArmor);
Register(g_userSettings.game.restoreWiiGlitches);
Register(g_userSettings.game.enableLinkDollRotation);
- Register(g_userSettings.game.enableAchievementNotifications);
+ Register(g_userSettings.game.enableAchievementToasts);
+ Register(g_userSettings.game.enableControllerToasts);
Register(g_userSettings.game.noMissClimbing);
Register(g_userSettings.game.noLowHpSound);
Register(g_userSettings.game.midnasLamentNonStop);
@@ -206,6 +210,7 @@ void registerSettings() {
Register(g_userSettings.game.superClawshot);
Register(g_userSettings.game.alwaysGreatspin);
Register(g_userSettings.game.enableFrameInterpolation);
+ Register(g_userSettings.game.gyroMode);
Register(g_userSettings.game.enableGyroAim);
Register(g_userSettings.game.enableGyroRollgoal);
Register(g_userSettings.game.gyroSensitivityX);
@@ -218,6 +223,7 @@ void registerSettings() {
Register(g_userSettings.game.freeCamera);
Register(g_userSettings.game.debugFlyCam);
Register(g_userSettings.game.debugFlyCamLockEvents);
+ Register(g_userSettings.game.allowBackgroundInput);
Register(g_userSettings.backend.isoPath);
Register(g_userSettings.backend.isoVerification);
diff --git a/src/dusk/ui/document.cpp b/src/dusk/ui/document.cpp
index a7bcc3f9ed..4296ac27d9 100644
--- a/src/dusk/ui/document.cpp
+++ b/src/dusk/ui/document.cpp
@@ -5,6 +5,7 @@
#include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h"
+#include
namespace dusk::ui {
namespace {
@@ -106,6 +107,7 @@ bool Document::visible() const {
bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) {
if (cmd == NavCommand::Menu) {
+ toggle_cursor_if_gyro(!visible());
mDoAud_seStartMenu(visible() ? kSoundMenuClose : kSoundMenuOpen);
toggle();
return true;
@@ -113,4 +115,17 @@ bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) {
return false;
}
+void Document::toggle_cursor_if_gyro(bool cursor_enabled) {
+ if (dusk::getSettings().game.gyroMode.getValue() == GyroMode::Mouse)
+ {
+ if (cursor_enabled) {
+ ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange;
+ SDL_ShowCursor();
+ } else {
+ ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;
+ SDL_HideCursor();
+ }
+ }
+}
+
} // namespace dusk::ui
diff --git a/src/dusk/ui/document.hpp b/src/dusk/ui/document.hpp
index d0f4cae841..eed489beb3 100644
--- a/src/dusk/ui/document.hpp
+++ b/src/dusk/ui/document.hpp
@@ -43,6 +43,8 @@ public:
bool pending_close() const { return mPendingClose; }
bool closed() const { return mClosed; }
+ void toggle_cursor_if_gyro(bool);
+
protected:
virtual bool handle_nav_command(Rml::Event& event, NavCommand cmd);
diff --git a/src/dusk/ui/editor.cpp b/src/dusk/ui/editor.cpp
index 09216a3b8a..469bb920e3 100644
--- a/src/dusk/ui/editor.cpp
+++ b/src/dusk/ui/editor.cpp
@@ -1881,9 +1881,9 @@ EditorWindow::EditorWindow() {
}
});
- add_tab("Flags", [this](Rml::Element* content) {
- // TODO
- });
+ //add_tab("Flags", [this](Rml::Element* content) {
+ // // TODO
+ //});
add_tab("Minigame", [this](Rml::Element* content) {
auto& leftPane = add_child(content, Pane::Type::Controlled);
diff --git a/src/dusk/ui/graphics_tuner.cpp b/src/dusk/ui/graphics_tuner.cpp
index 8a11528914..4f68d4a078 100644
--- a/src/dusk/ui/graphics_tuner.cpp
+++ b/src/dusk/ui/graphics_tuner.cpp
@@ -193,9 +193,9 @@ Rml::String format_graphics_setting_value(GraphicsOption option, int value) {
return "";
}
-GraphicsTuner::GraphicsTuner(GraphicsTunerProps props)
+GraphicsTuner::GraphicsTuner(GraphicsTunerProps props, bool prelaunch)
: Document(kDocumentSource), mOption(props.option), mValueMin(props.valueMin),
- mValueMax(props.valueMax), mDefaultValue(props.defaultValue) {
+ mValueMax(props.valueMax), mDefaultValue(props.defaultValue), mPrelaunch(prelaunch) {
if (mDocument == nullptr) {
return;
}
@@ -207,7 +207,7 @@ GraphicsTuner::GraphicsTuner(GraphicsTunerProps props)
description->SetInnerRML(escape(props.helpText));
}
if (auto* carouselParent = mDocument->GetElementById("carousel-container")) {
- add_component(carouselParent,
+ mCarousel = &add_component(carouselParent,
SteppedCarousel::Props{
.min = mValueMin,
.max = mValueMax,
@@ -281,7 +281,12 @@ bool GraphicsTuner::handle_nav_command(Rml::Event& event, NavCommand cmd) {
pop();
return true;
}
- return Document::handle_nav_command(event, cmd);
+
+ if (mCarousel && mCarousel->handle_nav_command(cmd)) {
+ return true;
+ }
+
+ return mPrelaunch ? false : Document::handle_nav_command(event, cmd);
}
void GraphicsTuner::reset_default() {
diff --git a/src/dusk/ui/graphics_tuner.hpp b/src/dusk/ui/graphics_tuner.hpp
index 3f2418ef30..254aada22b 100644
--- a/src/dusk/ui/graphics_tuner.hpp
+++ b/src/dusk/ui/graphics_tuner.hpp
@@ -28,9 +28,9 @@ public:
bool focus() override;
void update() override;
+ bool handle_nav_command(NavCommand cmd);
private:
- bool handle_nav_command(NavCommand cmd);
void apply(int value);
Props mProps;
@@ -59,7 +59,7 @@ struct GraphicsTunerProps {
class GraphicsTuner : public Document {
public:
- explicit GraphicsTuner(GraphicsTunerProps props);
+ explicit GraphicsTuner(GraphicsTunerProps props, bool prelaunch);
void show() override;
void hide(bool close) override;
@@ -86,7 +86,9 @@ private:
int mValueMax = 0;
int mDefaultValue = 0;
std::vector > mComponents;
+ SteppedCarousel* mCarousel;
Rml::Element* mRoot;
+ bool mPrelaunch;
};
} // namespace dusk::ui
diff --git a/src/dusk/ui/menu_bar.cpp b/src/dusk/ui/menu_bar.cpp
index 81c2eea441..ea791d745b 100644
--- a/src/dusk/ui/menu_bar.cpp
+++ b/src/dusk/ui/menu_bar.cpp
@@ -42,6 +42,7 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById(
mTabBar = std::make_unique(mRoot, TabBar::Props{
.onClose =
[this] {
+ toggle_cursor_if_gyro(false);
mDoAud_seStartMenu(kSoundMenuClose);
hide(false);
},
@@ -203,6 +204,7 @@ bool MenuBar::handle_nav_command(Rml::Event& event, NavCommand cmd) {
return true;
}
if (cmd == NavCommand::Cancel && visible()) {
+ toggle_cursor_if_gyro(false);
mDoAud_seStartMenu(kSoundMenuClose);
hide(false);
return true;
diff --git a/src/dusk/ui/number_button.cpp b/src/dusk/ui/number_button.cpp
index ab5095cf3f..3735855140 100644
--- a/src/dusk/ui/number_button.cpp
+++ b/src/dusk/ui/number_button.cpp
@@ -54,7 +54,7 @@ void NumberButton::set_value(Rml::String value) {
}
bool NumberButton::handle_nav_command(NavCommand cmd) {
- if (cmd == NavCommand::Left || cmd == NavCommand::Right) {
+ if (!is_editing() && (cmd == NavCommand::Left || cmd == NavCommand::Right)) {
const int newValue = std::clamp(
mGetValue() + (cmd == NavCommand::Right ? mStep : -mStep), mMin, mMax);
if (newValue != mGetValue()) {
@@ -66,4 +66,4 @@ bool NumberButton::handle_nav_command(NavCommand cmd) {
return BaseStringButton::handle_nav_command(cmd);
}
-} // namespace dusk::ui
\ No newline at end of file
+} // namespace dusk::ui
diff --git a/src/dusk/ui/overlay.cpp b/src/dusk/ui/overlay.cpp
index 0ce0b904ac..45482230a9 100644
--- a/src/dusk/ui/overlay.cpp
+++ b/src/dusk/ui/overlay.cpp
@@ -10,6 +10,10 @@
#include
#include
+#if defined(__APPLE__)
+#include
+#endif
+
namespace dusk::ui {
namespace {
aurora::Module Log{"dusk::ui::overlay"};
@@ -33,7 +37,7 @@ constexpr std::array, 3> kAutoSaveLayers{{
constexpr auto kMenuNotificationDuration = std::chrono::milliseconds(2500);
-constexpr std::array kFpsCorners = { "tl", "tr", "bl", "br" };
+constexpr std::array kFpsCorners = {"tl", "tr", "bl", "br"};
Rml::Element* create_toast(Rml::Element* parent, const Toast& toast) {
if (toast.type == "autosave") {
@@ -130,13 +134,19 @@ Rml::String back_button_name() {
return "Back";
}
+#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";
+#endif
+
Rml::Element* create_menu_notification(Rml::Element* parent) {
auto* elem = append(parent, "toast");
elem->SetClass("menu-notification", true);
auto* message = append(elem, "message");
auto* row = append(message, "row");
- append(row, "span")->SetInnerRML("Press F1 or");
+ append(row, "span")->SetInnerRML(kMenuNotificationPrefix);
auto* icon = append(row, "icon");
icon->SetClass("controller", true);
append(row, "span")->SetInnerRML(escape(back_button_name()));
@@ -242,7 +252,8 @@ void Overlay::update() {
const Uint64 now = SDL_GetPerformanceCounter();
// Limit updates to twice per second
- const bool refreshLabel = perfFreq == 0 || mFpsLastUpdate == 0 ||
+ const bool refreshLabel =
+ perfFreq == 0 || mFpsLastUpdate == 0 ||
static_cast(now - mFpsLastUpdate) >= 0.5 * static_cast(perfFreq);
if (refreshLabel) {
mFpsLastUpdate = now;
diff --git a/src/dusk/ui/prelaunch.cpp b/src/dusk/ui/prelaunch.cpp
index 41db426e6d..69e9bd57eb 100644
--- a/src/dusk/ui/prelaunch.cpp
+++ b/src/dusk/ui/prelaunch.cpp
@@ -302,9 +302,17 @@ std::string get_error_msg(iso::ValidationError error) {
}
void persist_disc_choice(const std::string& path, iso::ValidationError validation) {
+ const auto previousPath = getSettings().backend.isoPath.getValue();
+ const auto previousVerification = getSettings().backend.isoVerification.getValue();
+ const auto verification = verification_to_config(validation);
+
getSettings().backend.isoPath.setValue(path);
- getSettings().backend.isoVerification.setValue(verification_to_config(validation));
+ getSettings().backend.isoVerification.setValue(verification);
config::Save();
+
+ if (previousPath != path || previousVerification != verification) {
+ iso::log_verification_state(path, verification);
+ }
}
void apply_valid_disc_result(
@@ -445,8 +453,7 @@ private:
}
if (mFileName != nullptr) {
- std::string fileName =
- std::filesystem::path(sDiscVerificationTask->path).filename().string();
+ std::string fileName = display_name_for_path(sDiscVerificationTask->path);
if (fileName.empty()) {
fileName = sDiscVerificationTask->path;
}
@@ -712,6 +719,8 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
return;
}
+ toggle_cursor_if_gyro(false);
+
mDoAud_seStartMenu(kSoundPlay);
show_menu_notification();
diff --git a/src/dusk/ui/preset.cpp b/src/dusk/ui/preset.cpp
index 6b6bdc2248..1d559972f5 100644
--- a/src/dusk/ui/preset.cpp
+++ b/src/dusk/ui/preset.cpp
@@ -14,7 +14,8 @@ void applyPresetClassic() {
auto& s = getSettings();
s.video.lockAspectRatio.setValue(true);
s.game.bloomMode.setValue(BloomMode::Classic);
- s.game.enableAchievementNotifications.setValue(false);
+ s.game.enableAchievementToasts.setValue(false);
+ s.game.enableControllerToasts.setValue(false);
s.game.internalResolutionScale.setValue(1);
s.game.shadowResolutionMultiplier.setValue(1);
s.game.hideTvSettingsScreen.setValue(false);
@@ -33,7 +34,8 @@ void applyPresetDusk() {
s.game.biggerWallets.setValue(true);
s.game.invertCameraXAxis.setValue(true);
s.game.no2ndFishForCat.setValue(true);
- s.game.enableAchievementNotifications.setValue(true);
+ s.game.enableAchievementToasts.setValue(true);
+ s.game.enableControllerToasts.setValue(true);
s.game.enableQuickTransform.setValue(true);
s.game.instantSaves.setValue(true);
s.game.midnasLamentNonStop.setValue(true);
diff --git a/src/dusk/ui/settings.cpp b/src/dusk/ui/settings.cpp
index d381aecd31..c3a5f8de4b 100644
--- a/src/dusk/ui/settings.cpp
+++ b/src/dusk/ui/settings.cpp
@@ -6,12 +6,15 @@
#include "dusk/audio/DuskAudioSystem.h"
#include "dusk/audio/DuskDsp.hpp"
#include "dusk/config.hpp"
+#include "dusk/file_select.hpp"
#include "dusk/imgui/ImGuiEngine.hpp"
#include "dusk/livesplit.h"
+#include "dusk/main.h"
#include "graphics_tuner.hpp"
#include "m_Do/m_Do_main.h"
#include "menu_bar.hpp"
#include "number_button.hpp"
+#include "menu_bar.hpp"
#include "pane.hpp"
#include "prelaunch.hpp"
#include "ui.hpp"
@@ -41,6 +44,11 @@ constexpr std::array kFpsOverlayCornerNames = {
"Bottom Right",
};
+constexpr std::array kGyroInputModeLabels = {
+ "Sensor",
+ "Mouse",
+};
+
bool try_parse_backend(std::string_view backend, AuroraBackend& outBackend) {
if (backend == "auto") {
outBackend = BACKEND_AUTO;
@@ -199,7 +207,9 @@ int float_setting_percent(ConfigVar& var) {
}
bool gyro_enabled() {
- return getSettings().game.enableGyroAim || getSettings().game.enableGyroRollgoal;
+ return getSettings().game.enableGyroAim ||
+ (getSettings().game.enableGyroRollgoal &&
+ getSettings().game.gyroMode.getValue() != GyroMode::Mouse);
}
struct ConfigBoolProps {
@@ -265,7 +275,7 @@ SelectButton& config_percent_select(Pane& leftPane, Pane& rightPane, ConfigVar
void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, ConfigVar& var,
- const GraphicsTunerProps& props) {
+ const GraphicsTunerProps& props, bool prelaunch) {
leftPane.register_control(
leftPane
.add_select_button({
@@ -283,10 +293,10 @@ void graphics_tuner_control(Window& window, Pane& leftPane, Pane& rightPane, Con
.isModified = [&var] { return var.getValue() != var.getDefaultValue(); },
.submit = false,
})
- .on_nav_command([&window, props](Rml::Event&, NavCommand cmd) {
+ .on_nav_command([&window, props, prelaunch](Rml::Event&, NavCommand cmd) {
if (cmd == NavCommand::Confirm || cmd == NavCommand::Left ||
cmd == NavCommand::Right) {
- window.push(std::make_unique(props));
+ window.push(std::make_unique(props, prelaunch));
return true;
}
return false;
@@ -317,7 +327,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
if (path.empty()) {
display = "(none)";
} else {
- display = std::filesystem::path(path).filename().string();
+ display = display_name_for_path(path);
if (display.empty()) {
display = path;
}
@@ -574,7 +584,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
.valueMin = 0,
.valueMax = 12,
.defaultValue = 0,
- });
+ }, mPrelaunch);
graphics_tuner_control(*this, leftPane, rightPane,
getSettings().game.shadowResolutionMultiplier,
GraphicsTunerProps{
@@ -584,7 +594,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
.valueMin = 1,
.valueMax = 8,
.defaultValue = 1,
- });
+ }, mPrelaunch);
leftPane.add_section("Post-Processing");
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMode,
@@ -595,7 +605,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
.valueMin = static_cast(BloomMode::Off),
.valueMax = static_cast(BloomMode::Dusk),
.defaultValue = static_cast(BloomMode::Classic),
- });
+ }, mPrelaunch);
graphics_tuner_control(*this, leftPane, rightPane, getSettings().game.bloomMultiplier,
GraphicsTunerProps{
.option = GraphicsOption::BloomMultiplier,
@@ -604,7 +614,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
.valueMin = 0,
.valueMax = 100,
.defaultValue = 100,
- });
+ }, mPrelaunch);
leftPane.add_section("Rendering");
config_bool_select(leftPane, rightPane, getSettings().game.enableFrameInterpolation,
@@ -644,6 +654,12 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
pane.clear();
pane.add_text("Open controller 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.",
+ .onChange = [](bool value) { aurora_set_background_input(value); },
+ });
leftPane.add_section("Camera");
addOption("Free Camera", getSettings().game.freeCamera,
@@ -659,12 +675,50 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
[] { return !getSettings().game.freeCamera; });
leftPane.add_section("Gyro");
+ leftPane.register_control(
+ leftPane.add_select_button({
+ .key = "Gyro Input Method",
+ .getValue =
+ [] {
+ const auto mode = getSettings().game.gyroMode.getValue();
+ const auto idx = static_cast(mode);
+ return Rml::String{kGyroInputModeLabels[idx]};
+ },
+ .isModified =
+ [] {
+ return getSettings().game.gyroMode.getValue() !=
+ getSettings().game.gyroMode.getDefaultValue();
+ },
+ }),
+ rightPane, [](Pane& pane) {
+ for (size_t i = 0; i < kGyroInputModeLabels.size(); i++) {
+ pane
+ .add_button({
+ .text = Rml::String{kGyroInputModeLabels[i]},
+ .isSelected =
+ [i] {
+ return getSettings().game.gyroMode.getValue() == static_cast(i);
+ },
+ })
+ .on_pressed([i] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ const GyroMode mode = static_cast(i);
+ getSettings().game.gyroMode.setValue(mode);
+ config::Save();
+ });
+ }
+ pane.add_rml(
+ "
Sensor reads motion directly from a supported controller's gyro via SDL.
"
+ "
Mouse treats mouse input as gyro, intended for use with the Steam Deck.
"
+ "
Mouse input cannot currently be used with Gyro Rollgoal.");
+ });
addOption("Gyro Aim", getSettings().game.enableGyroAim,
"Enables gyro controls while in look mode, aiming a hawk, and aiming "
"supported items.
Supported items include the Slingshot, Gale Boomerang, "
"Hero's Bow, Clawshot(s), Ball and Chain, and Dominion Rod.");
addOption("Gyro Rollgoal", getSettings().game.enableGyroRollgoal,
- "Enables gyro controls for Rollgoal in Hena's Cabin.");
+ "Enables gyro controls for Rollgoal in Hena's Cabin.",
+ [] { return getSettings().game.gyroMode.getValue() == GyroMode::Mouse; });
config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityY,
"Gyro Pitch Sensitivity", "Controls vertical gyro aiming sensitivity.", 25, 400, 5,
[] { return !gyro_enabled(); });
@@ -673,7 +727,11 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
[] { return !gyro_enabled(); });
config_percent_select(leftPane, rightPane, getSettings().game.gyroSensitivityRollgoal,
"Rollgoal Sensitivity", "Controls how strongly gyro input tilts the Rollgoal table.",
- 25, 400, 5, [] { return !getSettings().game.enableGyroRollgoal; });
+ 25, 400, 5,
+ [] {
+ return !getSettings().game.enableGyroRollgoal ||
+ getSettings().game.gyroMode.getValue() == GyroMode::Mouse;
+ });
config_percent_select(leftPane, rightPane, getSettings().game.gyroDeadband, "Gyro Deadband",
"Ignores small gyro movement to reduce drift and jitter.", 0, 50, 1,
[] { return !gyro_enabled(); });
@@ -921,10 +979,81 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
auto& rightPane = add_child(content, Pane::Type::Uncontrolled);
leftPane.add_section("Dusk");
- config_bool_select(leftPane, rightPane, getSettings().game.enableAchievementNotifications,
- {
- .key = "Achievement Notifications",
- .helpText = "Display a toast when an achievement is unlocked.",
+#if DUSK_CAN_OPEN_DATA_FOLDER
+ leftPane.register_control(
+ leftPane.add_button("Open Data Folder").on_pressed([] {
+ mDoAud_seStartMenu(kSoundClick);
+ dusk::OpenDataFolder();
+ }),
+ rightPane, [](Pane& pane) {
+ pane.add_text(
+ "Open the folder where Dusk stores settings, saves, logs, texture "
+ "replacements, and other app data.");
+ });
+#endif
+ leftPane.register_control(
+ leftPane.add_select_button({
+ .key = "Notifications",
+ .getValue = [] {
+ const bool ach = getSettings().game.enableAchievementToasts.getValue();
+ const bool ctl = getSettings().game.enableControllerToasts.getValue();
+ if (!ach && !ctl) {
+ return Rml::String{"Off"};
+ }
+ if (ach && ctl) {
+ return Rml::String{"All"};
+ }
+ return Rml::String{"Some"};
+ },
+ .isModified = [] {
+ const auto& ach = getSettings().game.enableAchievementToasts;
+ const auto& ctl = getSettings().game.enableControllerToasts;
+ return ach.getValue() != ach.getDefaultValue() || ctl.getValue() != ctl.getDefaultValue();
+ },
+ }),
+ rightPane, [](Pane& pane) {
+ pane.clear();
+ pane.add_button("Select All").on_pressed([] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ getSettings().game.enableAchievementToasts.setValue(true);
+ getSettings().game.enableControllerToasts.setValue(true);
+ config::Save();
+ });
+ pane.add_button("Select None").on_pressed([] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ getSettings().game.enableAchievementToasts.setValue(false);
+ getSettings().game.enableControllerToasts.setValue(false);
+ config::Save();
+ });
+
+ pane.add_section("Types");
+ pane.add_button(
+ {
+ .text = "Achievements",
+ .isSelected =
+ [] {
+ return getSettings().game.enableAchievementToasts.getValue();
+ },
+ })
+ .on_pressed([] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ auto& v = getSettings().game.enableAchievementToasts;
+ v.setValue(!v.getValue());
+ config::Save();
+ });
+ pane.add_button(
+ {
+ .text = "Controller",
+ .isSelected =
+ [] { return getSettings().game.enableControllerToasts.getValue(); },
+ })
+ .on_pressed([] {
+ mDoAud_seStartMenu(kSoundItemChange);
+ auto& v = getSettings().game.enableControllerToasts;
+ v.setValue(!v.getValue());
+ config::Save();
+ });
+ pane.add_rml("
Choose which notifications can be displayed.");
});
#if DUSK_ENABLE_SENTRY_NATIVE
config_bool_select(leftPane, rightPane, getSettings().backend.enableCrashReporting,
@@ -955,6 +1084,12 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
.helpText = "Checks GitHub releases for a new Dusk version on startup.
"
"No personal information is transmitted or collected.",
});
+ config_bool_select(leftPane, rightPane, getSettings().game.pauseOnFocusLost,
+ {
+ .key = "Pause On Focus Lost",
+ .helpText = "Pause the game when window focus is lost.",
+ .onChange = [](bool value) { aurora_set_pause_on_focus_lost(value); },
+ });
config_bool_select(leftPane, rightPane, getSettings().backend.enableAdvancedSettings,
{
.key = "Enable Advanced Settings",
diff --git a/src/dusk/ui/string_button.cpp b/src/dusk/ui/string_button.cpp
index a267e5b0e0..7210e87803 100644
--- a/src/dusk/ui/string_button.cpp
+++ b/src/dusk/ui/string_button.cpp
@@ -24,7 +24,7 @@ void BaseStringButton::update() {
}
void BaseStringButton::start_editing() {
- if (mInputElem != nullptr) {
+ if (is_editing()) {
return;
}
@@ -79,14 +79,14 @@ void BaseStringButton::request_stop_editing(bool commit, bool refocusRoot) {
bool BaseStringButton::handle_nav_command(NavCommand cmd) {
if (cmd == NavCommand::Confirm) {
- if (mInputElem == nullptr) {
+ if (!is_editing()) {
start_editing();
} else {
request_stop_editing(true, true);
}
return true;
} else if (cmd == NavCommand::Cancel) {
- if (mInputElem != nullptr) {
+ if (is_editing()) {
request_stop_editing(false, true);
return true;
}
@@ -95,7 +95,7 @@ bool BaseStringButton::handle_nav_command(NavCommand cmd) {
}
void BaseStringButton::focus_input() {
- if (mInputElem == nullptr) {
+ if (!is_editing()) {
return;
}
@@ -111,7 +111,7 @@ void BaseStringButton::focus_input() {
void BaseStringButton::stop_editing(bool commit, bool refocusRoot) {
mPendingStopEditing = false;
mPendingInputFocusFrames = 0;
- if (mInputElem == nullptr) {
+ if (!is_editing()) {
return;
}
if (commit) {
diff --git a/src/dusk/ui/string_button.hpp b/src/dusk/ui/string_button.hpp
index d84b977f37..94f2cebcbe 100644
--- a/src/dusk/ui/string_button.hpp
+++ b/src/dusk/ui/string_button.hpp
@@ -20,6 +20,7 @@ public:
void request_stop_editing(bool commit, bool refocusRoot);
protected:
+ bool is_editing() { return mInputElem != nullptr; }
bool handle_nav_command(NavCommand cmd) override;
virtual void set_value(Rml::String value) = 0;
virtual Rml::String input_value() { return format_value(); }
diff --git a/src/dusk/ui/ui.cpp b/src/dusk/ui/ui.cpp
index a646527ee5..5ef15a6c41 100644
--- a/src/dusk/ui/ui.cpp
+++ b/src/dusk/ui/ui.cpp
@@ -11,6 +11,7 @@
#include
#include "aurora/lib/window.hpp"
+#include "dusk/io.hpp"
#include "input.hpp"
#include "prelaunch.hpp"
#include "window.hpp"
@@ -19,7 +20,7 @@ namespace dusk::ui {
namespace {
void load_font(const char* filename, bool fallback = false) {
- Rml::LoadFontFace(resource_path(filename).string(), fallback);
+ Rml::LoadFontFace(io::fs_path_to_string(resource_path(filename)), fallback);
}
bool sInitialized = false;
@@ -125,43 +126,47 @@ void handle_event(const SDL_Event& event) noexcept {
if (event.type == SDL_EVENT_GAMEPAD_ADDED) {
auto* gamepad = SDL_GetGamepadFromID(event.gdevice.which);
if (SDL_GamepadConnected(gamepad)) {
- const char* name = SDL_GetGamepadName(gamepad);
- Rml::String content = fmt::format("{}", name ? name : "[Unknown]");
- Rml::String title = "Controller connected";
- if (const char* icon = connection_state_icon(SDL_GetGamepadConnectionState(gamepad))) {
- title = fmt::format(
- "{} {};
", title,
- icon);
- }
- int batteryLevel = -1;
- const auto powerState = SDL_GetGamepadPowerInfo(gamepad, &batteryLevel);
- if (powerState != SDL_POWERSTATE_UNKNOWN) {
- content = fmt::format(
- "{}
{};",
- content, battery_icon(powerState, batteryLevel));
- if (batteryLevel > -1) {
- content = fmt::format("{} {}%", content, batteryLevel);
+ if (getSettings().game.enableControllerToasts) {
+ const char* name = SDL_GetGamepadName(gamepad);
+ Rml::String content = fmt::format("{}", name ? name : "[Unknown]");
+ Rml::String title = "Controller connected";
+ if (const char* icon = connection_state_icon(SDL_GetGamepadConnectionState(gamepad))) {
+ title = fmt::format(
+ "{} {};
", title,
+ icon);
}
- content += "
";
+ int batteryLevel = -1;
+ const auto powerState = SDL_GetGamepadPowerInfo(gamepad, &batteryLevel);
+ if (powerState != SDL_POWERSTATE_UNKNOWN) {
+ content = fmt::format(
+ "{}
{};",
+ content, battery_icon(powerState, batteryLevel));
+ if (batteryLevel > -1) {
+ content = fmt::format("{} {}%", content, batteryLevel);
+ }
+ content += "
";
+ }
+ push_toast({
+ .type = "controller",
+ .title = title,
+ .content = content,
+ .duration = std::chrono::seconds(4),
+ });
}
- push_toast({
- .type = "controller",
- .title = title,
- .content = content,
- .duration = std::chrono::seconds(4),
- });
sConnectedGamepads.insert(event.gdevice.which);
}
} else if (event.type == SDL_EVENT_GAMEPAD_REMOVED &&
sConnectedGamepads.contains(event.gdevice.which))
{
- const char* name = SDL_GetGamepadNameForID(event.gdevice.which);
- push_toast({
- .type = "controller",
- .title = "Controller disconnected",
- .content = name ? name : "[Unknown]",
- .duration = std::chrono::seconds(4),
- });
+ if (getSettings().game.enableControllerToasts) {
+ const char* name = SDL_GetGamepadNameForID(event.gdevice.which);
+ push_toast({
+ .type = "controller",
+ .title = "Controller disconnected",
+ .content = name ? name : "[Unknown]",
+ .duration = std::chrono::seconds(4),
+ });
+ }
sConnectedGamepads.erase(event.gdevice.which);
}
input::handle_event(event);
@@ -255,11 +260,7 @@ void update() noexcept {
}
std::filesystem::path resource_path(const std::filesystem::path& filename) noexcept {
- const char* basePath = SDL_GetBasePath();
- if (basePath == nullptr) {
- return std::filesystem::path("res") / filename;
- }
- return std::filesystem::path(basePath) / "res" / filename;
+ return std::filesystem::path("res") / filename;
}
std::string escape(std::string_view str) noexcept {
diff --git a/src/dusk/ui/window.cpp b/src/dusk/ui/window.cpp
index 55ce96c69f..41080ff287 100644
--- a/src/dusk/ui/window.cpp
+++ b/src/dusk/ui/window.cpp
@@ -16,10 +16,7 @@ namespace dusk::ui {
namespace {
float base_body_padding(Rml::Context* context) noexcept {
- if (context == nullptr) {
- return 64.0f;
- }
- const float dpRatio = std::max(context->GetDensityIndependentPixelRatio(), 0.001f);
+ const float dpRatio = context->GetDensityIndependentPixelRatio();
const float heightDp = static_cast(context->GetDimensions().y) / dpRatio;
if (heightDp <= 640.0f) {
return 16.0f * dpRatio;
diff --git a/src/dusk/update_check.cpp b/src/dusk/update_check.cpp
index 11c11795af..c1e332c432 100644
--- a/src/dusk/update_check.cpp
+++ b/src/dusk/update_check.cpp
@@ -5,9 +5,12 @@
#include "nlohmann/json.hpp"
#include "version.h"
+#include
#include
#include
+#include
#include
+#include
namespace dusk::update_check {
namespace {
@@ -20,8 +23,7 @@ struct Version {
int major = 0;
int minor = 0;
int patch = 0;
-
- friend auto operator<=>(const Version&, const Version&) = default;
+ std::vector prerelease;
};
std::string json_string(const json& value, const char* key) {
@@ -57,6 +59,134 @@ bool consume(std::string_view& value, char expected) {
return true;
}
+bool is_digit(char value) {
+ return value >= '0' && value <= '9';
+}
+
+bool is_identifier_char(char value) {
+ return is_digit(value) || (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z') || value == '-';
+}
+
+bool is_numeric_identifier(std::string_view value) {
+ if (value.empty()) {
+ return false;
+ }
+ for (const char c : value) {
+ if (!is_digit(c)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool is_identifier_list(std::string_view value) {
+ if (value.empty()) {
+ return false;
+ }
+
+ bool expectingIdentifier = true;
+ for (const char c : value) {
+ if (c == '.') {
+ if (expectingIdentifier) {
+ return false;
+ }
+ expectingIdentifier = true;
+ continue;
+ }
+ if (!is_identifier_char(c)) {
+ return false;
+ }
+ expectingIdentifier = false;
+ }
+
+ return !expectingIdentifier;
+}
+
+std::string_view trim_git_describe_suffix(std::string_view value) {
+ if (value.ends_with("-dirty")) {
+ value.remove_suffix(6);
+ }
+ if (is_numeric_identifier(value)) {
+ return {};
+ }
+
+ const size_t suffixStart = value.rfind('-');
+ if (suffixStart != std::string_view::npos && value.substr(0, suffixStart).find('.') != std::string_view::npos
+ && is_numeric_identifier(value.substr(suffixStart + 1))) {
+ value.remove_suffix(value.size() - suffixStart);
+ }
+ return value;
+}
+
+void split_identifiers(std::string_view value, std::vector& identifiers) {
+ while (!value.empty()) {
+ const size_t separator = value.find('.');
+ if (separator == std::string_view::npos) {
+ identifiers.push_back(value);
+ return;
+ }
+ identifiers.push_back(value.substr(0, separator));
+ value.remove_prefix(separator + 1);
+ }
+}
+
+std::string_view trim_leading_zeroes(std::string_view value) {
+ while (value.size() > 1 && value.front() == '0') {
+ value.remove_prefix(1);
+ }
+ return value;
+}
+
+int compare_identifier(std::string_view lhs, std::string_view rhs) {
+ const bool lhsNumeric = is_numeric_identifier(lhs);
+ const bool rhsNumeric = is_numeric_identifier(rhs);
+ if (lhsNumeric && rhsNumeric) {
+ lhs = trim_leading_zeroes(lhs);
+ rhs = trim_leading_zeroes(rhs);
+ if (lhs.size() != rhs.size()) {
+ return lhs.size() < rhs.size() ? -1 : 1;
+ }
+ } else if (lhsNumeric != rhsNumeric) {
+ return lhsNumeric ? -1 : 1;
+ }
+
+ const int result = lhs.compare(rhs);
+ if (result < 0) {
+ return -1;
+ }
+ if (result > 0) {
+ return 1;
+ }
+ return 0;
+}
+
+int compare_version(const Version& lhs, const Version& rhs) {
+ if (lhs.major != rhs.major) {
+ return lhs.major < rhs.major ? -1 : 1;
+ }
+ if (lhs.minor != rhs.minor) {
+ return lhs.minor < rhs.minor ? -1 : 1;
+ }
+ if (lhs.patch != rhs.patch) {
+ return lhs.patch < rhs.patch ? -1 : 1;
+ }
+ if (lhs.prerelease.empty() != rhs.prerelease.empty()) {
+ return lhs.prerelease.empty() ? 1 : -1;
+ }
+
+ const size_t commonSize = std::min(lhs.prerelease.size(), rhs.prerelease.size());
+ for (size_t i = 0; i < commonSize; ++i) {
+ const int result = compare_identifier(lhs.prerelease[i], rhs.prerelease[i]);
+ if (result != 0) {
+ return result;
+ }
+ }
+ if (lhs.prerelease.size() != rhs.prerelease.size()) {
+ return lhs.prerelease.size() < rhs.prerelease.size() ? -1 : 1;
+ }
+ return 0;
+}
+
std::optional parse_version(std::string_view value) {
if (!value.empty() && value.front() == 'v') {
value.remove_prefix(1);
@@ -75,13 +205,38 @@ std::optional parse_version(std::string_view value) {
if (!patch) {
return std::nullopt;
}
- if (!value.empty() && value.front() != '-' && value.front() != '+') {
- return std::nullopt;
- }
version.major = *major;
version.minor = *minor;
version.patch = *patch;
+
+ if (value.empty()) {
+ return version;
+ }
+ if (value.front() == '+') {
+ value.remove_prefix(1);
+ if (!is_identifier_list(value)) {
+ return std::nullopt;
+ }
+ return version;
+ }
+ if (!consume(value, '-')) {
+ return std::nullopt;
+ }
+
+ const size_t buildStart = value.find('+');
+ std::string_view prerelease = value.substr(0, buildStart);
+ if (!is_identifier_list(prerelease)) {
+ return std::nullopt;
+ }
+ if (buildStart != std::string_view::npos && !is_identifier_list(value.substr(buildStart + 1))) {
+ return std::nullopt;
+ }
+
+ prerelease = trim_git_describe_suffix(prerelease);
+ if (!prerelease.empty()) {
+ split_identifiers(prerelease, version.prerelease);
+ }
return version;
}
@@ -185,7 +340,7 @@ Result check_latest_github_release(std::string_view owner, std::string_view repo
};
}
- const bool updateAvailable = *latestVersion > *currentVersion;
+ const bool updateAvailable = compare_version(*latestVersion, *currentVersion) > 0;
return {
.status = updateAvailable ? Status::UpdateAvailable : Status::UpToDate,
.message = updateAvailable ? "Update available" : "Dusk is up to date",
diff --git a/src/f_ap/f_ap_game.cpp b/src/f_ap/f_ap_game.cpp
index dc14933b50..55ef94f8a3 100644
--- a/src/f_ap/f_ap_game.cpp
+++ b/src/f_ap/f_ap_game.cpp
@@ -740,12 +740,20 @@ static void fapGm_AfterRecord() {
fapGm_After();
}
+BOOL isRecording = false;
+
static void duskExecute() {
handleGamepadColor();
updateAutoSave();
if (dusk::getSettings().game.recordingMode) {
- Z2GetSeqMgr()->bgmAllMute(0, 0);
+ Z2GetSoundMgr()->getSeqMgr()->getParams()->moveVolume(0.0f, 0);
+ Z2GetSoundMgr()->getStreamMgr()->getParams()->moveVolume(0.0f, 0);
+ isRecording = true;
+ } else if (isRecording) {
+ Z2GetSoundMgr()->getSeqMgr()->getParams()->moveVolume(1.0f, 0);
+ Z2GetSoundMgr()->getStreamMgr()->getParams()->moveVolume(1.0f, 0);
+ isRecording = false;
}
if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigX(PAD_1)) {
diff --git a/src/m_Do/m_Do_graphic.cpp b/src/m_Do/m_Do_graphic.cpp
index 61eaf3bc2d..424f69cf4b 100644
--- a/src/m_Do/m_Do_graphic.cpp
+++ b/src/m_Do/m_Do_graphic.cpp
@@ -1194,8 +1194,13 @@ static void trimming(view_class* param_0, view_port_class* param_1) {
if ((y_orig_pos == 0) && (param_1->scissor.y_orig != param_1->y_orig ||
(param_1->scissor.height != param_1->height)))
{
+ #if TARGET_PC
+ f32 sc_top = param_1->scissor.y_orig;
+ f32 sc_bottom = param_1->scissor.y_orig + param_1->scissor.height;
+ #else
s32 sc_top = (int)param_1->scissor.y_orig;
s32 sc_bottom = param_1->scissor.y_orig + param_1->scissor.height;
+ #endif
GXSetNumChans(1);
GXSetChanCtrl(GX_ALPHA0, GX_FALSE, GX_SRC_REG, GX_SRC_REG, 0, GX_DF_NONE, GX_AF_NONE);
GXSetNumTexGens(0);
@@ -1224,20 +1229,20 @@ static void trimming(view_class* param_0, view_port_class* param_1) {
GXLoadPosMtxImm(cMtx_getIdentity(), 0);
GXClearVtxDesc();
GXSetVtxDesc(GX_VA_POS, GX_DIRECT);
- GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_CLR_RGBA, GX_RGBA4, 0);
+ GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_CLR_RGBA, DUSK_IF_ELSE(GX_F32, GX_RGBA4), 0);
GXSetProjection(ortho, GX_ORTHOGRAPHIC);
GXSetCurrentMtx(0);
GXBegin(GX_QUADS, GX_VTXFMT0, 8);
#if TARGET_PC
- GXPosition3s16(0, 0, -5);
- GXPosition3s16(param_1->width, 0, -5);
- GXPosition3s16(param_1->width, sc_top, -5);
- GXPosition3s16(0, sc_top, -5);
- GXPosition3s16(0, sc_bottom, -5);
- GXPosition3s16(param_1->width, sc_bottom, -5);
- GXPosition3s16(param_1->width, param_1->height, -5);
- GXPosition3s16(0, param_1->height, -5);
+ GXPosition3f32(0, 0, -5);
+ GXPosition3f32(param_1->width, 0, -5);
+ GXPosition3f32(param_1->width, sc_top, -5);
+ GXPosition3f32(0, sc_top, -5);
+ GXPosition3f32(0, sc_bottom, -5);
+ GXPosition3f32(param_1->width, sc_bottom, -5);
+ GXPosition3f32(param_1->width, param_1->height, -5);
+ GXPosition3f32(0, param_1->height, -5);
#else
GXPosition3s16(0, 0, -5);
GXPosition3s16(FB_WIDTH, 0, -5);
diff --git a/src/m_Do/m_Do_main.cpp b/src/m_Do/m_Do_main.cpp
index ad6604e9c7..500a219f17 100644
--- a/src/m_Do/m_Do_main.cpp
+++ b/src/m_Do/m_Do_main.cpp
@@ -71,12 +71,15 @@
#include
#include "SDL3/SDL_filesystem.h"
+#include "SDL3/SDL_iostream.h"
+#include "SDL3/SDL_misc.h"
#include "cxxopts.hpp"
#include "d/actor/d_a_movie_player.h"
#include "dusk/audio/DuskAudioSystem.h"
#include "dusk/audio/DuskDsp.hpp"
#include "dusk/config.hpp"
#include "dusk/settings.h"
+#include "dusk/io.hpp"
#include "dusk/version.hpp"
#include "dusk/discord_presence.hpp"
#if DUSK_TPHD
@@ -87,6 +90,9 @@
#include "f_pc/f_pc_draw.h"
#include "tracy/Tracy.hpp"
#include
+#ifdef __APPLE__
+#include
+#endif
// --- GLOBALS ---
s8 mDoMain::developmentMode = -1;
@@ -109,7 +115,6 @@ const int audioHeapSize = 0x14D800;
bool dusk::IsRunning = true;
bool dusk::IsShuttingDown = false;
bool dusk::IsGameLaunched = false;
-bool dusk::IsFocusPaused = false;
bool dusk::RestartRequested = false;
std::filesystem::path dusk::ConfigPath;
#endif
@@ -119,6 +124,32 @@ void dusk::RequestRestart() noexcept {
IsRunning = false;
}
+bool dusk::OpenDataFolder() {
+#if DUSK_CAN_OPEN_DATA_FOLDER
+ std::error_code ec;
+ std::filesystem::path path = std::filesystem::absolute(ConfigPath, ec);
+ if (ec) {
+ DuskLog.warn("Failed to resolve absolute data folder path '{}': {}",
+ io::fs_path_to_string(ConfigPath), ec.message());
+ path = ConfigPath;
+ }
+
+#if defined(_WIN32)
+ const std::string url = "file:///" + path.generic_string();
+#else
+ const std::string url = "file://" + path.generic_string();
+#endif
+ if (!SDL_OpenURL(url.c_str())) {
+ DuskLog.warn(
+ "Failed to open data folder '{}': {}", io::fs_path_to_string(path), SDL_GetError());
+ return false;
+ }
+ return true;
+#else
+ return false;
+#endif
+}
+
s32 LOAD_COPYDATE(void*) {
char buffer[32];
memset(buffer, 0, sizeof(buffer));
@@ -232,19 +263,16 @@ void main01(void) {
switch (event->type) {
case AURORA_NONE:
goto eventsDone;
+ case AURORA_PAUSED:
+ dusk::audio::SetPaused(true);
+ break;
+ case AURORA_UNPAUSED:
+ dusk::audio::SetPaused(false);
+ dusk::game_clock::reset_frame_timer();
+ break;
case AURORA_SDL_EVENT:
dusk::ui::handle_event(event->sdl);
dusk::g_imguiConsole.HandleSDLEvent(event->sdl);
- if (event->sdl.type == SDL_EVENT_WINDOW_FOCUS_LOST &&
- dusk::getSettings().game.pauseOnFocusLost) {
- dusk::IsFocusPaused = true;
- dusk::audio::SetPaused(true);
- } else if (event->sdl.type == SDL_EVENT_WINDOW_FOCUS_GAINED &&
- dusk::IsFocusPaused) {
- dusk::IsFocusPaused = false;
- dusk::audio::SetPaused(false);
- dusk::game_clock::reset_frame_timer();
- }
break;
case AURORA_DISPLAY_SCALE_CHANGED:
dusk::ImGuiEngine_Initialize(event->windowSize.scale);
@@ -258,19 +286,14 @@ void main01(void) {
eventsDone:;
- if (dusk::IsFocusPaused) {
- std::this_thread::sleep_for(std::chrono::milliseconds(16));
+ if (!aurora_begin_frame()) {
+ DuskLog.debug("aurora_begin_frame returned false, skipping draw this frame");
continue;
}
VIWaitForRetrace();
dusk::lastFrameAuroraStats = *aurora_get_stats();
- if (!aurora_begin_frame()) {
- DuskLog.debug("aurora_begin_frame returned false, skipping draw this frame");
- continue;
- }
-
mDoGph_gInf_c::updateRenderSize();
dusk::ui::update();
@@ -405,13 +428,77 @@ static void ApplyCVarOverrides(const cxxopts::OptionValue& option) {
}
}
-static std::filesystem::path CalculateConfigPath() {
+static void migrate_directory(const std::filesystem::path& from, const std::filesystem::path& to) {
+ std::error_code ec;
+ std::filesystem::create_directories(to, ec);
+ if (ec) {
+ return;
+ }
+
+ for (std::filesystem::recursive_directory_iterator it(
+ from, std::filesystem::directory_options::skip_permission_denied, ec);
+ it != std::filesystem::recursive_directory_iterator(); it.increment(ec))
+ {
+ if (ec) {
+ return;
+ }
+
+ const auto relativePath = std::filesystem::relative(it->path(), from, ec);
+ if (ec) {
+ return;
+ }
+
+ const auto targetPath = to / relativePath;
+ if (it->is_directory(ec)) {
+ std::filesystem::create_directories(targetPath, ec);
+ if (ec) {
+ return;
+ }
+ } else if (it->is_regular_file(ec) && !std::filesystem::exists(targetPath, ec)) {
+ std::filesystem::create_directories(targetPath.parent_path(), ec);
+ if (ec) {
+ return;
+ }
+ std::filesystem::copy_file(
+ it->path(), targetPath, std::filesystem::copy_options::skip_existing, ec);
+ if (ec) {
+ return;
+ }
+ }
+ }
+}
+
+static std::filesystem::path calculate_config_path() {
+#ifdef __APPLE__
+#if TARGET_OS_IOS && !TARGET_OS_TV
+ const char* documentsPath = SDL_GetUserFolder(SDL_FOLDER_DOCUMENTS);
+ if (!documentsPath) {
+ DuskLog.fatal("Unable to get iOS Documents path: {}", SDL_GetError());
+ }
+
+ std::filesystem::path configPath = reinterpret_cast(documentsPath);
+
+ char* oldPrefPath = SDL_GetPrefPath(dusk::OrgName, dusk::AppName);
+ if (oldPrefPath) {
+ const std::filesystem::path oldConfigPath = reinterpret_cast(oldPrefPath);
+ SDL_free(oldPrefPath);
+
+ std::error_code ec;
+ if (oldConfigPath != configPath && std::filesystem::exists(oldConfigPath, ec)) {
+ migrate_directory(oldConfigPath, configPath);
+ }
+ }
+
+ return configPath;
+#endif
+#endif
+
const auto result = SDL_GetPrefPath(dusk::OrgName, dusk::AppName);
if (!result) {
DuskLog.fatal("Unable to get PrefPath: {}", SDL_GetError());
}
- return result;
+ return reinterpret_cast(result);
}
static void EnsureInitialPipelineCache(const std::filesystem::path& configDir) {
@@ -424,16 +511,21 @@ static void EnsureInitialPipelineCache(const std::filesystem::path& configDir) {
return;
}
- const char* basePath = SDL_GetBasePath();
- if (basePath == nullptr) {
- DuskLog.warn("Unable to resolve base path while seeding pipeline cache: {}", SDL_GetError());
- return;
- }
+ std::string sourcePathString;
+ SDL_IOStream* source = nullptr;
- const std::filesystem::path initialPipelineCachePath =
- std::filesystem::path(basePath) / "initial_pipeline_cache.db";
- if (!std::filesystem::exists(initialPipelineCachePath)) {
- DuskLog.info("No bundled initial pipeline cache found at '{}'", initialPipelineCachePath.string());
+ const char* basePath = SDL_GetBasePath();
+ if (basePath != nullptr) {
+ sourcePathString = dusk::io::fs_path_to_string(
+ std::filesystem::path(basePath) / "initial_pipeline_cache.db");
+ source = SDL_IOFromFile(sourcePathString.c_str(), "rb");
+ }
+ if (source == nullptr) {
+ sourcePathString = "initial_pipeline_cache.db";
+ source = SDL_IOFromFile(sourcePathString.c_str(), "rb");
+ }
+ if (source == nullptr) {
+ DuskLog.info("No bundled initial pipeline cache found");
return;
}
@@ -441,18 +533,68 @@ static void EnsureInitialPipelineCache(const std::filesystem::path& configDir) {
std::filesystem::create_directories(configDir, ec);
if (ec) {
DuskLog.warn("Failed to create config directory '{}' for pipeline cache: {}",
- configDir.string(), ec.message());
+ dusk::io::fs_path_to_string(configDir), ec.message());
+ SDL_CloseIO(source);
return;
}
- std::filesystem::copy_file(initialPipelineCachePath, pipelineCachePath, std::filesystem::copy_options::none, ec);
- if (ec) {
- DuskLog.warn("Failed to seed pipeline cache from '{}' to '{}': {}",
- initialPipelineCachePath.string(), pipelineCachePath.string(), ec.message());
+ const auto pipelineCacheString = dusk::io::fs_path_to_string(pipelineCachePath);
+ SDL_IOStream* destination = SDL_IOFromFile(pipelineCacheString.c_str(), "wb");
+ if (destination == nullptr) {
+ DuskLog.warn("Failed to open '{}' for seeded pipeline cache: {}", pipelineCacheString,
+ SDL_GetError());
+ SDL_CloseIO(source);
return;
}
- DuskLog.info("Seeded pipeline cache from '{}'", initialPipelineCachePath.string());
+ bool copied = true;
+ std::array buffer{};
+ while (true) {
+ const size_t bytesRead = SDL_ReadIO(source, buffer.data(), buffer.size());
+ if (bytesRead > 0) {
+ size_t bytesWritten = 0;
+ while (bytesWritten < bytesRead) {
+ const size_t written = SDL_WriteIO(
+ destination, buffer.data() + bytesWritten, bytesRead - bytesWritten);
+ if (written == 0) {
+ DuskLog.warn("Failed to write seeded pipeline cache '{}': {}",
+ pipelineCacheString, SDL_GetError());
+ copied = false;
+ break;
+ }
+ bytesWritten += written;
+ }
+ }
+
+ if (!copied) {
+ break;
+ }
+
+ if (bytesRead < buffer.size()) {
+ if (SDL_GetIOStatus(source) == SDL_IO_STATUS_EOF) {
+ break;
+ }
+
+ DuskLog.warn(
+ "Failed to read bundled pipeline cache '{}': {}", sourcePathString, SDL_GetError());
+ copied = false;
+ break;
+ }
+ }
+
+ if (!SDL_CloseIO(destination)) {
+ DuskLog.warn("Failed to close seeded pipeline cache '{}': {}",
+ dusk::io::fs_path_to_string(pipelineCachePath), SDL_GetError());
+ copied = false;
+ }
+ SDL_CloseIO(source);
+
+ if (!copied) {
+ std::filesystem::remove(pipelineCachePath, ec);
+ return;
+ }
+
+ DuskLog.info("Seeded pipeline cache from '{}'", sourcePathString);
}
static constexpr PADDefaultMapping defaultPadMapping = {
@@ -549,7 +691,7 @@ int game_main(int argc, char* argv[]) {
exit(1);
}
- dusk::ConfigPath = CalculateConfigPath();
+ dusk::ConfigPath = calculate_config_path();
const auto startupLogLevel = static_cast(parsed_arg_options["log-level"].as());
dusk::InitializeFileLogging(dusk::ConfigPath, startupLogLevel);
@@ -557,13 +699,14 @@ int game_main(int argc, char* argv[]) {
ApplyCVarOverrides(parsed_arg_options["cvar"]);
dusk::InitializeCrashReporting();
EnsureInitialPipelineCache(dusk::ConfigPath);
- PADSetDefaultMapping(&defaultPadMapping);
+ // TODO: How to handle this?
+ //PADSetDefaultMapping(&defaultPadMapping, PAD_TYPE_STANDARD);
{
- const auto configPathString = dusk::ConfigPath.string();
+ const auto configPathString = dusk::ConfigPath.u8string();
AuroraConfig config{};
config.appName = dusk::AppName;
- config.configPath = configPathString.c_str();
+ config.configPath = reinterpret_cast(configPathString.c_str());
config.vsync = dusk::getSettings().video.enableVsync;
config.startFullscreen = dusk::getSettings().video.enableFullscreen;
config.windowPosX = -1;
@@ -582,7 +725,8 @@ int game_main(int argc, char* argv[]) {
config.mem1Size = 256 * 1024 * 1024;
#endif
config.mem2Size = 24 * 1024 * 1024;
- config.allowJoystickBackgroundEvents = true;
+ config.allowJoystickBackgroundEvents = dusk::getSettings().game.allowBackgroundInput;
+ config.pauseOnFocusLost = dusk::getSettings().game.pauseOnFocusLost;
config.imGuiInitCallback = &aurora_imgui_init_callback;
config.allowTextureReplacements = true;
config.allowTextureDumps = false;
@@ -629,13 +773,19 @@ int game_main(int argc, char* argv[]) {
// Invalidate a bad saved isoPath so that Dusk can't get blocked from starting up.
// This is only a metadata check; full hash verification is handled by the prelaunch UI.
+ bool forcePreLaunchUI = false;
+ bool saveConfigBeforePrelaunch = false;
+
const std::string p = dusk::getSettings().backend.isoPath;
dusk::iso::DiscInfo discInfo{};
if (!p.empty() &&
dusk::iso::inspect(p.c_str(), discInfo) != dusk::iso::ValidationError::Success)
{
+ DuskLog.warn("Saved DVD image path failed validation, clearing configured path: {}", p);
dusk::getSettings().backend.isoPath.setValue("");
dusk::getSettings().backend.isoVerification.setValue(dusk::DiscVerificationState::Unknown);
+ forcePreLaunchUI = true;
+ saveConfigBeforePrelaunch = true;
}
std::string dvd_path;
@@ -647,6 +797,7 @@ int game_main(int argc, char* argv[]) {
dvd_opened = aurora_dvd_open(dvd_path.c_str());
if (!dvd_opened) {
DuskLog.warn("Failed to open DVD image from command line: {}, opening prelaunch UI", dvd_path);
+ forcePreLaunchUI = true;
} else {
dusk::getSettings().backend.isoPath.setValue(dvd_path);
dusk::getSettings().backend.isoVerification.setValue(
@@ -656,10 +807,27 @@ int game_main(int argc, char* argv[]) {
}
} else {
DuskLog.warn("DVD image from command line failed validation: {}, opening prelaunch UI", dvd_path);
+ forcePreLaunchUI = true;
}
}
+ dusk::iso::log_verification_state(
+ dusk::getSettings().backend.isoPath.getValue(),
+ dusk::getSettings().backend.isoVerification.getValue());
+
if (!dvd_opened) {
+ if (dusk::getSettings().backend.isoPath.getValue().empty()) {
+ forcePreLaunchUI = true;
+ }
+ if (forcePreLaunchUI && dusk::getSettings().backend.skipPreLaunchUI.getValue()) {
+ DuskLog.warn("Prelaunch UI was disabled with no usable DVD image, enabling prelaunch UI");
+ dusk::getSettings().backend.skipPreLaunchUI.setValue(false);
+ saveConfigBeforePrelaunch = true;
+ }
+ if (saveConfigBeforePrelaunch) {
+ dusk::config::Save();
+ }
+
if (!dusk::getSettings().backend.skipPreLaunchUI) {
dusk::ui::push_document(std::make_unique(), true);