Compare commits

...

99 Commits

Author SHA1 Message Date
madeline d204de47bf Merge branch 'main' of https://github.com/TakaRikka/dusk into phong 2026-05-12 20:37:35 -07:00
madeline 23dc0cdbf0 Merge branch 'main' into phong 2026-05-12 19:35:31 -07:00
Luke Street 861efaa053 Update aurora 2026-05-12 20:33:01 -06:00
qwertyquerty 8e41e0195e Merge pull request #1142 from SailorSnoW/fix/lja-achievement-bugfix
Fix LJA achievement triggering from cutscene teleport
2026-05-12 19:00:51 -07:00
qwertyquerty e9359a92d7 Merge branch 'main' into fix/lja-achievement-bugfix 2026-05-12 19:00:08 -07:00
SailorSnoW 0c78376ba8 Fix LJA achievement triggering from cutscene teleport 2026-05-13 03:40:38 +02:00
Loïs 8c001f7968 Add Nix devshell for Linux and macOS development (#1044)
- Restructure flake to expose `devShells.<system>.default` across
  x86_64-linux, aarch64-linux, x86_64-darwin, and aarch64-darwin via
  `nixpkgs.lib.genAttrs`. The existing `packages.x86_64-linux.default`
  build is preserved (still tied to the linux-x86_64 dawn/nod prebuilts).
- Linux devshell (`mkShell`): gcc + clang/lld, cmake, ninja, pkg-config,
  python3 + markupsafe, rustc/cargo, sccache, plus the system libs
  mirrored from the Ubuntu apt list in .github/workflows/build.yml
  (X11/Wayland, Vulkan, GL, ALSA/PulseAudio/PipeWire, GTK3, freetype,
  zstd, ...).
- macOS devshell (`mkShellNoCC`): cmake, ninja, python3 + markupsafe,
  rustc/cargo, sccache. No cc-wrapper so CMake picks up Apple Clang and
  the Xcode SDK directly, matching the build-apple CI job.
- Ignore `.direnv/` and `.envrc` so local direnv state stays out of git.
2026-05-12 19:37:31 -06:00
gymnast86 ef43b94370 Add options for binding custom buttons to specific actions (#1141)
* custom action framework and first person custom action

* add bind for midna call

* custom binding for opening dusklight menu

* turbo speed button action

* text descriptions

* fix not stopping default GC controller menu combo

* more explanation text

* block bind actions when in the dusklight menu
2026-05-12 19:36:07 -06:00
Pieter-Jan Briers 76efa02beb Allow written files to be read by other applications (#1092)
* Allow written files to be read by other applications

Intended for the log file mainly. Hopefully fixes https://github.com/TwilitRealm/dusk/issues/966?

* Consistency
2026-05-12 19:23:49 -06:00
MelonSpeedruns aeeb1ccdd2 Auto Save Protection (#1102)
* Auto Save Protection

* added line behind target_pc define

---------

Co-authored-by: MelonSpeedruns <melonspeedruns@stratobox.net>
Co-authored-by: TakaRikka <38417346+TakaRikka@users.noreply.github.com>
2026-05-12 19:23:28 -06:00
SuperDude88 93c7d0d64d Only Flip Stick Axes (#1132)
Some people asked about this, I think I've come around to their position (follow-up to #870 ).

I checked what TPHD does and its invert setting only changes the sticks
2026-05-12 19:21:28 -06:00
qwertyquerty 1b76b2650c input viewer options in rmlui and reset key option (#1136)
* input viewer options in rmlui

* reset key
2026-05-12 19:18:00 -06:00
Pieter-Jan Briers 45196886b0 Trim trailing newlines off OSReport (#1127) 2026-05-12 19:16:49 -06:00
Pieter-Jan Briers 80af15c95b Log build info on startup (#1117)
Co-authored-by: Luke Street <luke@street.dev>
2026-05-12 19:16:37 -06:00
Sulfrix 4c5e3b933e set sdl app metadata when initializing audio (#1137)
* set sdl app metadata when initializing audio

* move metadata to main
2026-05-12 19:15:33 -06:00
SuperDude88 5eddcb9653 Discord RPC Toggle (#1120)
* Discord RPC Toggle

* I learned my lesson (Formatting)

Took me long enough

* Fix Mobile Platforms

- ifdef the setting so it builds properly on platforms that don't have rpc
- More formatting I missed
2026-05-12 19:14:44 -06:00
tomlube 6dd50c955c fix minor typo (#1134) 2026-05-13 01:21:45 +02:00
TakaRikka 4db65b9845 Merge pull request #1071 from TwilitRealm/better-speedrun-mode
improved speedrun mode
2026-05-12 15:11:24 -07:00
madeline ede6827369 Merge branch 'main' of https://github.com/TakaRikka/dusk into better-speedrun-mode 2026-05-12 14:15:05 -07:00
Luke Street 2c9b20841d Update aurora 2026-05-12 14:00:38 -06:00
Luke Street 2b9ed729a3 Fix file logging on Android 2026-05-12 14:00:38 -06:00
Luke Street f39195c5e0 Fix duplicate "Pause on Focus Lost" option 2026-05-12 14:00:38 -06:00
Luke Street a0f42c0c80 Incorporate SDL_GameControllerDB 2026-05-12 14:00:38 -06:00
Joey Cato d1e9d5af2f Add Fast Roll cheat (#929)
* Add roll fast cheat

* Corrected case on cheat name

* Addressed PR feedback

* Fixed whitespace

* Renamed cheat to be more consistent with other options
2026-05-12 15:56:54 -04:00
Rib 61b2e6ce4d Allow menu navigation via D-Pad (#814) 2026-05-12 11:48:14 -04:00
BoLThompson e73244bca5 add deltatime to darkworld blur size oscillation (#957)
* add deltatime to darkworld blur size oscillation

* updated bloom oscillation to use game_clock
2026-05-12 11:47:14 -04:00
Nathan Mena a4f25ecb28 Fix map offset when toggling mirror mode (#938)
Co-authored-by: Nathan Mena <natemena153+git@gmail.com>
2026-05-12 14:12:38 +02:00
Flash Computer 39b546b81f Don't slow down underwater rolling if the fastIronBoots cheat is on. (#1085)
* Don't slow down underwater rolling if the fastIronBoots cheat is on.

* Removed the comment
2026-05-12 14:11:59 +02:00
madeline 1bd4585994 fix indent 2026-05-11 22:23:31 -07:00
madeline c896bb39ea improved speedrun mode 2026-05-11 22:20:53 -07:00
doop 3366613354 Pillarbox widezoom cutscenes instead of cropping (#1054)
Fixes #777.
2026-05-11 22:59:17 -06:00
Luke Street 79b1f4ab4d Customizable data directory & migration (#1059)
* Customizable data directory & migration

* Add file/dir rename fast-path

* Write data_location.json to base path on Windows; fix UTF-8 custom paths

* Build fix

* Another build fix

* Android data directory selection

* Fix CMake target ref
2026-05-11 22:57:59 -06:00
Irastris 157f4f9df2 Rebrand (#1064)
* Rebrand

* Revert Info.plist.in

* Think, Mark!
2026-05-11 22:06:58 -06:00
SuperDude88 9d5d8dd13a Fix Prelaunch Break-Out With Controller Config (#950)
* Fix Prelaunch Break-Out With Controller Config

Fixes #945

* Formatting
2026-05-11 21:49:24 -06:00
Krutonium b0f1fbee1c Fix Overflow/Off-by-one. Fixes #1036 and #1012 (#1042)
* Fix Overflow/Off-by-one. Fixes #1036 and #1012

* Guard behind TARGET_PC
2026-05-12 01:16:33 +02:00
Howard Luck 40e3f7d057 freedom units (#948) 2026-05-11 12:23:04 +02:00
Luke Street 764fc0b96f Fix "Pause on Focus Lost" preventing startup 2026-05-10 22:29:34 -06:00
SuperDude88 1b4a842eec Invert First Person Aiming (#870)
- Inverts first person aiming on either axis, for both stick and gyro controls

Could be separated from gyro if desirable, but I think it makes the most sense to have it apply to both
2026-05-10 19:00:20 -06:00
gymnast86 08c7261262 fix going into first person with orbital cam (#923) 2026-05-10 19:00:09 -06:00
Luke Street 07b440a1c9 Update aurora 2026-05-10 18:42:31 -06:00
SuperDude88 e7ab978a30 Crash Reporting Popup (#879)
* Initial Draft

- Add draft crash report window on startup

If you want to disable them before/during startup, there is a command line option to force it

* Fixes

- Update language to be more precise, consistent with settings menu
- Actually shut down reporting properly if you disable it
- Fix my silly syntax errors

* Update text & use Sentry consent

---------

Co-authored-by: Luke Street <luke@street.dev>
2026-05-10 18:37:22 -06:00
qwertyquerty 3a02e129e7 Fix keyboard not binding maybe (idk i cant repro) (#901)
* fixkb

* restore comment
2026-05-10 17:35:29 -06:00
qwertyquerty 93c6106770 cherry pick actor spawner (#920) 2026-05-10 17:30:39 -06:00
Luke Street b08d994e32 Update aurora 2026-05-10 17:27:30 -06:00
Luke Street 0665a78c84 Update CMakePresets.json 2026-05-10 13:35:58 -06:00
Luke Street 4db8b51f24 Disable Sentry on Android 2026-05-10 13:22:26 -06:00
Luke Street 800da8dbff Update aurora 2026-05-10 12:48:49 -06:00
Luke Street a0ecdb1735 Re-enable Sentry for release builds 2026-05-10 12:10:32 -06:00
Luke Street c948bffd32 Update aurora 2026-05-10 10:55:30 -06:00
Luke Street 4bcf4ca354 Improve build-appimage.sh 2026-05-10 10:54:43 -06:00
Krutonium bfd9917ca1 Fix Flake to Build Successfully (#791)
* Fix Flake to Build Successfully

* Fix typo; Res Folder is now correctly placed
2026-05-10 10:53:07 -06:00
Pieter-Jan Briers 7f6212f9b7 Add some basic code conventions to the README (#831) 2026-05-10 10:52:47 -06:00
Irastris 80245387f3 Update README & fallback ImGui error message (#857) 2026-05-10 10:52:34 -06:00
Markos Theocharis 0a1fea4bc7 Add LSSupportsGameMode to enable game mode (#859) 2026-05-10 10:51:45 -06:00
project516 4ec7b01213 update github actions in ci (#852) 2026-05-10 10:39:55 -06:00
Luke Street 5187fe90c3 Seed initial pipeline cache through SDL IO & UTF8 path fixes 2026-05-10 10:39:11 -06:00
Pieter-Jan Briers a86fa9c162 State share Unicode fix (#834) 2026-05-10 10:35:11 -06:00
MelonSpeedruns 4ec8c1aaee Fix recording mode muting music until next reboot (#832)
Co-authored-by: MelonSpeedruns <melonspeedruns@stratobox.net>
2026-05-10 17:32:55 +02:00
Luke Street 97d032f8b5 Add Android CI 2026-05-10 00:51:34 -06:00
Luke Street 286532904a Update aurora 2026-05-10 00:31:49 -06:00
Luke Street 04b5861f29 Update CI (again x2) 2026-05-10 00:12:11 -06:00
Luke Street 453e958068 Update CI (again) 2026-05-10 00:11:05 -06:00
Luke Street e7d2fbcc0b Update CI 2026-05-10 00:08:42 -06:00
Krutonium 8f71c70d14 fix io.hpp to enable compiling on GCC15 (#790) 2026-05-09 23:41:15 -06:00
Luke Street df23edcb69 Add iOS UIFileSharingEnabled integration 2026-05-09 21:42:43 -06:00
Luke Street daff157027 Fix change notifications in Android DocumentsProvider 2026-05-09 21:42:32 -06:00
Luke Street 0c23bd4332 Add "Open Data Folder" to Interface menu 2026-05-09 20:57:34 -06:00
Luke Street 7562486449 Log disc verification status 2026-05-09 20:40:59 -06:00
Luke Street 5e08b810fc Update aurora 2026-05-09 20:26:37 -06:00
Pieter-Jan Briers c66cccf660 Fix handling of Unicode paths on Windows (#767)
I love C++
2026-05-09 20:24:50 -06:00
Luke Street 3b1118229b Add Android DocumentsProvider 2026-05-09 20:23:11 -06:00
TakaRikka 491da372a1 Merge pull request #764 from TwilitRealm/gyro-axis-fix
Fix Gyro Sensitivity Axes
2026-05-09 17:47:59 -07:00
SuperDude88 a2f463d146 Fix Gyro Sensitivity Axes
Fixes #759
2026-05-09 20:15:33 -04:00
TakaRikka 63b3ce4849 temp fix for speedrun timer controls 2026-05-09 15:16:54 -07:00
MelonSpeedruns 8280ac00a0 Always disable cursor if Gyro is set to Mouse mode and the menu is not open (#736)
Co-authored-by: MelonSpeedruns <melonspeedruns@stratobox.net>
2026-05-09 15:06:58 -06:00
Luke Street 45ef0d72b1 Fix Android backgrounding & re-hide bars after swipe 2026-05-09 15:03:21 -06:00
SuperDude88 ad9c460ec9 Navigate Carousel Without Focus (#741)
* Navigate Carousel Without Focus

- Allow left/right inputs to change the setting value without having the arrow explicitly clicked

* Formatting
2026-05-09 14:23:59 -06:00
Phillip Stephens 23dc9bc39a Disable setting default mappings for now (#742) 2026-05-09 14:14:45 -06:00
SuperDude88 bf23d44389 Fix Syntax Warning (#743)
- Remove invalid property value
2026-05-09 14:14:36 -06:00
SuperDude88 d0b9b6d10f Fix Prelaunch Break-out (#738)
- Prevent users from breaking out of the prelaunch menu through the GraphicsTuner pages
2026-05-09 13:37:14 -06:00
Luke Street 2f83753260 Fix Android release gradle build 2026-05-09 10:13:59 -06:00
Luke Street eeb0ad77a4 Missed a spot 2026-05-09 10:03:05 -06:00
Luke Street 594cadcf7d Update Android app ID to dev.twilitrealm.dusk 2026-05-09 09:55:26 -06:00
TakaRikka 4290726691 update setup instructions 2026-05-09 07:38:19 -07:00
TakaRikka 80dd5ff278 revert mirror mode message override for now 2026-05-09 07:32:29 -07:00
TakaRikka 08efb9a3cf Merge pull request #735 from TwilitRealm/mute-streams
Recording Mode - Mute streams & Fix 1 note playing
2026-05-09 06:38:33 -07:00
MelonSpeedruns d8a1dd1da4 Recording Mode - Mute streams & Fix 1 note playing 2026-05-09 09:15:10 -04:00
SuperDude88 13dd3c3932 Clamp LOD For Cutscene Midna (#728)
- Sets max eye LOD level to 0 for `demo00_Midna_cut00_FC_tongue` and `demo00_Midna_cut00_BD_tmp`
2026-05-09 06:30:46 -06:00
TakaRikka dd7885da9c Merge pull request #734 from TwilitRealm/number-button-fix
Number Button Fix
2026-05-09 04:40:55 -07:00
TakaRikka 5a05433a2b Merge pull request #733 from TwilitRealm/fix/e_s1_e_yg
Frame interp: Fix e_s1 & e_yg
2026-05-09 04:35:25 -07:00
TakaRikka c3ff3884d7 Merge pull request #730 from TwilitRealm/fix/fchain
Frame interp: Fix obj_fchain
2026-05-09 04:34:34 -07:00
SuperDude88 e42c4d3174 Number Button Fix
- Add `is_editing` helper to BaseStringButton
- Block left/right input from changing number while typing

Resolves #706
2026-05-08 23:19:48 -04:00
Pheenoh 06c77a6818 frame interp: e_s1 & e_yg 2026-05-08 21:16:57 -06:00
Pheenoh 4d4a80891f frame interp: fix obj_fchain 2026-05-08 20:21:37 -06:00
doop 71c892368d Use float vertex positions for trim (#729)
Fixes #726 and looks much smoother.
2026-05-08 21:31:46 -04:00
Irastris d2a1dda523 Add interp callbacks to the stalks of four Baba variants 2026-05-08 21:00:16 -04:00
roeming 78179eb93f comment out flags tab in editor (#727)
Co-authored-by: roeming <roeming@users.noreply.github.com>
2026-05-08 18:56:38 -06:00
Luke Street 34e10d3844 Show "3-finger tap" for menu notif on mobile 2026-05-08 17:55:39 -06:00
madeline 24b468f2a2 phong 2026-04-26 01:08:58 -07:00
150 changed files with 7458 additions and 945 deletions
+86 -26
View File
@@ -8,7 +8,7 @@ on:
pull_request:
env:
# SCCACHE_GHA_ENABLED: "true"
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache"
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
@@ -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,17 +65,16 @@ 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}}
name: dusklight-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}}
path: |
build/install/Dusk-*.AppImage
build/install/Dusklight-*.AppImage
build/install/debug.tar.*
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,14 +133,80 @@ 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}}
name: dusklight-${{env.DUSK_VERSION}}-${{matrix.artifact_name}}
path: |
build/install/Dusk.app
build/install/Dusklight.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 dusklight
- 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: dusklight-${{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,10 +264,9 @@ 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}}
name: dusklight-${{env.DUSK_VERSION}}-win32-msvc-${{matrix.artifact_arch}}
path: |
build/install/*.exe
build/install/*.dll
+4
View File
@@ -41,6 +41,10 @@ compile_commands.json
# MacOS
.DS_Store
# direnv / nix
.direnv/
.envrc
# ISOs
*.iso
+1 -1
View File
@@ -2,7 +2,7 @@
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) Launch Dusk MSVC",
"name": "(gdb) Launch Dusklight MSVC",
"type": "cppvsdbg",
"request": "launch",
"program": "${command:cmake.launchTargetPath}",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"cmake.buildDirectory": "${workspaceFolder}/build/dusk/${buildType}/${variant:tp_version}",
"cmake.buildDirectory": "${workspaceFolder}/build/dusklight/${buildType}/${variant:tp_version}",
"cmake.generator": "Ninja",
"cmake.configureSettings": {
"CMAKE_EXPORT_COMPILE_COMMANDS": "ON"
+70 -60
View File
@@ -67,9 +67,9 @@ endif ()
if(DEFINED ENV{GITHUB_ENV})
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION=${DUSK_WC_DESCRIBE}\n")
endif()
message(STATUS "Dusk version set to ${DUSK_WC_DESCRIBE}")
message(STATUS "Dusklight version set to ${DUSK_WC_DESCRIBE}")
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
project(dusk LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING})
project(dusklight LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING})
if (APPLE)
enable_language(OBJC OBJCXX)
endif ()
@@ -115,22 +115,28 @@ option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking 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)
message(STATUS "dusk: Using system libjpeg-turbo")
message(STATUS "dusklight: Using system libjpeg-turbo")
else ()
message(STATUS "dusk: Fetching libjpeg-turbo")
message(STATUS "dusklight: Fetching libjpeg-turbo")
include(ExternalProject)
set(_jpeg_install_dir ${CMAKE_BINARY_DIR}/libjpeg-turbo-install)
if (WIN32)
@@ -154,6 +160,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
@@ -219,13 +226,13 @@ endif ()
include(FetchContent)
# Declare all dependencies first so CMake can download them in parallel
message(STATUS "dusk: Fetching cxxopts")
message(STATUS "dusklight: Fetching cxxopts")
FetchContent_Declare(cxxopts
URL https://github.com/jarro2783/cxxopts/archive/refs/tags/v3.3.1.tar.gz
URL_HASH SHA256=3bfc70542c521d4b55a46429d808178916a579b28d048bd8c727ee76c39e2072
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
)
message(STATUS "dusk: Fetching nlohmann/json")
message(STATUS "dusklight: Fetching nlohmann/json")
FetchContent_Declare(json
URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz
URL_HASH SHA256=42f6e95cad6ec532fd372391373363b62a14af6d771056dbfc86160e6dfff7aa
@@ -234,7 +241,7 @@ FetchContent_Declare(json
FetchContent_MakeAvailable(cxxopts json)
if (DUSK_ENABLE_SENTRY_NATIVE)
message(STATUS "dusk: Fetching sentry-native")
message(STATUS "dusklight: Fetching sentry-native")
set(SENTRY_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
set(SENTRY_BACKEND crashpad CACHE STRING "" FORCE)
if (WIN32)
@@ -279,15 +286,15 @@ include(files.cmake)
# TODO: version handling for res includes
set(DUSK_BUNDLE_NAME Dusk)
set(DUSK_BUNDLE_NAME Dusklight)
set(DUSK_BUNDLE_IDENTIFIER dev.twilitrealm.dusk)
set(DUSK_COMPANY_NAME "Twilit Realm")
set(DUSK_FILE_DESCRIPTION "Dusk")
set(DUSK_PRODUCT_NAME "Dusk")
set(DUSK_FILE_DESCRIPTION "Dusklight")
set(DUSK_PRODUCT_NAME "Dusklight")
set(DUSK_COPYRIGHT "Copyright (C) Twilit Realm contributors")
source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${REL_FILES})
source_group("dusk" FILES ${DUSK_FILES} ${DUSK_HTTP_BACKEND_FILES})
source_group("dusklight" FILES ${DUSK_FILES} ${DUSK_HTTP_BACKEND_FILES})
set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 MTX_USE_PS=1)
@@ -324,30 +331,30 @@ if (DUSK_ENABLE_UPDATE_CHECKER)
set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/winhttp.cpp)
list(APPEND GAME_LIBS winhttp)
list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_WINHTTP=1)
message(STATUS "dusk: Enabled update checker (WinHTTP)")
message(STATUS "dusklight: 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)")
message(STATUS "dusklight: Enabled update checker (Android)")
elseif (APPLE)
find_library(FOUNDATION_FRAMEWORK Foundation REQUIRED)
set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/url_session.mm)
set_source_files_properties(src/dusk/http/url_session.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
list(APPEND GAME_LIBS ${FOUNDATION_FRAMEWORK})
list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_URLSESSION=1)
message(STATUS "dusk: Enabled update checker (NSURLSession)")
message(STATUS "dusklight: Enabled update checker (NSURLSession)")
elseif (CMAKE_SYSTEM_NAME STREQUAL Linux)
find_package(CURL QUIET OPTIONAL_COMPONENTS HTTPS SSL)
if (CURL_FOUND AND CURL_HTTPS_FOUND AND CURL_SSL_FOUND)
set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/curl.cpp)
list(APPEND GAME_LIBS CURL::libcurl)
list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_LIBCURL=1)
message(STATUS "dusk: Enabled update checker (libcurl)")
message(STATUS "dusklight: Enabled update checker (libcurl)")
else ()
message(STATUS "dusk: Disabled update checker (libcurl + HTTPS/SSL not found)")
message(STATUS "dusklight: Disabled update checker (libcurl + HTTPS/SSL not found)")
endif ()
else ()
message(STATUS "dusk: Disabled update checker (unsupported platform)")
message(STATUS "dusklight: Disabled update checker (unsupported platform)")
endif ()
endif ()
list(APPEND DUSK_FILES ${DUSK_HTTP_BACKEND_SOURCE})
@@ -370,16 +377,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 ()
@@ -435,31 +432,37 @@ endif ()
set(DUSK_FILES src/dusk/main.cpp ${GAME_BASE_FILES} ${GAME_DEBUG_FILES})
if(ANDROID)
add_library(dusk SHARED ${DUSK_FILES})
set_target_properties(dusk PROPERTIES OUTPUT_NAME main)
add_library(dusklight SHARED ${DUSK_FILES})
set_target_properties(dusklight PROPERTIES OUTPUT_NAME main)
else ()
add_executable(dusk ${DUSK_FILES})
add_executable(dusklight ${DUSK_FILES})
endif ()
target_compile_definitions(dusk PRIVATE ${GAME_COMPILE_DEFS})
target_include_directories(dusk PRIVATE ${GAME_INCLUDE_DIRS})
target_link_libraries(dusk PRIVATE aurora::main ${GAME_LIBS} ${JSYSTEM_LINK_LIBRARIES})
target_precompile_headers(dusk PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_SOURCE_DIR}/include/dusk_pch.hpp>")
target_compile_definitions(dusklight PRIVATE ${GAME_COMPILE_DEFS})
target_include_directories(dusklight PRIVATE ${GAME_INCLUDE_DIRS})
target_link_libraries(dusklight PRIVATE aurora::main ${GAME_LIBS} ${JSYSTEM_LINK_LIBRARIES})
target_precompile_headers(dusklight PRIVATE "$<$<COMPILE_LANGUAGE:CXX>:${CMAKE_SOURCE_DIR}/include/dusk_pch.hpp>")
if (TARGET crashpad_handler)
add_dependencies(dusk crashpad_handler)
add_dependencies(dusklight crashpad_handler)
add_custom_command(TARGET dusklight POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:crashpad_handler>"
"$<TARGET_FILE_DIR:dusklight>"
COMMENT "Copying crashpad handler"
)
endif ()
if (ANDROID)
# SDLActivity loads SDL_main via dlsym on Android. Since aurora::main is a static
# archive, force an undefined reference so the linker keeps the SDL_main object.
target_link_options(dusk PRIVATE "-Wl,-u,SDL_main")
target_link_options(dusklight PRIVATE "-Wl,-u,SDL_main")
endif ()
if (NOT APPLE)
add_custom_command(TARGET dusk POST_BUILD
add_custom_command(TARGET dusklight POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${CMAKE_SOURCE_DIR}/res"
"$<TARGET_FILE_DIR:dusk>/res"
"$<TARGET_FILE_DIR:dusklight>/res"
COMMENT "Copying resources"
)
endif ()
@@ -467,9 +470,9 @@ endif ()
if (WIN32)
set(DUSK_WINDOWS_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/windows)
set(DUSK_WINDOWS_ICON_PNG ${CMAKE_CURRENT_SOURCE_DIR}/res/icon.png)
set(DUSK_WINDOWS_ICON_ICO ${CMAKE_CURRENT_BINARY_DIR}/dusk.ico)
set(DUSK_WINDOWS_RC ${CMAKE_CURRENT_BINARY_DIR}/dusk.rc)
set(DUSK_WINDOWS_MANIFEST ${CMAKE_CURRENT_BINARY_DIR}/dusk.manifest)
set(DUSK_WINDOWS_ICON_ICO ${CMAKE_CURRENT_BINARY_DIR}/dusklight.ico)
set(DUSK_WINDOWS_RC ${CMAKE_CURRENT_BINARY_DIR}/dusklight.rc)
set(DUSK_WINDOWS_MANIFEST ${CMAKE_CURRENT_BINARY_DIR}/dusklight.manifest)
add_custom_command(
OUTPUT ${DUSK_WINDOWS_ICON_ICO}
@@ -482,14 +485,14 @@ if (WIN32)
COMMENT "Generating Windows icon"
)
configure_file(${DUSK_WINDOWS_RESOURCE_DIR}/dusk.manifest.in ${DUSK_WINDOWS_MANIFEST} @ONLY)
configure_file(${DUSK_WINDOWS_RESOURCE_DIR}/dusk.rc.in ${DUSK_WINDOWS_RC} @ONLY)
configure_file(${DUSK_WINDOWS_RESOURCE_DIR}/dusklight.manifest.in ${DUSK_WINDOWS_MANIFEST} @ONLY)
configure_file(${DUSK_WINDOWS_RESOURCE_DIR}/dusklight.rc.in ${DUSK_WINDOWS_RC} @ONLY)
target_sources(dusk PRIVATE ${DUSK_WINDOWS_ICON_ICO} ${DUSK_WINDOWS_RC})
set_target_properties(dusk PROPERTIES WIN32_EXECUTABLE TRUE)
target_sources(dusklight PRIVATE ${DUSK_WINDOWS_ICON_ICO} ${DUSK_WINDOWS_RC})
set_target_properties(dusklight PROPERTIES WIN32_EXECUTABLE TRUE)
if (MSVC)
target_link_options(dusk PRIVATE /MANIFEST:NO)
target_link_options(dusklight PRIVATE /MANIFEST:NO)
endif ()
endif ()
@@ -505,10 +508,10 @@ if (APPLE)
file(GLOB_RECURSE DUSK_RESOURCE_FILES
"${DUSK_RESOURCE_DIR}/Assets.car"
"${DUSK_RESOURCE_DIR}/Base.lproj/*"
"${DUSK_RESOURCE_DIR}/Dusk.icns")
"${DUSK_RESOURCE_DIR}/Dusklight.icns")
file(GLOB_RECURSE DUSK_APP_RESOURCE_FILES "${CMAKE_CURRENT_SOURCE_DIR}/res/*")
target_sources(dusk PRIVATE ${DUSK_RESOURCE_FILES})
target_sources(dusk PRIVATE ${DUSK_APP_RESOURCE_FILES})
target_sources(dusklight PRIVATE ${DUSK_RESOURCE_FILES})
target_sources(dusklight PRIVATE ${DUSK_APP_RESOURCE_FILES})
foreach (FILE ${DUSK_RESOURCE_FILES})
file(RELATIVE_PATH NEW_FILE "${DUSK_RESOURCE_DIR}" ${FILE})
get_filename_component(NEW_FILE_PATH ${NEW_FILE} DIRECTORY)
@@ -520,29 +523,36 @@ if (APPLE)
set_property(SOURCE ${FILE} PROPERTY MACOSX_PACKAGE_LOCATION "Resources/${NEW_FILE_PATH}")
endforeach ()
set_target_properties(
dusk PROPERTIES
dusklight PROPERTIES
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_BUNDLE_NAME ${DUSK_BUNDLE_NAME}
MACOSX_BUNDLE_GUI_IDENTIFIER ${DUSK_BUNDLE_IDENTIFIER}
MACOSX_BUNDLE_BUNDLE_VERSION ${DUSK_VERSION_STRING}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${DUSK_SHORT_VERSION_STRING}
MACOSX_BUNDLE_INFO_PLIST ${DUSK_INFO_PLIST}
OUTPUT_NAME Dusk
OUTPUT_NAME Dusklight
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "YES"
XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED "YES"
)
endif ()
if (APPLE AND NOT IOS AND NOT TVOS)
find_library(APPKIT_FRAMEWORK AppKit REQUIRED)
target_sources(dusklight PRIVATE src/dusk/file_select_macos.mm)
set_source_files_properties(src/dusk/file_select_macos.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
target_link_libraries(dusklight PRIVATE ${APPKIT_FRAMEWORK})
endif ()
if (IOS)
find_library(UIKIT_FRAMEWORK UIKit REQUIRED)
find_library(UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK UniformTypeIdentifiers REQUIRED)
target_sources(dusk PRIVATE src/dusk/ios/FileSelectDialog.m)
target_sources(dusklight PRIVATE src/dusk/ios/FileSelectDialog.m)
set_source_files_properties(src/dusk/ios/FileSelectDialog.m PROPERTIES COMPILE_FLAGS -fobjc-arc)
target_link_libraries(dusk PRIVATE ${UIKIT_FRAMEWORK} ${UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK})
target_link_libraries(dusklight PRIVATE ${UIKIT_FRAMEWORK} ${UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK})
endif ()
include(extern/aurora/cmake/AuroraCopyRuntimeDLLs.cmake)
aurora_copy_runtime_dlls(dusk)
aurora_copy_runtime_dlls(dusklight)
if (DUSK_SELECTED_OPT)
if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
@@ -580,13 +590,13 @@ function(get_target_prefix target result_var)
endif ()
endif ()
endfunction()
list(APPEND BINARY_TARGETS dusk)
list(APPEND BINARY_TARGETS dusklight)
set(EXTRA_TARGETS "")
if (TARGET crashpad_handler)
list(APPEND EXTRA_TARGETS crashpad_handler)
endif ()
install(TARGETS ${BINARY_TARGETS} ${EXTRA_TARGETS} DESTINATION ${CMAKE_INSTALL_PREFIX})
aurora_install_runtime_dlls(dusk ${CMAKE_INSTALL_PREFIX})
aurora_install_runtime_dlls(dusklight ${CMAKE_INSTALL_PREFIX})
if (NOT APPLE)
install(DIRECTORY ${CMAKE_SOURCE_DIR}/res DESTINATION ${CMAKE_INSTALL_PREFIX})
endif ()
+40 -5
View File
@@ -30,7 +30,7 @@
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache",
"DUSK_ENABLE_SENTRY_NATIVE": {
"type": "BOOL",
"value": false
"value": true
},
"DUSK_SENTRY_DSN": "$env{SENTRY_DSN}",
"DUSK_SENTRY_ENVIRONMENT": "production"
@@ -352,6 +352,31 @@
"ANDROID_ABI": "x86_64"
}
},
{
"name": "x-android-ci",
"hidden": true,
"inherits": [
"android-base",
"ci"
],
"cacheVariables": {
"DUSK_ENABLE_SENTRY_NATIVE": {
"type": "BOOL",
"value": false
}
}
},
{
"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 +437,7 @@
"x-macos-ci"
],
"cacheVariables": {
"AURORA_DAWN_PROVIDER": "vendor",
"CMAKE_OSX_ARCHITECTURES": "x86_64",
"Rust_CARGO_TARGET": "x86_64-apple-darwin"
}
@@ -525,7 +551,7 @@
"description": "iOS release build with debug info",
"displayName": "iOS RelWithDebInfo",
"targets": [
"dusk"
"dusklight"
]
},
{
@@ -534,7 +560,7 @@
"description": "tvOS release build with debug info",
"displayName": "tvOS RelWithDebInfo",
"targets": [
"dusk"
"dusklight"
]
},
{
@@ -543,7 +569,7 @@
"description": "Android arm64-v8a release build with debug info",
"displayName": "Android arm64-v8a RelWithDebInfo",
"targets": [
"dusk"
"dusklight"
]
},
{
@@ -552,7 +578,16 @@
"description": "Android x86_64 release build with debug info",
"displayName": "Android x86_64 RelWithDebInfo",
"targets": [
"dusk"
"dusklight"
]
},
{
"name": "x-android-ci-arm64",
"configurePreset": "x-android-ci-arm64",
"description": "(Internal) Android CI arm64-v8a",
"displayName": "(Internal) Android CI arm64-v8a",
"targets": [
"dusklight"
]
},
{
+26 -12
View File
@@ -1,51 +1,65 @@
<div align="center">
<img src="res/logo-mascot.png" alt="Logo" width="640">
<img src="res/logo.png" alt="Logo" width="640">
<p align="center">
<a href="https://twilitrealm.dev">Official Website</a>
<a href="https://discord.gg/QACynxeyna">Discord</a>
<a href="https://discord.gg/6NpMhefCK9">Discord</a>
</p>
</div>
# Overview
Dusk is a reverse-engineered reimplementation of Twilight Princess.
Dusklight is a reverse-engineered reimplementation of Twilight Princess.
It aims to be as accurate as possible to the original while also providing new options, enhancements, and tools to customize your experience.
# Setup
> [!IMPORTANT]
> Dusk does *not* provide any copyrighted assets. You must provide your own copy of the original game.
> Dusklight does *not* provide any copyrighted assets. You must provide your own copy of the original game.
> [!IMPORTANT]
> At a minimum, Dusklight requires a GPU with support for either D3D12, Vulkan, or Metal. Your experience with specific hardware, operating systems, and drivers may vary. In particular, older Intel iGPUs have a high likelihood of incompatibility. We are also aware of a number of issues on devices with Adreno GPUs and are working to resolve them.
### 1. Verify your dump
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:
First, make sure your dump of the game is clean and supported by Dusklight. You can do this by checking the SHA-1 hash of your dump against this list of supported versions:
| Version | SHA-1 hash |
|--------------| ------------------------------------------ |
| GameCube USA | `75edd3ddff41f125d1b4ce1a40378f1b565519e7` |
| GameCube EUR | `2601822a488eeb86fb89db16ca8f29c2c953e1ca` |
### 2. Download [Dusk](https://github.com/TwilitRealm/dusk/releases)
*Support for other versions of the game is planned in the future.
### 2. Download [Dusklight](https://github.com/TwilitRealm/dusklight/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.
- Launch Dusklight
- 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 Dusklight APK
- Launch Dusklight
- 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).
If you'd like to build Dusklight 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
Special thanks to the [TP decompilation](https://github.com/zeldaret/tp) team, the GC/Wii decompilation community, the [Aurora](https://github.com/encounter/aurora) developers, the [TP speedrunning community](https://zsrtp.link), and all [contributors](https://github.com/TwilitRealm/dusk/graphs/contributors).
Special thanks to the [TP decompilation](https://github.com/zeldaret/tp) team, the GC/Wii decompilation community, the [Aurora](https://github.com/encounter/aurora) developers, the [TP speedrunning community](https://zsrtp.link), and all [contributors](https://github.com/TwilitRealm/dusklight/graphs/contributors).
<br/>
<div align="center">
+17 -9
View File
@@ -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
cp platforms/freedesktop/dusklight.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
+5 -5
View File
@@ -36,10 +36,10 @@
sudo dnf groupinstall "Development Tools" "Development Libraries"
```
#### Setup
Clone and initialize the Dusk repository
Clone and initialize the Dusklight repository
```sh
git clone --recursive https://github.com/TwilitRealm/dusk.git
cd dusk
git clone --recursive https://github.com/TwilitRealm/dusklight.git
cd dusklight
git pull
git submodule update --init --recursive
```
@@ -93,6 +93,6 @@ Alternate presets available:
#### Running
Pass the disc image as a positional argument. Supported formats: ISO (GCM), RVZ, WIA, WBFS, CISO, GCZ
```sh
build/{preset}/dusk /path/to/game.rvz
build/{preset}/dusklight/path/to/game.rvz
```
If no path is specified, Dusk defaults to `game.iso` in the current working directory.
If no path is specified, Dusklight defaults to `game.iso` in the current working directory.
+13
View File
@@ -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.
+46
View File
@@ -0,0 +1,46 @@
# Installing Dusklight on iOS via AltStore
## Prerequisites
- Mac with Homebrew installed
- iPhone connected via USB
- Dusklight IPA file (download the latest `Dusklight-vX.X.X-ios-arm64.ipa` from the [releases page](https://github.com/TwilitRealm/dusk/releases))
- Game disc - `GZ2E01` (Gamecube USA) or `GZ2PE01` (Gamecube PAL)
## 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
+1 -1
+10 -2
View File
@@ -1411,6 +1411,7 @@ set(DOLPHIN_FILES
)
set(DUSK_FILES
include/dusk/action_bindings.h
include/dusk/endian_gx.hpp
include/dusk/config.hpp
include/dusk/dvd_asset.hpp
@@ -1420,6 +1421,8 @@ set(DUSK_FILES
src/dusk/asserts.cpp
src/dusk/config.cpp
src/dusk/crash_reporting.cpp
src/dusk/data.cpp
src/dusk/data.hpp
src/dusk/endian.cpp
src/dusk/extras.c
src/dusk/file_select.cpp
@@ -1435,6 +1438,7 @@ set(DUSK_FILES
src/dusk/layout.cpp
src/dusk/logging.cpp
src/dusk/settings.cpp
src/dusk/speedrun.cpp
src/dusk/stubs.cpp
src/dusk/update_check.cpp
src/dusk/update_check.hpp
@@ -1444,12 +1448,13 @@ set(DUSK_FILES
src/dusk/imgui/ImGuiConsole.cpp
src/dusk/imgui/ImGuiEngine.cpp
src/dusk/imgui/ImGuiEngine.hpp
src/dusk/imgui/ImGuiMenuGame.cpp
src/dusk/imgui/ImGuiMenuGame.hpp
src/dusk/imgui/ImGuiBloomWindow.cpp
src/dusk/imgui/ImGuiBloomWindow.hpp
src/dusk/imgui/ImGuiEnhancedLightingWindow.cpp
src/dusk/imgui/ImGuiEnhancedLightingWindow.hpp
src/dusk/imgui/ImGuiMenuTools.cpp
src/dusk/imgui/ImGuiMenuTools.hpp
src/dusk/imgui/ImGuiActorSpawner.cpp
src/dusk/imgui/ImGuiProcessOverlay.cpp
src/dusk/imgui/ImGuiCameraOverlay.cpp
src/dusk/imgui/ImGuiHeapOverlay.cpp
@@ -1494,6 +1499,8 @@ set(DUSK_FILES
src/dusk/ui/prelaunch.hpp
src/dusk/ui/preset.cpp
src/dusk/ui/preset.hpp
src/dusk/ui/reporting.cpp
src/dusk/ui/reporting.hpp
src/dusk/ui/select_button.cpp
src/dusk/ui/select_button.hpp
src/dusk/ui/settings.cpp
@@ -1518,6 +1525,7 @@ set(DUSK_FILES
src/dusk/discord.hpp
src/dusk/discord_presence.cpp
src/dusk/version.cpp
src/dusk/action_bindings.cpp
)
set(DUSK_HTTP_BACKEND_FILES
+205 -23
View File
@@ -4,30 +4,212 @@
};
outputs = { self, nixpkgs }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
dusk = pkgs.stdenv.mkDerivation {
name = "dusk";
src = ./.;
nativeBuildInputs = [
pkgs.cmake
pkgs.pkg-config
pkgs.wayland
];
buildInputs = [
pkgs.libGL
pkgs.libX11
pkgs.libXcursor
pkgs.libxi
pkgs.libxcb
pkgs.libxrandr
pkgs.libxscrnsaver
pkgs.libxtst
pkgs.libjpeg8
pkgs.libxkbcommon
pkgs.libglvnd
];
supportedSystems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
pkgsFor = system: import nixpkgs { inherit system; };
# Dependencies that are not packaged in nixpkgs (used by the Linux package build):
buildSources = pkgs: {
aurora-src = pkgs.fetchFromGitHub {
owner = "encounter";
repo = "aurora";
rev = "63606a43265a3bc18dafd500ab4d7a2108f109e6";
hash = "sha256-xBvnAwGwNzav67Ac6oUz7RqDUwqgL2bsME3OOMn8Tqw=";
};
dawn-src = pkgs.fetchzip {
url = "https://github.com/encounter/dawn-build/releases/download/v20260423.175430/dawn-linux-x86_64.tar.gz";
hash = "sha256-HXfKTLHtMPwupnFnaflCARtXVPuS/0PoCePXidjE5xs=";
stripRoot = false;
};
nod-src = pkgs.fetchzip {
url = "https://github.com/encounter/nod/releases/download/v2.0.0-alpha.8/libnod-linux-x86_64.tar.gz";
hash = "sha256-mUqvLsbsqaZ+HAjMmHYPYO+MgtanGRTw7Gzn5uXR5rE=";
stripRoot = false;
};
# The version of imgui on nixpkgs does not map cleanly.
imgui-src = pkgs.fetchFromGitHub {
owner = "ocornut";
repo = "imgui";
rev = "v1.91.9b-docking";
hash = "sha256-mQOJ6jCN+7VopgZ61yzaCnt4R1QLrW7+47xxMhFRHLQ=";
};
sqlite-src = pkgs.fetchzip {
url = "https://sqlite.org/2026/sqlite-amalgamation-3510300.zip";
hash = "sha256-pNMR8zxaaqfAzQ0AQBOXMct4usdjey1Q0Gnitg06UhM=";
};
rmlui-src = pkgs.fetchzip {
url = "https://github.com/mikke89/RmlUi/archive/f9b8c9e2935d5df2c7dff2c190d3968e99b0c3dc.tar.gz";
hash = "sha256-g4O/JZUrrcseOz8o2QJRt+2CeuiLnVeuDJc906xvuIg=";
};
};
# Dusklight Actual (Linux x86_64 only — relies on prebuilt dawn/nod binaries)
mkDusklight = pkgs:
let srcs = buildSources pkgs; in
pkgs.stdenv.mkDerivation {
name = "dusklight";
src = ./.;
postUnpack = ''
mkdir -p $sourceRoot/extern/aurora
cp -r ${srcs.aurora-src}/. $sourceRoot/extern/aurora/
chmod -R u+w $sourceRoot/extern/aurora
sed -i '/add_subdirectory(tests)/d' $sourceRoot/extern/aurora/CMakeLists.txt
'';
# Remove last line to re-enable tests
cmakeFlags = [
"-DFETCHCONTENT_FULLY_DISCONNECTED=ON"
"-DFETCHCONTENT_SOURCE_DIR_CXXOPTS=${pkgs.cxxopts.src}"
"-DFETCHCONTENT_SOURCE_DIR_JSON=${pkgs.nlohmann_json.src}"
"-DFETCHCONTENT_SOURCE_DIR_DAWN_PREBUILT=${srcs.dawn-src}"
"-DFETCHCONTENT_SOURCE_DIR_XXHASH=${pkgs.xxHash.src}"
"-DFETCHCONTENT_SOURCE_DIR_FMT=${pkgs.fmt.src}"
"-DFETCHCONTENT_SOURCE_DIR_TRACY=${pkgs.tracy.src}"
"-DAURORA_SDL3_PROVIDER=system"
"-DFETCHCONTENT_SOURCE_DIR_NOD_PREBUILT=${srcs.nod-src}"
"-DAURORA_NOD_PROVIDER=package"
"-DFETCHCONTENT_SOURCE_DIR_FREETYPE=${pkgs.freetype.src}"
"-DFETCHCONTENT_SOURCE_DIR_ZSTD=${pkgs.zstd.src}"
"-DFETCHCONTENT_SOURCE_DIR_SQLITE3=${srcs.sqlite-src}"
"-DFETCHCONTENT_SOURCE_DIR_IMGUI=${srcs.imgui-src}"
"-DFETCHCONTENT_SOURCE_DIR_RMLUI=${srcs.rmlui-src}"
"-DCMAKE_CROSSCOMPILING=ON" # Tests are not working as I didn't want to work through getting google's test suite working as well. This is the only guard I could find to disable it.
];
installPhase = ''
mkdir -p $out/bin
cp dusklight $out/bin/dusklight
cp -r ./res $out/bin/res
'';
nativeBuildInputs = [
pkgs.cmake
pkgs.pkg-config
pkgs.wayland
];
buildInputs = [
pkgs.libGL
pkgs.libX11
pkgs.libXcursor
pkgs.libxi
pkgs.libxcb
pkgs.libxrandr
pkgs.libxscrnsaver
pkgs.libxtst
pkgs.libjpeg8
pkgs.libxkbcommon
pkgs.libglvnd
pkgs.cxxopts
pkgs.abseil-cpp
pkgs.sdl3
pkgs.fmt
pkgs.tracy
pkgs.freetype
pkgs.zstd
];
};
# Tooling common to every supported host (Linux and macOS).
commonDevTools = pkgs: [
pkgs.cmake
pkgs.ninja
pkgs.pkg-config
pkgs.git
pkgs.python3
pkgs.python3Packages.markupsafe
pkgs.rustc
pkgs.cargo
pkgs.sccache
];
# Linux-only system libraries — mirrors the apt deps from .github/workflows/build.yml
# so the cmake presets resolve the same set of headers as CI.
linuxDevDeps = pkgs: [
# Compilers / linkers
pkgs.clang
pkgs.lld
# C/C++ utilities
pkgs.curl
pkgs.openssl
pkgs.zlib
pkgs.libpng
pkgs.libjpeg_turbo
pkgs.freetype
pkgs.zstd
pkgs.fmt
pkgs.tracy
pkgs.cxxopts
pkgs.abseil-cpp
pkgs.sdl3
pkgs.ncurses
pkgs.libunwind
pkgs.libusb1
pkgs.fuse
# Wayland / display server
pkgs.wayland
pkgs.wayland-protocols
pkgs.libxkbcommon
pkgs.libdecor
# OpenGL / Vulkan
pkgs.libGL
pkgs.libGLU
pkgs.libglvnd
pkgs.vulkan-headers
pkgs.vulkan-loader
# X11
pkgs.libX11
pkgs.libxcb
pkgs.libXcursor
pkgs.libxi
pkgs.libxrandr
pkgs.libxscrnsaver
pkgs.libxtst
pkgs.libxinerama
# Audio
pkgs.alsa-lib
pkgs.libpulseaudio
pkgs.pipewire
# System integration
pkgs.dbus
pkgs.udev
pkgs.gtk3
];
# On macOS we deliberately avoid pulling Nix's cc-wrapper so CMake picks up
# Apple Clang and the Xcode SDK directly, matching the macOS CI workflow.
mkDarwinShell = pkgs:
pkgs.mkShellNoCC {
packages = commonDevTools pkgs;
shellHook = ''
echo "Dusklight dev shell (macOS)"
echo "Requires Xcode Command Line Tools for Apple Clang and the macOS SDK."
echo "Configure: cmake --preset macos-default-relwithdebinfo"
echo "Build: cmake --build --preset macos-default-relwithdebinfo"
'';
};
mkLinuxShell = pkgs:
pkgs.mkShell {
packages = (commonDevTools pkgs) ++ (linuxDevDeps pkgs);
shellHook = ''
echo "Dusklight dev shell (Linux)"
echo "Configure: cmake --preset linux-default-relwithdebinfo"
echo " cmake --preset linux-clang-relwithdebinfo"
echo "Build: cmake --build --preset <preset>"
'';
};
mkDevShell = pkgs:
if pkgs.stdenv.isDarwin
then mkDarwinShell pkgs
else mkLinuxShell pkgs;
in {
packages.x86_64-linux.default = dusk;
packages.x86_64-linux.default = mkDusklight (pkgsFor "x86_64-linux");
devShells = forAllSystems (system: {
default = mkDevShell (pkgsFor system);
});
};
}
+6
View File
@@ -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);
+6
View File
@@ -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);
+9
View File
@@ -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);
+6
View File
@@ -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);
+9
View File
@@ -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);
+6
View File
@@ -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);
+12
View File
@@ -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);
+4
View File
@@ -196,7 +196,11 @@ public:
/* 0x108 */ int mSkipTimer;
/* 0x10C */ int mSkipParameter;
/* 0x110 */ BOOL mIsSkipFade;
#if TARGET_PC
/* 0x114 */ char mSkipEventName[21];
#else
/* 0x114 */ char mSkipEventName[20];
#endif
/* 0x128 */ u8 mCompulsory;
/* 0x129 */ bool mRoomInfoSet;
/* 0x12C */ int mRoomNo;
+3
View File
@@ -223,6 +223,9 @@ private:
/* 0x8F */ u8 field_0x8f;
/* 0x90 */ u8 field_0x90;
/* 0x91 */ u8 field_0x91;
#if TARGET_PC
bool previousMirror;
#endif
}; // Size: 0x94
class dMap_HIO_list_c : public dMpath_HIO_n::hioList_c {
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <unordered_map>
#include "dusk/config_var.hpp"
namespace dusk {
enum class ActionBinds {
FIRST_PERSON_CAMERA,
CALL_MIDNA,
OPEN_DUSKLIGHT_MENU,
TURBO_SPEED_BUTTON,
COUNT,
};
struct ActionBindData {
std::array<config::ActionBindConfigVar, 4>* configVars{};
std::string actionName{};
};
struct ActionBindPressData {
bool pressedCurFrame{false};
bool pressedPrevFrame{false};
};
using ActionBindsMap = std::unordered_map<ActionBinds, ActionBindData>;
ActionBindsMap& getActionBinds();
bool isActionBound(ActionBinds action, u32 port);
void updateActionBindings();
bool getActionBindTrig(ActionBinds action, u32 port);
bool getActionBindHold(ActionBinds action, u32 port);
bool getActionBindHoldAnyPort(ActionBinds action);
int getActionBindButton(ActionBinds action, u32 port);
}
+6 -1
View File
@@ -7,7 +7,12 @@ namespace dusk {
*
* This gets used for file paths and such, and cannot be changed!
*/
constexpr auto AppName = "Dusk";
constexpr auto AppName = "Dusklight";
/**
* Previous AppName to migrate data from.
*/
constexpr auto LegacyAppName = "Dusk";
/**
* \brief The internal organization name for the game.
+1
View File
@@ -13,5 +13,6 @@ void enterAutoSave();
void autoSaving();
void waitingForWrite();
void endAutoSave();
void toggleAutoSave(bool enabled);
#endif
+13
View File
@@ -1,6 +1,7 @@
#ifndef DUSK_CONFIG_HPP
#define DUSK_CONFIG_HPP
#include <functional>
#include <stdexcept>
#include "nlohmann/json.hpp"
#include "config_var.hpp"
@@ -111,6 +112,18 @@ void Save();
*/
ConfigVarBase* GetConfigVar(std::string_view name);
/**
* \brief Resets all custom action bindings for a specific port to nothing
*
* @param port The port to be cleared of action bindings
*/
void ClearAllActionBindings(int port);
/**
* \brief Call a function on every registered CVar.
*/
void EnumerateRegistered(std::function<void(ConfigVarBase&)> callback);
template <ConfigValue T>
const ConfigImplBase* GetConfigImpl() {
static ConfigImpl<T> config;
+48
View File
@@ -48,6 +48,13 @@ enum class ConfigVarLayer : u8 {
* Will not get saved to config.
*/
Override,
/**
* The CVar is temporarily overridden by speedrun mode.
* Will not get saved to config. Cleared when speedrun mode is disabled.
* Lower priority than Override, so launch args still win.
*/
Speedrun,
};
class ConfigImplBase;
@@ -113,6 +120,12 @@ public:
* This is necessary to make it legal to access.
*/
void markRegistered();
/**
* Clear a speedrun-mode override if one is active on this CVar.
* Safe to call on any CVar, no-op if not at the Speedrun layer.
*/
virtual void clearSpeedrunOverride() {}
};
template <typename T>
@@ -189,6 +202,7 @@ public:
case ConfigVarLayer::Value:
return value;
case ConfigVarLayer::Override:
case ConfigVarLayer::Speedrun:
return overrideValue;
default:
abort();
@@ -239,8 +253,42 @@ public:
overrideValue = std::move(newValue);
layer = ConfigVarLayer::Override;
}
/**
* \brief Give a CVar a speedrun-mode override value.
*
* Lower priority than a launch-arg override. Cleared when speedrun mode is disabled.
* The overridden value will not get saved to config.
*
* @param newValue The new value the CVar will get.
*/
void setSpeedrunValue(T newValue) {
checkRegistered();
if (layer != ConfigVarLayer::Override) {
overrideValue = std::move(newValue);
layer = ConfigVarLayer::Speedrun;
}
}
void clearOverride() {
checkRegistered();
if (layer == ConfigVarLayer::Override) {
overrideValue = {};
layer = ConfigVarLayer::Value;
}
}
void clearSpeedrunOverride() override {
checkRegistered();
if (layer == ConfigVarLayer::Speedrun) {
overrideValue = {};
layer = ConfigVarLayer::Value;
}
}
};
using ActionBindConfigVar = ConfigVar<int>;
}
#endif // DUSK_CONFIG_VAR_HPP
+13 -4
View File
@@ -1,8 +1,17 @@
#pragma once
namespace dusk {
namespace dusk::crash_reporting {
void InitializeCrashReporting();
void ShutdownCrashReporting();
enum class Consent {
Unavailable,
Unknown,
Given,
Revoked,
};
} // namespace dusk
void initialize();
void shutdown();
Consent get_consent();
void set_consent(bool enabled);
} // namespace dusk::crash_reporting
+35 -5
View File
@@ -1,6 +1,7 @@
#ifndef DUSK_IO_HPP
#define DUSK_IO_HPP
#include <filesystem>
#include <vector>
// 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<u8> ReadAllBytes(const char* utf8Path);
/**
* \brief Read the byte contents of a file directly into a vector.
*/
static std::vector<u8> 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<const char*>(u8str.c_str())};
}
} // namespace dusk::io
#endif // DUSK_IO_HPP
+12 -14
View File
@@ -1,27 +1,25 @@
#ifndef DUSK_MAIN_H
#define DUSK_MAIN_H
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#include <filesystem>
namespace dusk {
extern bool IsRunning;
extern bool IsShuttingDown;
extern bool IsGameLaunched;
extern bool RestartRequested;
extern std::filesystem::path ConfigPath;
#if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \
extern bool IsRunning;
extern bool IsShuttingDown;
extern bool IsGameLaunched;
extern bool RestartRequested;
extern std::filesystem::path ConfigPath;
#if defined(__ANDROID__) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS) || \
(defined(TARGET_OS_TV) && TARGET_OS_TV)
inline constexpr bool SupportsProcessRestart = false;
inline constexpr bool SupportsProcessRestart = false;
#else
inline constexpr bool SupportsProcessRestart = true;
inline constexpr bool SupportsProcessRestart = true;
#endif
void RequestRestart() noexcept;
}
void RequestRestart() noexcept;
} // namespace dusk
#endif // DUSK_MAIN_H
+26 -1
View File
@@ -1,6 +1,8 @@
#ifndef DUSK_CONFIG_H
#define DUSK_CONFIG_H
#include <array>
#include "dusk/config_var.hpp"
namespace dusk {
@@ -115,8 +117,16 @@ struct UserSettings {
ConfigVar<bool> enableLinkDollRotation;
ConfigVar<bool> enableAchievementToasts;
ConfigVar<bool> enableControllerToasts;
ConfigVar<bool> enableDiscordPresence;
// Graphics
ConfigVar<bool> enhancedLighting;
ConfigVar<bool> enableSpecularLighting;
ConfigVar<bool> enableRimLighting;
ConfigVar<float> specularIntensity;
ConfigVar<float> rimIntensity;
ConfigVar<float> ambientLightMultiplier;
ConfigVar<float> diffuseLightMultiplier;
ConfigVar<BloomMode> bloomMode;
ConfigVar<float> bloomMultiplier;
ConfigVar<bool> disableWaterRefraction;
@@ -125,6 +135,7 @@ struct UserSettings {
ConfigVar<int> shadowResolutionMultiplier;
ConfigVar<bool> enableDepthOfField;
ConfigVar<bool> enableMapBackground;
ConfigVar<bool> disableCutscenePillarboxing;
// Audio
ConfigVar<bool> noLowHpSound;
@@ -144,6 +155,8 @@ struct UserSettings {
ConfigVar<bool> freeCamera;
ConfigVar<bool> invertCameraXAxis;
ConfigVar<bool> invertCameraYAxis;
ConfigVar<bool> invertFirstPersonXAxis;
ConfigVar<bool> invertFirstPersonYAxis;
ConfigVar<float> freeCameraSensitivity;
ConfigVar<bool> debugFlyCam;
ConfigVar<bool> debugFlyCamLockEvents;
@@ -162,6 +175,7 @@ struct UserSettings {
ConfigVar<bool> alwaysGreatspin;
ConfigVar<bool> enableFastIronBoots;
ConfigVar<bool> canTransformAnywhere;
ConfigVar<bool> fastRoll;
ConfigVar<bool> fastSpinner;
ConfigVar<bool> freeMagicArmor;
@@ -170,11 +184,15 @@ struct UserSettings {
// Controls
ConfigVar<bool> enableTurboKeybind;
ConfigVar<bool> enableResetKeybind;
// Tools
ConfigVar<bool> speedrunMode;
ConfigVar<bool> liveSplitEnabled;
ConfigVar<bool> showSpeedrunRTATimer;
ConfigVar<bool> recordingMode;
ConfigVar<bool> showInputViewer;
ConfigVar<bool> showInputViewerGyro;
} game;
struct {
@@ -184,11 +202,18 @@ struct UserSettings {
ConfigVar<bool> skipPreLaunchUI;
ConfigVar<bool> showPipelineCompilation;
ConfigVar<bool> wasPresetChosen;
ConfigVar<bool> enableCrashReporting;
ConfigVar<bool> checkForUpdates;
ConfigVar<int> cardFileType;
ConfigVar<bool> enableAdvancedSettings;
} backend;
// Arrays of size 4 for 4 ports
struct {
std::array<ActionBindConfigVar, 4> firstPersonCamera;
std::array<ActionBindConfigVar, 4> callMidna;
std::array<ActionBindConfigVar, 4> openDusklightMenu;
std::array<ActionBindConfigVar, 4> turboSpeedButton;
} actionBindings;
};
UserSettings& getSettings();
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <aurora/aurora.h>
namespace dusk {
struct SpeedrunInfo {
void startRun() {
m_isRunStarted = true;
m_startTimestamp = OSGetTime();
}
void stopRun() {
m_isRunStarted = false;
m_endTimestamp = OSGetTime() - m_startTimestamp;
}
void reset() {
m_isRunStarted = false;
m_startTimestamp = 0;
m_endTimestamp = 0;
m_isPauseIGT = false;
m_loadStartTimestamp = 0;
m_totalLoadTime = 0;
m_igtTimer = 0;
}
bool m_isRunStarted = false;
OSTime m_startTimestamp = 0;
OSTime m_endTimestamp = 0;
bool m_isPauseIGT = false;
OSTime m_loadStartTimestamp = 0;
OSTime m_totalLoadTime = 0;
OSTime m_igtTimer = 0;
};
extern SpeedrunInfo m_speedrunInfo;
void resetForSpeedrunMode();
} // namespace dusk
+7
View File
@@ -4,6 +4,10 @@
#include <cmath>
#include "os_report.h"
#if TARGET_PC
#include "dusk/action_bindings.h"
#endif
u32 JUTGamePad::CRumble::sChannelMask[4] = {
PAD_CHAN0_BIT,
PAD_CHAN1_BIT,
@@ -85,6 +89,9 @@ u32 JUTGamePad::sRumbleSupported;
u32 JUTGamePad::read() {
sRumbleSupported = PADRead(mPadStatus);
#if TARGET_PC
dusk::updateActionBindings();
#endif
switch (sClampMode) {
case EClampStick:
+3 -4
View File
@@ -1,6 +1,6 @@
# Android Shell
This directory contains a minimal SDLActivity-based Android app wrapper for Dusk.
This directory contains a minimal SDLActivity-based Android app wrapper for Dusklight.
## Prerequisites
@@ -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)
+5 -4
View File
@@ -3,7 +3,7 @@ plugins {
}
def duskRepoDir = rootProject.projectDir.parentFile.parentFile
def duskGeneratedAssetsDir = layout.buildDirectory.dir('generated/assets/dusk').get().asFile
def duskGeneratedAssetsDir = layout.buildDirectory.dir('generated/assets/dusklight').get().asFile
def syncDuskAssets = tasks.register('syncDuskAssets', Sync) {
from(new File(duskRepoDir, 'res')) {
into 'res'
@@ -13,11 +13,11 @@ def syncDuskAssets = tasks.register('syncDuskAssets', Sync) {
}
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
@@ -60,7 +60,8 @@ dependencies {
}
tasks.configureEach { task ->
if (task.name.startsWith('merge') && task.name.endsWith('Assets')) {
if ((task.name.startsWith('merge') && task.name.endsWith('Assets')) ||
task.name.toLowerCase().contains('lint')) {
task.dependsOn(syncDuskAssets)
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
# Keep SDL activity and related JNI bridge methods.
-keep class org.libsdl.app.** { *; }
-keep class com.twilitrealm.dusk.DuskHttpClient { *; }
-keep class com.twilitrealm.dusk.DuskHttpClient$Response { *; }
-keep class dev.twilitrealm.dusk.DuskHttpClient { *; }
-keep class dev.twilitrealm.dusk.DuskHttpClient$Response { *; }
@@ -24,8 +24,19 @@
<meta-data android:name="android.game_mode_config"
android:resource="@xml/game_mode_config" />
<provider
android:name="dev.twilitrealm.dusk.DuskDocumentsProvider"
android:authorities="dev.twilitrealm.dusk.documents"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
<activity
android:name="com.twilitrealm.dusk.DuskActivity"
android:name="dev.twilitrealm.dusk.DuskActivity"
android:alwaysRetainTaskState="true"
android:configChanges="layoutDirection|locale|grammaticalGender|fontScale|fontWeightAdjustment|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
android:exported="true"
@@ -1,15 +1,20 @@
package com.twilitrealm.dusk;
package dev.twilitrealm.dusk;
import android.app.ActionBar;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.OpenableColumns;
import android.util.Log;
import android.view.View;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
@@ -21,6 +26,13 @@ import java.util.List;
public class DuskActivity extends SDLActivity {
private static final String TAG = "DuskActivity";
private static final int FOLDER_DIALOG_REQUEST_CODE = 0x4455;
private static final String EXTERNAL_STORAGE_AUTHORITY =
"com.android.externalstorage.documents";
private long folderDialogUserdata = 0;
private static native void nativeFolderDialogResult(long userdata, String path, String error);
private static String[] splitArgs(String raw) {
List<String> out = new ArrayList<>();
@@ -70,19 +82,42 @@ 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) {
WindowInsetsController ctrl = getWindow().getDecorView().getWindowInsetsController();
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 = getWindow().getDecorView();
// Hide the status bar.
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
} 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();
if (actionBar != null) {
actionBar.hide();
@@ -114,11 +149,6 @@ 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];
}
@@ -128,9 +158,154 @@ public class DuskActivity extends SDLActivity {
if (resultCode == RESULT_OK) {
persistUriPermissions(data);
}
if (requestCode == FOLDER_DIALOG_REQUEST_CODE) {
finishFolderDialog(resultCode, data);
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
public boolean showFolderDialog(long userdata) {
if (userdata == 0 || folderDialogUserdata != 0) {
return false;
}
folderDialogUserdata = userdata;
runOnUiThread(() -> {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
try {
startActivityForResult(intent, FOLDER_DIALOG_REQUEST_CODE);
} catch (ActivityNotFoundException e) {
Log.w(TAG, "Unable to open folder dialog.", e);
finishFolderDialog(Activity.RESULT_CANCELED, null);
}
});
return true;
}
private void finishFolderDialog(int resultCode, Intent data) {
long userdata = folderDialogUserdata;
folderDialogUserdata = 0;
if (userdata == 0) {
return;
}
if (resultCode == RESULT_OK && data != null && data.getData() != null) {
String path = getRealPathForUri(data.getData());
if (path != null && !path.isEmpty()) {
nativeFolderDialogResult(userdata, path, null);
} else {
nativeFolderDialogResult(
userdata, null, "Selected folder is not available as a filesystem path");
}
return;
}
nativeFolderDialogResult(userdata, null, null);
}
private String getRealPathForUri(Uri uri) {
if (uri == null) {
return null;
}
String scheme = uri.getScheme();
if ("file".equals(scheme)) {
return uri.getPath();
}
if (!"content".equals(scheme) ||
!EXTERNAL_STORAGE_AUTHORITY.equals(uri.getAuthority()) ||
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
{
return null;
}
try {
return getExternalStoragePathForDocumentId(getExternalStorageDocumentId(uri));
} catch (IllegalArgumentException e) {
Log.w(TAG, "Unable to resolve URI: " + uri, e);
return null;
}
}
private static String getExternalStorageDocumentId(Uri uri) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && isTreeDocumentUri(uri)) {
return DocumentsContract.getTreeDocumentId(uri);
}
return DocumentsContract.getDocumentId(uri);
}
private static boolean isTreeDocumentUri(Uri uri) {
List<String> segments = uri.getPathSegments();
return segments.size() >= 2 && "tree".equals(segments.get(0));
}
private String getExternalStoragePathForDocumentId(String documentId) {
if (documentId == null || documentId.isEmpty()) {
return null;
}
if (documentId.startsWith("raw:")) {
return documentId.substring("raw:".length());
}
String[] parts = documentId.split(":", 2);
String volumeId = parts[0];
String relativePath = parts.length > 1 ? parts[1] : "";
File root = getExternalStorageRoot(volumeId);
if (root == null) {
return null;
}
return relativePath.isEmpty()
? root.getAbsolutePath()
: new File(root, relativePath).getAbsolutePath();
}
private File getExternalStorageRoot(String volumeId) {
if ("primary".equalsIgnoreCase(volumeId)) {
return Environment.getExternalStorageDirectory();
}
if ("home".equalsIgnoreCase(volumeId)) {
return new File(
Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOCUMENTS);
}
File[] externalFilesDirs = getExternalFilesDirs(null);
if (externalFilesDirs != null) {
for (File externalFilesDir : externalFilesDirs) {
File root = getStorageRootForExternalFilesDir(externalFilesDir);
if (root != null && volumeId.equalsIgnoreCase(root.getName())) {
return root;
}
}
}
File fallback = new File("/storage", volumeId);
return fallback.exists() ? fallback : null;
}
private File getStorageRootForExternalFilesDir(File externalFilesDir) {
if (externalFilesDir == null) {
return null;
}
String path = externalFilesDir.getAbsolutePath();
int androidDir = path.indexOf("/Android/");
if (androidDir <= 0) {
return null;
}
return new File(path.substring(0, androidDir));
}
private void persistUriPermissions(Intent data) {
if (data == null) {
return;
@@ -0,0 +1,467 @@
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 org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
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 LOCATION_DESCRIPTOR_NAME = "data_location.json";
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() {
if (!isCustomDataPathEnabled()) {
ensureUserDirectories();
}
return true;
}
@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
if (isCustomDataPathEnabled()) {
return result;
}
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 {
if (isCustomDataPathEnabled()) {
throw new FileNotFoundException(
"Dusk DocumentsProvider is disabled while a custom data path is configured");
}
final File root = getContext().getFilesDir();
if (root == null) {
throw new FileNotFoundException("Dusklight 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 Dusklight 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 Dusklight 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 boolean isCustomDataPathEnabled() {
if (getContext() == null) {
return false;
}
final File filesDir = getContext().getFilesDir();
if (filesDir == null) {
return false;
}
final File descriptor = new File(filesDir, LOCATION_DESCRIPTOR_NAME);
if (!descriptor.isFile()) {
return false;
}
try {
final JSONObject json = new JSONObject(readText(descriptor));
return "custom".equals(json.optString("mode", "default"));
} catch (IOException | JSONException e) {
return false;
}
}
private static String readText(File file) throws IOException {
try (FileInputStream input = new FileInputStream(file);
ByteArrayOutputStream output = new ByteArrayOutputStream())
{
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
return output.toString(StandardCharsets.UTF_8.name());
}
}
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;
}
}
@@ -1,4 +1,4 @@
package com.twilitrealm.dusk;
package dev.twilitrealm.dusk;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -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();
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">Dusk</string>
<string name="app_name">Dusklight</string>
<string name="documents_provider_root_name">Dusklight Data</string>
<string name="documents_provider_summary">Saves, texture packs, settings, and logs</string>
</resources>
Vendored Regular → Executable
View File
+1 -1
View File
@@ -14,5 +14,5 @@ dependencyResolutionManagement {
}
}
rootProject.name = "dusk-android"
rootProject.name = "dusklight-android"
include ':app'

Before

Width:  |  Height:  |  Size: 928 KiB

After

Width:  |  Height:  |  Size: 928 KiB

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Before

Width:  |  Height:  |  Size: 1014 B

After

Width:  |  Height:  |  Size: 1014 B

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Before

Width:  |  Height:  |  Size: 279 KiB

After

Width:  |  Height:  |  Size: 279 KiB

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 8.7 KiB

-9
View File
@@ -1,9 +0,0 @@
[Desktop Entry]
Name=Dusk
GenericName=Dusk
Comment=The Legend of Zelda: Twilight Princess
Exec=dusk
Icon=dusk
Terminal=false
Type=Application
Categories=Game;
+9
View File
@@ -0,0 +1,9 @@
[Desktop Entry]
Name=Dusklight
GenericName=Dusklight
Comment=PC port of a classic adventure game
Exec=dusklight
Icon=dusklight
Terminal=false
Type=Application
Categories=Game;
+6
View File
@@ -79,5 +79,11 @@
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>LSSupportsGameMode</key>
<true/>
</dict>
</plist>
+3 -1
View File
@@ -13,7 +13,7 @@
<key>CFBundleExecutable</key>
<string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleIconFile</key>
<string>Dusk</string>
<string>Dusklight</string>
<key>CFBundleIconName</key>
<string>Dusk</string>
<key>CFBundleIdentifier</key>
@@ -28,5 +28,7 @@
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>LSSupportsGameMode</key>
<true/>
</dict>
</plist>
+2
View File
@@ -45,5 +45,7 @@
<string>LaunchScreen</string>
<key>UIUserInterfaceStyle</key>
<string>Automatic</string>
<key>LSSupportsGameMode</key>
<true />
</dict>
</plist>
@@ -24,9 +24,9 @@ BEGIN
VALUE "CompanyName", "@DUSK_COMPANY_NAME@\0"
VALUE "FileDescription", "@DUSK_FILE_DESCRIPTION@\0"
VALUE "FileVersion", "@DUSK_VERSION_STRING@\0"
VALUE "InternalName", "dusk\0"
VALUE "InternalName", "dusklight\0"
VALUE "LegalCopyright", "@DUSK_COPYRIGHT@\0"
VALUE "OriginalFilename", "dusk.exe\0"
VALUE "OriginalFilename", "dusklight.exe\0"
VALUE "ProductName", "@DUSK_PRODUCT_NAME@\0"
VALUE "ProductVersion", "@DUSK_VERSION_STRING@\0"
END
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 457 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 340 KiB

After

Width:  |  Height:  |  Size: 642 KiB

+31
View File
@@ -201,6 +201,37 @@ fps {
white-space: nowrap;
}
speedrun-timer {
display: none;
position: absolute;
bottom: 0;
right: 0;
z-index: 99;
background-color: rgba(0, 0, 0, 65%);
padding: 2dp 4dp;
pointer-events: none;
font-family: "Noto Mono";
font-size: 16dp;
color: #ffffff;
white-space: nowrap;
}
speedrun-timer[open] {
display: block;
}
speedrun-rta {
display: none;
}
speedrun-rta[open] {
display: block;
}
speedrun-igt {
display: block;
}
fps[open] {
display: block;
}
+8 -9
View File
@@ -65,10 +65,10 @@ menu {
right: auto;
top: 50%;
transform: translateY(-50%);
/* Scale based on a reference screen width, 428/1216 */
width: 35.230264vw;
/* Scale based on a reference screen width, 856/1216 */
width: 70.394736vw;
min-width: 428dp;
max-width: 856dp;
max-width: 50vw;
height: auto;
display: flex;
flex-direction: column;
@@ -83,9 +83,8 @@ body.mirrored menu {
hero {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
gap: 8dp;
gap: 4dp;
}
body.mirrored hero {
@@ -96,19 +95,19 @@ hero img {
width: 100%;
}
.eyebrow {
eyebrow {
font-family: "Alegreya SC";
font-size: 32dp;
}
@media (min-width: 1216dp) {
.eyebrow {
eyebrow {
/* Same logic as .menu, 32/1216 */
font-size: 2.631579vw;
}
}
.eyebrow span {
eyebrow span {
font-weight: bold;
}
@@ -437,7 +436,7 @@ body.animate-in .intro-item {
decorator: horizontal-gradient(#FEE685FF #FEE68500);
}
.eyebrow {
eyebrow {
display: none;
}
+11 -1
View File
@@ -105,6 +105,12 @@ window content pane:last-of-type > div {
line-height: 1.625;
}
.data-folder-current {
display: block;
font-size: 16dp;
color: rgba(224, 219, 200, 65%);
}
window content pane > spacer {
display: block;
/* Completes the 24dp bottom inset after the pane's 8dp gap. */
@@ -199,6 +205,11 @@ button:not(:disabled):active {
box-shadow: #C2A42D 0 0 0 2dp;
}
button:disabled {
opacity: 0.35;
cursor: default;
}
button.modal-btn {
flex: 1 1 0;
text-align: center;
@@ -498,7 +509,6 @@ progress.verification-progress-bar {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: stretch;
align-items: stretch;
gap: 12dp;
width: 100%;
+55 -7
View File
@@ -51,10 +51,13 @@
#include "d/actor/d_a_ni.h"
#include "d/d_s_play.h"
#if TARGET_PC
#include "dusk/action_bindings.h"
#include "dusk/frame_interpolation.h"
#include "dusk/settings.h"
#include "res/Object/Alink.h"
#include <cstring>
#endif
static int daAlink_Create(fopAc_ac_c* i_this);
static int daAlink_Delete(daAlink_c* i_this);
@@ -9363,6 +9366,12 @@ BOOL daAlink_c::spActionTrigger() {
}
BOOL daAlink_c::midnaTalkTrigger() const {
#if TARGET_PC
// If we have a custom bind for Midna, check that instead
if (dusk::isActionBound(dusk::ActionBinds::CALL_MIDNA, 0)) {
return dusk::getActionBindTrig(dusk::ActionBinds::CALL_MIDNA, 0);
}
#endif
return mItemTrigger & BTN_Z;
}
@@ -16114,6 +16123,9 @@ int daAlink_c::procSlideLand() {
int daAlink_c::procFrontRollInit() {
BOOL is_guard_anime = checkUpperGuardAnime();
#ifdef TARGET_PC
const f32 fastRollMultiplier = dusk::getSettings().game.fastRoll ? 2.0f : 1.0f;
#endif
if (mProcID == PROC_FRONT_ROLL && mDemo.getDemoMode() == daPy_demo_c::DEMO_FRONT_ROLL_e) {
return 0;
@@ -16129,10 +16141,16 @@ int daAlink_c::procFrontRollInit() {
roll_anm_speed = mpHIO->mFrontRoll.m.mRollAnm.mStartFrame;
}
setSingleAnime(ANM_FRONT_ROLL, mpHIO->mFrontRoll.m.mRollAnm.mSpeed, roll_anm_speed,
setSingleAnime(ANM_FRONT_ROLL,
#ifdef TARGET_PC
mpHIO->mFrontRoll.m.mRollAnm.mSpeed * fastRollMultiplier,
#else
mpHIO->mFrontRoll.m.mRollAnm.mSpeed,
#endif
roll_anm_speed,
mpHIO->mFrontRoll.m.mRollAnm.mEndFrame,
mpHIO->mFrontRoll.m.mRollAnm.mInterpolation);
mNormalSpeed = speedF * mpHIO->mFrontRoll.m.mSpeedRate + mpHIO->mFrontRoll.m.mInitSpeed;
f32 max_speed = mpHIO->mFrontRoll.m.mInitSpeed + mpHIO->mMove.m.mMaxSpeed * mpHIO->mFrontRoll.m.mSpeedRate;
@@ -16145,11 +16163,20 @@ int daAlink_c::procFrontRollInit() {
}
if (checkNoResetFlg0(FLG0_WATER_IN_MOVE)) {
mNormalSpeed *= mpHIO->mItem.mIronBoots.m.mWaterVelocityX;
#if TARGET_PC
if (!(dusk::getSettings().game.enableFastIronBoots))
#endif
{
mNormalSpeed *= mpHIO->mItem.mIronBoots.m.mWaterVelocityX;
}
} else if (checkHeavyStateOn(TRUE, TRUE)) {
mNormalSpeed *= mHeavySpeedMultiplier;
}
#ifdef TARGET_PC
mNormalSpeed *= fastRollMultiplier;
#endif
current.angle.y = shape_angle.y;
voiceStart(Z2SE_AL_V_BACKTEN);
mProcVar2.field_0x300c = 0;
@@ -16280,8 +16307,13 @@ int daAlink_c::procFrontRollCrashInit() {
speed.y = mpHIO->mFrontRoll.m.mCrashSpeedV;
if (checkNoResetFlg0(FLG0_WATER_IN_MOVE)) {
mNormalSpeed *= mpHIO->mItem.mIronBoots.m.mWaterVelocityX;
speed.y *= mpHIO->mItem.mIronBoots.m.mWaterVelocityY;
#if TARGET_PC
if (!(dusk::getSettings().game.enableFastIronBoots))
#endif
{
mNormalSpeed *= mpHIO->mItem.mIronBoots.m.mWaterVelocityX;
speed.y *= mpHIO->mItem.mIronBoots.m.mWaterVelocityY;
}
}
ANGLE_ADD_2(current.angle.y, 0x8000);
@@ -16375,6 +16407,9 @@ int daAlink_c::procFrontRollSuccess() {
int daAlink_c::procSideRollInit(int param_0) {
BOOL is_prev_guardAnm = checkUpperGuardAnime();
#ifdef TARGET_PC
const f32 fastRollMultiplier = dusk::getSettings().game.fastRoll ? 2.0f : 1.0f;
#endif
if (!commonProcInitNotSameProc(PROC_SIDE_ROLL)) {
return 0;
@@ -16391,17 +16426,30 @@ int daAlink_c::procSideRollInit(int param_0) {
current.angle.y = shape_angle.y + -0x4000;
}
setSingleAnime(anmID, mpHIO->mGuard.mTurnMove.m.mSideRollAnmSpeed,
setSingleAnime(anmID,
#ifdef TARGET_PC
mpHIO->mGuard.mTurnMove.m.mSideRollAnmSpeed * fastRollMultiplier,
#else
mpHIO->mGuard.mTurnMove.m.mSideRollAnmSpeed,
#endif
mpHIO->mGuard.mTurnMove.m.mTurnAnm.mStartFrame,
mpHIO->mGuard.mTurnMove.m.mTurnAnm.mEndFrame,
mpHIO->mGuard.mTurnMove.m.mTurnAnm.mInterpolation);
mNormalSpeed = mpHIO->mGuard.mTurnMove.m.mSideRollSpeed;
if (checkNoResetFlg0(FLG0_WATER_IN_MOVE)) {
mNormalSpeed *= mpHIO->mItem.mIronBoots.m.mWaterVelocityX;
#if TARGET_PC
if (!(dusk::getSettings().game.enableFastIronBoots))
#endif
{
mNormalSpeed *= mpHIO->mItem.mIronBoots.m.mWaterVelocityX;
}
} else if (checkHeavyStateOn(TRUE, TRUE)) {
mNormalSpeed *= mHeavySpeedMultiplier;
}
#ifdef TARGET_PC
mNormalSpeed *= fastRollMultiplier;
#endif
setFootEffectProcType(0);
field_0x2f9d = 4;
+1
View File
@@ -25,6 +25,7 @@
#include "dusk/imgui/ImGuiConsole.hpp"
#include "dusk/settings.h"
#include "dusk/speedrun.h"
BOOL daAlink_c::checkEventRun() const {
return dComIfGp_event_runCheck() || checkPlayerDemoMode();
+6 -3
View File
@@ -12,6 +12,7 @@
#if TARGET_PC
#include "dusk/gyro.h"
#include "dusk/action_bindings.h"
#endif
bool daAlink_c::checkNoSubjectModeCamera() {
@@ -119,8 +120,8 @@ BOOL daAlink_c::setBodyAngleToCamera() {
var_f31 /= dComIfGp_getCameraZoomScale(field_0x317c);
}
shape_angle.y = shape_angle.y + (var_f31 * cM_ssin(mStickAngle));
sp8 = mBodyAngle.x + (var_f31 * cM_scos(mStickAngle));
shape_angle.y = shape_angle.y + (var_f31 * cM_ssin(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonXAxis ? -1.0f : 1.0f)));
sp8 = mBodyAngle.x + (var_f31 * cM_scos(mStickAngle) IF_DUSK(* (dusk::getSettings().game.invertFirstPersonYAxis ? -1.0f : 1.0f)));
if (checkNotItemSinkLimit() && sp8 > 0 && sp8 > mBodyAngle.x) {
sp8 = mBodyAngle.x;
@@ -192,7 +193,9 @@ BOOL daAlink_c::subjectCancelTrigger() {
BOOL daAlink_c::checkSubjectEnd(BOOL i_isPlaySe) {
setDoStatus(BUTTON_STATUS_BACK);
if (checkEventRun() || checkEquipAnime() || doTrigger() || checkSetItemTrigger(dItemNo_HAWK_EYE_e) || subjectCancelTrigger() || checkEndResetFlg0(ERFLG0_FORCE_SUBJECT_CANCEL) || dComIfGp_checkCameraAttentionStatus(field_0x317c, 0x2000)) {
// Allow pressing the first person binding to also leave first person
if (IF_DUSK(dusk::getActionBindTrig(dusk::ActionBinds::FIRST_PERSON_CAMERA, 0)) ||
checkEventRun() || checkEquipAnime() || doTrigger() || checkSetItemTrigger(dItemNo_HAWK_EYE_e) || subjectCancelTrigger() || checkEndResetFlg0(ERFLG0_FORCE_SUBJECT_CANCEL) || dComIfGp_checkCameraAttentionStatus(field_0x317c, 0x2000)) {
if (i_isPlaySe) {
seStartSystem(Z2SE_SUBJ_VIEW_OUT);
}
+31
View File
@@ -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) {
+31
View File
@@ -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) {
+42
View File
@@ -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 <cstring>
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) {
+31
View File
@@ -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);
+44 -1
View File
@@ -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;
+31
View File
@@ -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) {
+36
View File
@@ -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)) {
+39
View File
@@ -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 <cstring>
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<daObjFchain_c*>(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, &current.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;
}
+5 -1
View File
@@ -11,7 +11,9 @@
#include "d/d_msg_string.h"
#include "dusk/livesplit.h"
#include "dusk/imgui/ImGuiConsole.hpp"
#include "dusk/speedrun.h"
#include "m_Do/m_Do_controller_pad.h"
#include <dusk/autosave.h>
dBrightCheck_c::dBrightCheck_c(JKRArchive* i_archive) {
mArchive = i_archive;
@@ -146,10 +148,12 @@ void dBrightCheck_c::modeMove() {
if (dusk::getSettings().game.speedrunMode && !dusk::getSettings().game.hideTvSettingsScreen) {
// start a new run if a run isn't already in progress
if (!dusk::m_speedrunInfo.m_isRunStarted) {
dusk::ImGuiMenuGame::resetForSpeedrunMode();
dusk::resetForSpeedrunMode();
dusk::m_speedrunInfo.startRun();
}
}
toggleAutoSave(true);
#endif
mCompleteCheck = true;
mMode = MODE_WAIT_e;
+96 -59
View File
@@ -31,6 +31,7 @@
#if TARGET_PC
#include "dusk/frame_interpolation.h"
#include "dusk/logging.h"
#include "dusk/action_bindings.h"
#include "imgui.h"
#endif
@@ -838,6 +839,12 @@ void dCamera_c::updatePad() {
mTrigB = mDoCPd_c::getTrigB(mPadID) ? true : false;
#if TARGET_PC
// If our custom action binding is triggered, and we're not already in first person, go into first person
if (dusk::getActionBindTrig(dusk::ActionBinds::FIRST_PERSON_CAMERA, mPadID) && mGear != -1) {
setComStat(0x1000);
mGear = 0;
}
if (mCamParam.mManualMode) {
return;
}
@@ -877,7 +884,8 @@ void dCamera_c::updatePad() {
if (mPadInfo.mCStick.mLastPosY < -mCamSetup.mCStick.SwTHH()) {
if (mCStickYState != -1) {
if (mGear == -1 && mCurMode == 4) {
// Don't use regular first person trigger if custom mapping is set
if (mGear == -1 && mCurMode == 4 IF_DUSK(&& !dusk::isActionBound(dusk::ActionBinds::FIRST_PERSON_CAMERA, mPadID))) {
mGear = 0;
setComStat(0x2000);
} else if (mGear == 0 && sp6C) {
@@ -888,7 +896,8 @@ void dCamera_c::updatePad() {
mCStickYState = -1;
} else if (mPadInfo.mCStick.mLastPosY > mCamSetup.mCStick.SwTHH()) {
if (mCStickYState != 1) {
if (mGear == 0 && sp6B) {
// Don't use regular first person trigger if custom mapping is set
if (mGear == 0 && sp6B IF_DUSK(&& !dusk::isActionBound(dusk::ActionBinds::FIRST_PERSON_CAMERA, mPadID))) {
setComStat(0x1000);
} else if (mGear == 1) {
mGear = 0;
@@ -7648,7 +7657,11 @@ bool dCamera_c::freeCamera() {
cXyz camMovement = {mPadInfo.mCStick.mLastPosX, mPadInfo.mCStick.mLastPosY, 0.0f};
f32 magnitude = sqrt(mPadInfo.mCStick.mLastPosX * mPadInfo.mCStick.mLastPosX + mPadInfo.mCStick.mLastPosY * mPadInfo.mCStick.mLastPosY);
if (mPadInfo.mCStick.mLastPosX != 0 || mPadInfo.mCStick.mLastPosY != 0) {
// If we aren't in manual cam mode, don't trigger it if the player tries to hit C-up
// for first person unless they have first person bound to a custom binding
if ((dusk::isActionBound(dusk::ActionBinds::FIRST_PERSON_CAMERA, mPadID) && mPadInfo.mCStick.mLastPosY != 0) ||
mPadInfo.mCStick.mLastPosX != 0 || mPadInfo.mCStick.mLastPosY < 0 || (mCamParam.mManualMode == 1 && mPadInfo.mCStick.mLastPosY != 0))
{
mCamParam.mManualMode = 1;
camMovement = camMovement.normalize();
camMovement.y *= dusk::getSettings().game.invertCameraYAxis ? 1.0f : -1.0f;
@@ -11231,6 +11244,62 @@ cXyz dCamera_c::Center() {
return mCenter + mShake.field_0x24;
}
#ifdef TARGET_PC
f32 get_target_trim_height(camera_process_class* i_this) {
const auto camera = &i_this->mCamera;
if (camera->mCurState != 2) {
switch (camera->mTrimSize) {
case 0:
case 4:
return 0.0f;
case 1:
return camera->mCamSetup.VistaTrimHeight();
case 2:
case 3:
return camera->mCamSetup.CinemaScopeTrimHeight();
default:
return camera->mTrimHeight;
}
}
return camera->mTrimHeight;
}
void widezoom_correction(camera_process_class* i_this, float trim_height) {
camera_class* camera = (camera_class*)i_this;
dDlst_window_c* window = get_window(camera);
view_port_class* viewport = window->getViewPort();
auto trim_width = 0.0f;
if (mDoGph_gInf_c::isWideZoom()) {
const auto target_ar = FB_WIDTH_BASE / (FB_HEIGHT_BASE - trim_height * 2.0f);
const auto target_ar_real =
FB_WIDTH_BASE / (FB_HEIGHT_BASE - get_target_trim_height(i_this) * 2.0f);
const auto current_ar = camera->view.aspect;
if (current_ar < target_ar) {
trim_height = FB_HEIGHT_BASE / 2.0f * (1.0f - current_ar / target_ar);
} else {
trim_height = 0.0f;
trim_width = FB_WIDTH_BASE / 2.0f * (1.0f - target_ar_real / current_ar);
}
if (dusk::frame_interp::is_sim_frame()) {
constexpr auto base_ar =
static_cast<f32>(FB_WIDTH_BASE) / static_cast<f32>(FB_HEIGHT_BASE);
const auto ar_corr = base_ar / std::min(current_ar, target_ar_real);
camera->view.fovy =
MTXRadToDeg(2.0f * atanf(tanf(MTXDegToRad(camera->view.fovy) * 0.5f) * ar_corr));
}
}
trim_width *= viewport->width / FB_WIDTH_BASE;
trim_height *= viewport->height / FB_HEIGHT_BASE;
window->setScissor(trim_width, trim_height, viewport->width - trim_width * 2.0f,
viewport->height - trim_height * 2.0f);
}
#endif
static int camera_execute(camera_process_class* i_this) {
preparation(i_this);
@@ -11251,6 +11320,28 @@ static int camera_execute(camera_process_class* i_this) {
store(i_this);
#ifdef TARGET_PC
widezoom_correction(i_this, i_this->mCamera.TrimHeight());
if (dusk::getSettings().game.enableFrameInterpolation) {
dusk::frame_interp::add_interpolation_callback([](bool _, void* pUserWork) {
const auto i_this = static_cast<camera_process_class*>(pUserWork);
const auto camera = &i_this->mCamera;
const auto trim_size = camera->mTrimSize;
if (camera->mCurState != 2 && trim_size >= 0 && trim_size <= 3) {
// derive trim height at previous tick using current camera state
const auto target = get_target_trim_height(i_this);
const auto step = dusk::frame_interp::get_interpolation_step();
const auto cur = camera->TrimHeight();
const auto prev = (4.0f * cur - target) / 3.0f;
const auto trim_height = prev + (cur - prev) * step;
widezoom_correction(i_this, trim_height);
}
}, i_this);
}
// record new camera for our sim frame
dusk::frame_interp::record_camera(i_this, get_camera_id(i_this));
// interpolate the view now so that this sim frame's view matrix matches what
@@ -11262,26 +11353,6 @@ static int camera_execute(camera_process_class* i_this) {
return 1;
}
#ifdef TARGET_PC
void set_ar_corrected_trim(dDlst_window_c* window, float trim_height) {
const auto viewport = window->getViewPort();
if (mDoGph_gInf_c::isWideZoom()) {
const auto target_ar = FB_WIDTH / (FB_HEIGHT - trim_height * 2.0f);
const auto current_ar = mDoGph_gInf_c::m_safeWidthF / mDoGph_gInf_c::m_safeHeightF;
if (current_ar < target_ar) {
trim_height = FB_HEIGHT / 2.0f * (1.0f - current_ar / target_ar);
} else {
trim_height = 0.0f;
}
}
trim_height *= viewport->height / FB_HEIGHT;
window->setScissor(0.0f, trim_height, viewport->width, viewport->height - trim_height * 2.0f);
}
#endif
static int camera_draw(camera_process_class* i_this) {
camera_class* a_this = (camera_class*)i_this;
dCamera_c* body = &i_this->mCamera;
@@ -11334,42 +11405,8 @@ static int camera_draw(camera_process_class* i_this) {
}
#endif
#if TARGET_PC
set_ar_corrected_trim(window, body->TrimHeight());
if (dusk::getSettings().game.enableFrameInterpolation) {
dusk::frame_interp::add_interpolation_callback([](bool _, void* pUserWork) {
const auto i_this = static_cast<camera_process_class*>(pUserWork);
const auto camera = &i_this->mCamera;
const auto trim_size = camera->mTrimSize;
if (camera->mCurState != 2 && trim_size >= 0 && trim_size <= 3) {
// derive trim height at previous tick using current camera state
f32 target;
switch (trim_size) {
case 0:
target = 0.0f;
break;
case 1:
target = camera->mCamSetup.VistaTrimHeight();
break;
case 2:
case 3:
target = camera->mCamSetup.CinemaScopeTrimHeight();
break;
}
const auto step = dusk::frame_interp::get_interpolation_step();
const auto cur = camera->TrimHeight();
const auto prev = (4.0f * cur - target) / 3.0f;
const auto trim_height = prev + (cur - prev) * step;
set_ar_corrected_trim(get_window((camera_class*)i_this), trim_height);
}
}, i_this);
}
#else
#if !TARGET_PC
// trim handling moved to camera_execute for PC
int trim_height = body->TrimHeight();
window->setScissor(0.0f, trim_height, FB_WIDTH, FB_HEIGHT - trim_height * 2.0f);
+10 -1
View File
@@ -36,6 +36,7 @@
#include "dusk/imgui/ImGuiBloomWindow.hpp"
#include "dusk/settings.h"
#include "dusk/frame_interpolation.h"
#include "dusk/game_clock.h"
#endif
static void GxXFog_set();
@@ -2268,6 +2269,7 @@ void dKy_calc_color_set(GXColorS10* out_color_p, color_RGB_class* color_a_start_
color_b_start_p->b, color_b_end_p->b, blend_ratio, add_col.b, scale);
}
void dScnKy_env_light_c::setLight() {
f32 color_ratio;
@@ -2513,7 +2515,14 @@ void dScnKy_env_light_c::setLight() {
static s16 S_fuwan_sin;
f32 sin = cM_ssin(S_fuwan_sin);
S_fuwan_sin += (s16)cM_rndF(2000.0f) + 500;
#if TARGET_PC
const f32 deltaTime = dusk::game_clock::consume_interval(this);
const f32 timeScale = deltaTime / dusk::game_clock::period_for_original_frames(1.0f);
S_fuwan_sin += (s16)((cM_rndF(2000.0f) + 500) * timeScale);
#else
S_fuwan_sin += (s16)cM_rndF(2000.0f) + 500;
#endif
blure_size += (u8)(sin * (0.2f * blure_size));
}
+26 -1
View File
@@ -152,7 +152,7 @@ u8 STControl::checkTrigger() {
field_0x22 = -field_0x24;
}
}
#if !TARGET_PC
if (!(mDirectionTrig & 3)) {
Xinit();
}
@@ -160,10 +160,35 @@ u8 STControl::checkTrigger() {
if (!(mDirectionTrig & 0xC)) {
Yinit();
}
#endif
} else {
mDirectionTrig = 0;
#if !TARGET_PC
Xinit();
Yinit();
#endif
#if TARGET_PC
if (mDoCPd_c::getHoldLeft(PAD_1)) {
mDirectionTrig |= TRIG_LEFT;
}
if (mDoCPd_c::getHoldRight(PAD_1)) {
mDirectionTrig |= TRIG_RIGHT;
}
if (mDoCPd_c::getHoldUp(PAD_1)) {
mDirectionTrig |= TRIG_UP;
}
if (mDoCPd_c::getHoldDown(PAD_1)) {
mDirectionTrig |= TRIG_DOWN;
}
}
if (!(mDirectionTrig & 3)) {
Xinit();
}
if (!(mDirectionTrig & 0xC)) {
Yinit();
#endif
}
if ((field_0x0d & mDirectionTrig & 3) && field_0x0e > 0) {
+14
View File
@@ -1141,6 +1141,9 @@ dMap_c::dMap_c(int width, int height, int param_2, int param_3) {
field_0x91 = 0;
m_mySelfPointer = this;
#endif
#if TARGET_PC
previousMirror = dusk::getSettings().game.enableMirrorMode;
#endif
m_res = JKR_NEW_ARGS (0x20) dMap_prm_res_s;
JUT_ASSERT(2559, m_res != NULL);
@@ -1579,6 +1582,17 @@ bool dMap_c::isDrawRoomIcon(int param_0, int param_1) const {
}
void dMap_c::_move(f32 i_centerX, f32 i_centerZ, int i_roomNo, f32 param_3) {
#if TARGET_PC
bool currentMirror = dusk::getSettings().game.enableMirrorMode;
if (currentMirror != previousMirror) {
previousMirror = currentMirror;
if (currentMirror) {
mCenterX -= 2.0f * mPackX;
} else {
mCenterX += 2.0f * mPackX;
}
}
#endif
if (mStayRoomNo == -1) {
mStayRoomNo = i_roomNo;
field_0x80 = mStayRoomNo;
+1 -1
View File
@@ -139,7 +139,7 @@ bool dMenu_Fishing_c::isSync() {
void dMenu_Fishing_c::init() {
#if TARGET_PC || VERSION == VERSION_GCN_PAL
BOOL isEnglish = FALSE;
if (dusk::version::isRegionUsa() || (dusk::version::isRegionPal() && dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH)) {
if (dusk::version::isRegionPal() && dComIfGs_getPalLanguage() == dSv_player_config_c::LANGUAGE_ENGLISH) {
isEnglish = TRUE;
}
#endif
+3 -2
View File
@@ -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;
+5 -1
View File
@@ -11,6 +11,7 @@
#include "d/d_s_name.h"
#include "dusk/imgui/ImGuiConsole.hpp"
#include "dusk/memory.h"
#include "dusk/speedrun.h"
#include "dusk/settings.h"
#include "f_op/f_op_overlap_mng.h"
#include "f_op/f_op_scene_mng.h"
@@ -19,6 +20,7 @@
#include "m_Do/m_Do_machine.h"
#include "m_Do/m_Do_main.h"
#include "m_Do/m_Do_mtx.h"
#include <dusk/autosave.h>
#if TARGET_PC
#define SHOW_TV_SETTINGS_SCREEN (this->mShowTvSettingsScreen)
@@ -418,10 +420,12 @@ void dScnName_c::changeGameScene() {
if (dusk::getSettings().game.speedrunMode && dusk::getSettings().game.hideTvSettingsScreen) {
// start a new run on file load if a run isn't already in progress
if (!dusk::m_speedrunInfo.m_isRunStarted) {
dusk::ImGuiMenuGame::resetForSpeedrunMode();
dusk::resetForSpeedrunMode();
dusk::m_speedrunInfo.startRun();
}
}
toggleAutoSave(true);
#endif
}
}
+4
View File
@@ -1042,6 +1042,10 @@ static BOOL heapSizeCheck() {
bool dScnPly_c::resetGame() {
if (fpcM_GetName(this) == fpcNm_OPENING_SCENE_e) {
#if TARGET_PC
toggleAutoSave(false);
#endif
if (!dStage_roomControl_c::resetArchiveBank(0)) {
return false;
}
+5
View File
@@ -53,6 +53,11 @@ static std::string FormatToString(const char* msg, va_list list) {
size *= 2;
}
}
while (!str.empty() && str[str.size()-1] == '\n') {
str.pop_back();
}
return str;
}
+8 -2
View File
@@ -692,6 +692,12 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
return;
}
// prevent stuff like https://github.com/TwilitRealm/dusklight/issues/949
if (link->getDemoMode() != 0) {
inJump = false;
return;
}
if (!inJump) {
if (link->mProcID == daAlink_c::PROC_CUT_JUMP) {
inJump = true;
@@ -1030,7 +1036,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 +1073,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&) {}
+96
View File
@@ -0,0 +1,96 @@
#include "dusk/action_bindings.h"
#include "aurora/lib/input.hpp"
#include "dusk/settings.h"
#include "dusk/ui/ui.hpp"
namespace dusk {
static std::array<std::array<ActionBindPressData, static_cast<int>(ActionBinds::COUNT)>, PAD_CHANMAX> actionPressData{};
ActionBindsMap& getActionBinds() {
static ActionBindsMap actionBinds = {
{ActionBinds::FIRST_PERSON_CAMERA, {&getSettings().actionBindings.firstPersonCamera, "First Person Camera"}},
{ActionBinds::CALL_MIDNA, {&getSettings().actionBindings.callMidna, "Call Midna"}},
{ActionBinds::OPEN_DUSKLIGHT_MENU, {&getSettings().actionBindings.openDusklightMenu, "Open Dusklight Menu"}},
{ActionBinds::TURBO_SPEED_BUTTON, {&getSettings().actionBindings.turboSpeedButton, "Turbo Speed Button"}},
};
return actionBinds;
}
bool isActionBound(ActionBinds action, u32 port) {
auto& actionBinds = getActionBinds();
// Check to make sure action is properly bound
if (!actionBinds.contains(action)) {
return false;
}
return getActionBindButton(action, port) != PAD_NATIVE_BUTTON_INVALID;
}
void updateActionBindings() {
for (u32 port = 0; port < PAD_CHANMAX; ++port) {
// Move the current press to the previous frame
for (auto& pressData : actionPressData[port]) {
pressData.pressedPrevFrame = pressData.pressedCurFrame;
pressData.pressedCurFrame = false;
}
// Update current frame with whether action button is pressed
for (auto& [action, boundAction] : getActionBinds()) {
// If the action isn't bound, or if documents are visible and the action isn't
// opening the dusklight menu, don't update. Otherwise, we may accidentally
// perform actions while the dusklight menu is open.
if (!isActionBound(action, port) ||
(ui::any_document_visible() && action != ActionBinds::OPEN_DUSKLIGHT_MENU)) {
continue;
}
int button = boundAction.configVars->at(port);
// If keyboard is active for this port
u32 count = 0;
if (PADGetKeyButtonBindings(port, &count) != nullptr) {
int numKeys = 0;
const bool* kbState = SDL_GetKeyboardState(&numKeys);
if (kbState[button]) {
actionPressData[port][static_cast<int>(action)].pressedCurFrame = true;
}
} else {
// If controller is active
auto controller = aurora::input::get_controller_for_player(port);
if (controller) {
if (SDL_GetGamepadButton(controller->m_controller, static_cast<SDL_GamepadButton>(button))) {
actionPressData[port][static_cast<int>(action)].pressedCurFrame = true;
}
}
}
}
}
}
bool getActionBindTrig(ActionBinds action, u32 port) {
return isActionBound(action, port) &&
actionPressData[port][static_cast<int>(action)].pressedCurFrame &&
!actionPressData[port][static_cast<int>(action)].pressedPrevFrame;
}
bool getActionBindHold(ActionBinds action, u32 port) {
return isActionBound(action, port) &&
actionPressData[port][static_cast<int>(action)].pressedCurFrame &&
actionPressData[port][static_cast<int>(action)].pressedPrevFrame;
}
bool getActionBindHoldAnyPort(ActionBinds action) {
for (u32 port = 0; port < PAD_CHANMAX; ++port) {
if (getActionBindHold(action, port)) {
return true;
}
}
return false;
}
int getActionBindButton(ActionBinds action, u32 port) {
return (*getActionBinds()[action].configVars)[port];
}
}
+6 -1
View File
@@ -2,6 +2,7 @@
#include "dusk/ui/ui.hpp"
#include "imgui/ImGuiConsole.hpp"
bool shouldAutoSave = false;
u8 mSaveBuffer[QUEST_LOG_SIZE * 3];
u8 mAutoSaveProc = 0;
int autoSaveWriteState = 0;
@@ -14,7 +15,7 @@ static AutoSaveFuncs AutoSaveFuncsProc[] = {
void noAutoSave() {}
void triggerAutoSave() {
if (dusk::getSettings().game.autoSave && mAutoSaveProc == 0 &&
if (dusk::getSettings().game.autoSave && shouldAutoSave && mAutoSaveProc == 0 &&
strcmp(dComIfGp_getStartStageName(), "F_SP102") != 0)
{
mAutoSaveProc = 1;
@@ -89,4 +90,8 @@ void endAutoSave() {
.duration = std::chrono::milliseconds(1500),
});
mAutoSaveProc = 0;
}
void toggleAutoSave(bool enabled) {
shouldAutoSave = enabled;
}
+21 -5
View File
@@ -11,6 +11,7 @@
#include <string>
#include "dusk/main.h"
#include "dusk/action_bindings.h"
using namespace dusk::config;
@@ -23,8 +24,8 @@ aurora::Module DuskConfigLog("dusk::config");
static absl::flat_hash_map<std::string_view, ConfigVarBase*> 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) {
@@ -189,7 +190,7 @@ void dusk::config::LoadFromUserPreferences() {
if (configJsonPath.empty()) {
return;
}
LoadFromFileName(configJsonPath.c_str());
LoadFromFileName(reinterpret_cast<const char*>(configJsonPath.c_str()));
}
static void LoadFromPath(const char* path) {
@@ -241,7 +242,9 @@ void dusk::config::Save() {
return;
}
DuskConfigLog.info("Saving config to '{}'", configJsonPath);
DuskConfigLog.info(
"Saving config to '{}'",
reinterpret_cast<const char*>(configJsonPath.c_str()));
json j;
@@ -251,7 +254,14 @@ void dusk::config::Save() {
}
}
io::FileStream::WriteAllText(configJsonPath.c_str(), j.dump(4));
io::FileStream::WriteAllText(reinterpret_cast<const char*>(configJsonPath.c_str()), j.dump(4));
}
void dusk::config::ClearAllActionBindings(int port) {
for (auto& actionBinding : getActionBinds() | std::views::values) {
actionBinding.configVars->at(port).setValue(PAD_NATIVE_BUTTON_INVALID);
}
Save();
}
ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) {
@@ -262,3 +272,9 @@ ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) {
return nullptr;
}
void dusk::config::EnumerateRegistered(std::function<void(ConfigVarBase&)> callback) {
for (auto& pair : RegisteredConfigVars) {
callback(*pair.second);
}
}
+66 -65
View File
@@ -4,7 +4,6 @@
#include "dusk/dusk.h"
#include "dusk/logging.h"
#include "dusk/main.h"
#include "dusk/settings.h"
#include "version.h"
#include <cstdlib>
@@ -13,114 +12,83 @@
#include <string_view>
#include <system_error>
#include "SDL3/SDL_filesystem.h"
#if DUSK_ENABLE_SENTRY_NATIVE
#include <sentry.h>
#endif
namespace dusk {
namespace dusk::crash_reporting {
namespace {
#if DUSK_ENABLE_SENTRY_NATIVE
bool g_sentryInitialized = false;
bool IsTruthy(std::string_view value) {
return value == "1" || value == "true" || value == "TRUE" || value == "yes"
|| value == "YES" || value == "on" || value == "ON";
bool truthy(std::string_view value) {
return value == "1" || value == "true" || value == "TRUE" || value == "yes" || value == "YES" ||
value == "on" || value == "ON";
}
std::string GetEnvOrEmpty(const char* name) {
std::string env_or_empty(const char* name) {
if (const char* value = std::getenv(name)) {
return value;
}
return {};
}
bool GetEffectiveEnabled() {
const std::string env = GetEnvOrEmpty("DUSK_SENTRY_ENABLED");
if (!env.empty()) {
return IsTruthy(env);
}
return getSettings().backend.enableCrashReporting;
bool disabled_by_env() {
const std::string env = env_or_empty("DUSK_SENTRY_ENABLED");
return !env.empty() && !truthy(env);
}
std::string GetEffectiveDsn() {
const std::string env = GetEnvOrEmpty("DUSK_SENTRY_DSN");
std::string effective_dsn() {
const std::string env = env_or_empty("DUSK_SENTRY_DSN");
if (!env.empty()) {
return env;
}
return DUSK_SENTRY_DSN;
}
bool GetEffectiveDebug() {
const std::string env = GetEnvOrEmpty("DUSK_SENTRY_DEBUG");
bool effective_debug() {
const std::string env = env_or_empty("DUSK_SENTRY_DEBUG");
if (!env.empty()) {
return IsTruthy(env);
return truthy(env);
}
return false;
}
std::string GetReleaseName() {
std::string release_name() {
return std::string(AppName) + "@" DUSK_WC_DESCRIBE;
}
std::filesystem::path GetSentryDatabasePath() {
std::filesystem::path sentry_database_path() {
return dusk::ConfigPath / "sentry";
}
std::filesystem::path GetLogAttachmentPath() {
std::filesystem::path log_attachment_path() {
if (const char* logPath = GetLogFilePath()) {
return logPath;
}
return {};
}
std::filesystem::path GetCrashpadHandlerPath() {
const char* basePath = SDL_GetBasePath();
if (!basePath) {
return {};
}
const std::filesystem::path handlerDir(basePath);
#if _WIN32
return handlerDir / "crashpad_handler.exe";
#else
return handlerDir / "crashpad_handler";
#endif
}
void ConfigurePathOptions(sentry_options_t* options) {
const auto databasePath = GetSentryDatabasePath();
void configure_path_options(sentry_options_t* options) {
const auto databasePath = sentry_database_path();
std::error_code ec;
std::filesystem::create_directories(databasePath, ec);
if (ec) {
DuskLog.warn("Unable to create Sentry database path '{}': {}",
databasePath.string(), ec.message());
DuskLog.warn(
"Unable to create Sentry database path '{}': {}", databasePath.string(), ec.message());
}
#if _WIN32
const std::wstring databasePathWide = databasePath.wstring();
sentry_options_set_database_pathw(options, databasePathWide.c_str());
const auto handlerPath = GetCrashpadHandlerPath();
if (!handlerPath.empty()) {
const std::wstring handlerPathWide = handlerPath.wstring();
sentry_options_set_handler_pathw(options, handlerPathWide.c_str());
}
#else
const std::string databasePathUtf8 = databasePath.string();
sentry_options_set_database_path(options, databasePathUtf8.c_str());
const auto handlerPath = GetCrashpadHandlerPath();
if (!handlerPath.empty()) {
const std::string handlerPathUtf8 = handlerPath.string();
sentry_options_set_handler_path(options, handlerPathUtf8.c_str());
}
#endif
const auto logPath = GetLogAttachmentPath();
const auto logPath = log_attachment_path();
if (!logPath.empty()) {
#if _WIN32
sentry_options_add_attachmentw(options, logPath.wstring().c_str());
@@ -133,32 +101,29 @@ void ConfigurePathOptions(sentry_options_t* options) {
} // namespace
void InitializeCrashReporting() {
void initialize() {
#if DUSK_ENABLE_SENTRY_NATIVE
if (g_sentryInitialized) {
if (g_sentryInitialized || disabled_by_env()) {
return;
}
if (!GetEffectiveEnabled()) {
return;
}
const std::string dsn = GetEffectiveDsn();
const std::string dsn = effective_dsn();
if (dsn.empty()) {
DuskLog.warn("Crash reporting is enabled but no Sentry DSN is configured");
return;
}
const std::string release = GetReleaseName();
const std::string release = release_name();
sentry_options_t* options = sentry_options_new();
sentry_options_set_dsn(options, dsn.c_str());
sentry_options_set_release(options, release.c_str());
sentry_options_set_environment(options, DUSK_SENTRY_ENVIRONMENT);
sentry_options_set_debug(options, GetEffectiveDebug() ? 1 : 0);
sentry_options_set_debug(options, effective_debug() ? 1 : 0);
sentry_options_set_require_user_consent(options, 1);
sentry_options_set_cache_keep(options, 1);
sentry_options_set_max_breadcrumbs(options, 100);
ConfigurePathOptions(options);
configure_path_options(options);
if (sentry_init(options) != 0) {
DuskLog.warn("Failed to initialize Sentry crash reporting");
@@ -173,7 +138,7 @@ void InitializeCrashReporting() {
#endif
}
void ShutdownCrashReporting() {
void shutdown() {
#if DUSK_ENABLE_SENTRY_NATIVE
if (!g_sentryInitialized) {
return;
@@ -184,4 +149,40 @@ void ShutdownCrashReporting() {
#endif
}
} // namespace dusk
Consent get_consent() {
#if DUSK_ENABLE_SENTRY_NATIVE
if (!g_sentryInitialized) {
return Consent::Unavailable;
}
switch (sentry_user_consent_get()) {
case SENTRY_USER_CONSENT_GIVEN:
return Consent::Given;
case SENTRY_USER_CONSENT_REVOKED:
return Consent::Revoked;
case SENTRY_USER_CONSENT_UNKNOWN:
default:
return Consent::Unknown;
}
#else
return Consent::Unavailable;
#endif
}
void set_consent(bool enabled) {
#if DUSK_ENABLE_SENTRY_NATIVE
if (!g_sentryInitialized) {
return;
}
if (enabled) {
sentry_user_consent_give();
} else {
sentry_user_consent_revoke();
}
#else
(void)enabled;
#endif
}
} // namespace dusk::crash_reporting
+971
View File
@@ -0,0 +1,971 @@
#include "data.hpp"
#include "dusk/app_info.hpp"
#include "dusk/io.hpp"
#include "dusk/logging.h"
#include "dusk/main.h"
#include <array>
#include <filesystem>
#include <optional>
#include <ranges>
#include <string>
#include <string_view>
#include <system_error>
#include <vector>
#include <SDL3/SDL_filesystem.h>
#include <SDL3/SDL_iostream.h>
#include <SDL3/SDL_misc.h>
#include <SDL3/SDL_stdinc.h>
#include "nlohmann/json.hpp"
namespace dusk::data {
namespace {
aurora::Module Log{"dusk::data"};
constexpr auto kLocationDescriptorName = "data_location.json";
constexpr auto kPipelineCacheName = "pipeline_cache.db";
constexpr auto kInitialPipelineCacheName = "initial_pipeline_cache.db";
enum class LocationMode {
Default,
Portable,
Custom,
};
struct LocationDescriptor {
LocationMode mode = LocationMode::Default;
std::filesystem::path customPath;
std::filesystem::path previousPath;
};
struct LocatedDescriptor {
LocationDescriptor descriptor;
std::filesystem::path path;
};
struct MigrationStats {
std::uintmax_t directoriesCreated = 0;
std::uintmax_t filesCopied = 0;
std::uintmax_t symlinksCopied = 0;
std::uintmax_t sourcesRemoved = 0;
std::uintmax_t emptyDirectoriesRemoved = 0;
std::uintmax_t skippedExistingTargets = 0;
std::uintmax_t skippedDescriptorFiles = 0;
std::uintmax_t skippedNestedTargets = 0;
std::uintmax_t skippedUnsupportedEntries = 0;
std::uintmax_t failures = 0;
};
std::optional<std::filesystem::path> sConfiguredDataPath;
std::optional<std::filesystem::path> sActiveDescriptorPath;
std::filesystem::path path_from_utf8(std::string_view value) {
return std::filesystem::path{
reinterpret_cast<const char8_t*>(value.data()),
reinterpret_cast<const char8_t*>(value.data() + value.size()),
};
}
std::filesystem::path get_legacy_path() {
if (std::string_view{LegacyAppName}.empty()) {
return {};
}
char* prefPath = SDL_GetPrefPath(OrgName, LegacyAppName);
if (!prefPath) {
Log.fatal("Unable to get PrefPath: {}", SDL_GetError());
}
std::filesystem::path result{reinterpret_cast<const char8_t*>(prefPath)};
SDL_free(prefPath);
return result;
}
std::filesystem::path get_pref_path() {
char* prefPath = SDL_GetPrefPath(OrgName, AppName);
if (!prefPath) {
Log.fatal("Unable to get PrefPath: {}", SDL_GetError());
}
std::filesystem::path result{reinterpret_cast<const char8_t*>(prefPath)};
SDL_free(prefPath);
return result;
}
std::filesystem::path base_path_relative(const std::filesystem::path& path) {
const auto* basePath = SDL_GetBasePath();
if (!basePath) {
return path;
}
return std::filesystem::path{basePath} / path;
}
std::filesystem::path default_data_path(const std::filesystem::path& prefPath) {
#ifdef __APPLE__
#if TARGET_OS_IOS && !TARGET_OS_TV
const char* documentsPath = SDL_GetUserFolder(SDL_FOLDER_DOCUMENTS);
if (!documentsPath) {
Log.fatal("Unable to get iOS Documents path: {}", SDL_GetError());
}
return reinterpret_cast<const char8_t*>(documentsPath);
#endif
#endif
return prefPath;
}
std::filesystem::path portable_data_path() {
return base_path_relative("data");
}
std::vector<std::filesystem::path> descriptor_paths(const std::filesystem::path& prefPath) {
std::vector<std::filesystem::path> paths;
if (const auto basePath = base_path_relative(kLocationDescriptorName); !basePath.empty()) {
paths.push_back(basePath);
}
paths.push_back(prefPath / kLocationDescriptorName);
return paths;
}
std::optional<LocationDescriptor> read_location_descriptor_file(const std::filesystem::path& path) {
if (path.empty()) {
return std::nullopt;
}
if (std::error_code ec; !std::filesystem::exists(path, ec)) {
return std::nullopt;
}
try {
const auto bytes = io::FileStream::ReadAllBytes(path);
const auto json = nlohmann::json::parse(bytes);
if (!json.is_object()) {
Log.warn("Ignoring data location descriptor '{}': root is not an object",
io::fs_path_to_string(path));
return std::nullopt;
}
LocationDescriptor descriptor;
const auto mode = json.value<std::string>("mode", "default");
if (mode == "portable") {
descriptor.mode = LocationMode::Portable;
} else if (mode == "custom") {
descriptor.mode = LocationMode::Custom;
} else if (mode != "default") {
Log.warn("Ignoring unknown data location mode '{}'", mode);
}
if (const auto customPath = json.find("customPath");
customPath != json.end() && customPath->is_string())
{
descriptor.customPath = path_from_utf8(customPath->get<std::string>());
}
if (const auto previousPath = json.find("previousPath");
previousPath != json.end() && previousPath->is_string())
{
descriptor.previousPath = path_from_utf8(previousPath->get<std::string>());
}
return descriptor;
} catch (const std::exception& e) {
Log.warn(
"Ignoring data location descriptor '{}': {}", io::fs_path_to_string(path), e.what());
return std::nullopt;
}
}
std::optional<LocatedDescriptor> read_location_descriptor(const std::filesystem::path& prefPath) {
for (const auto& path : descriptor_paths(prefPath)) {
if (auto descriptor = read_location_descriptor_file(path)) {
return LocatedDescriptor{
.descriptor = *descriptor,
.path = path,
};
}
}
return std::nullopt;
}
std::filesystem::path resolve_data_path(
const std::filesystem::path& prefPath, const LocationDescriptor* descriptor) {
if (!descriptor) {
return default_data_path(prefPath);
}
switch (descriptor->mode) {
case LocationMode::Default:
return default_data_path(prefPath);
case LocationMode::Portable:
return portable_data_path();
case LocationMode::Custom:
if (!descriptor->customPath.empty()) {
return descriptor->customPath;
}
Log.warn("Data location descriptor requested custom mode without a path");
return default_data_path(prefPath);
}
return default_data_path(prefPath);
}
const char* location_mode_id(LocationMode mode) {
switch (mode) {
case LocationMode::Default:
return "default";
case LocationMode::Portable:
return "portable";
case LocationMode::Custom:
return "custom";
}
return "default";
}
std::filesystem::path normalized_path(const std::filesystem::path& path) {
std::error_code ec;
auto normalized = std::filesystem::weakly_canonical(path, ec);
if (!ec) {
return normalized;
}
normalized = std::filesystem::absolute(path, ec);
if (!ec) {
return normalized.lexically_normal();
}
return path.lexically_normal();
}
std::filesystem::path absolute_path(const std::filesystem::path& path) {
std::error_code ec;
const auto absolute = std::filesystem::absolute(path, ec);
if (ec) {
return path;
}
return absolute.lexically_normal();
}
bool is_same_or_inside(const std::filesystem::path& root, const std::filesystem::path& path) {
const auto normalizedRoot = normalized_path(root);
const auto normalizedPath = normalized_path(path);
const auto relativePath = normalizedPath.lexically_relative(normalizedRoot);
if (relativePath.empty()) {
return normalizedPath == normalizedRoot;
}
if (relativePath == ".") {
return true;
}
if (relativePath.is_absolute()) {
return false;
}
const auto it = relativePath.begin();
return it == relativePath.end() || *it != "..";
}
bool should_skip_migration_path(const std::filesystem::path& path,
const std::filesystem::path& from, const std::filesystem::path& to, MigrationStats& stats) {
if (is_same_or_inside(to, path)) {
++stats.skippedNestedTargets;
return true;
}
const auto relativePath = path.lexically_relative(from);
if (relativePath == kLocationDescriptorName) {
++stats.skippedDescriptorFiles;
return true;
}
return false;
}
bool has_location_descriptor(const std::filesystem::path& path) {
std::error_code ec;
return std::filesystem::exists(path / kLocationDescriptorName, ec);
}
bool remove_empty_destination_for_rename(const std::filesystem::path& path) {
std::error_code ec;
const bool exists = std::filesystem::exists(path, ec);
if (ec) {
Log.debug("Could not inspect migration destination '{}': {}", io::fs_path_to_string(path),
ec.message());
return false;
}
if (!exists) {
return true;
}
const bool canRemove = std::filesystem::is_directory(path, ec) &&
std::filesystem::is_empty(path, ec) && !has_location_descriptor(path);
if (ec || !canRemove) {
if (ec) {
Log.debug("Could not inspect migration destination '{}': {}",
io::fs_path_to_string(path), ec.message());
}
return false;
}
std::filesystem::remove(path, ec);
if (ec) {
Log.debug("Could not remove empty migration destination '{}': {}",
io::fs_path_to_string(path), ec.message());
return false;
}
return true;
}
bool try_rename_directory_migration(
const std::filesystem::path& from, const std::filesystem::path& to) {
std::error_code ec;
if (!std::filesystem::is_directory(from, ec)) {
return false;
}
if (ec) {
Log.debug("Could not inspect migration source '{}': {}", io::fs_path_to_string(from),
ec.message());
return false;
}
if (has_location_descriptor(from)) {
return false;
}
if (!remove_empty_destination_for_rename(to)) {
return false;
}
std::filesystem::create_directories(to.parent_path(), ec);
if (ec) {
Log.debug("Could not create migration destination parent '{}': {}",
io::fs_path_to_string(to.parent_path()), ec.message());
return false;
}
std::filesystem::rename(from, to, ec);
if (ec) {
Log.debug("Could not rename data directory '{}' to '{}': {}", io::fs_path_to_string(from),
io::fs_path_to_string(to), ec.message());
return false;
}
Log.info("Renamed data directory '{}' to '{}'", io::fs_path_to_string(from),
io::fs_path_to_string(to));
return true;
}
std::filesystem::path current_data_path() {
if (!ConfigPath.empty()) {
return ConfigPath;
}
const auto prefPath = get_pref_path();
const auto descriptor = read_location_descriptor(prefPath);
if (descriptor) {
sActiveDescriptorPath = descriptor->path;
}
return resolve_data_path(prefPath, descriptor ? &descriptor->descriptor : nullptr);
}
std::vector<std::filesystem::path> descriptor_write_paths(const std::filesystem::path& prefPath) {
if (sActiveDescriptorPath && !sActiveDescriptorPath->empty()) {
return {*sActiveDescriptorPath};
}
std::vector<std::filesystem::path> paths;
#if defined(_WIN32)
if (const auto basePath = base_path_relative(kLocationDescriptorName); !basePath.empty()) {
paths.push_back(basePath);
}
#endif
paths.push_back(prefPath / kLocationDescriptorName);
return paths;
}
bool write_descriptor_json(const std::filesystem::path& path, const nlohmann::json& json) {
std::error_code ec;
std::filesystem::create_directories(path.parent_path(), ec);
if (ec) {
Log.warn("Failed to create data location descriptor directory '{}': {}",
io::fs_path_to_string(path.parent_path()), ec.message());
return false;
}
try {
io::FileStream::WriteAllText(path, json.dump(4));
} catch (const std::exception& e) {
Log.warn("Failed to write data location descriptor '{}': {}", io::fs_path_to_string(path),
e.what());
return false;
}
return true;
}
bool write_location_descriptor(LocationMode mode, const std::filesystem::path& targetPath) {
LocationDescriptor descriptor;
descriptor.mode = mode;
if (mode == LocationMode::Custom) {
descriptor.customPath = absolute_path(targetPath);
}
const auto currentPath = current_data_path();
const auto resolvedTargetPath =
mode == LocationMode::Custom ? descriptor.customPath : targetPath;
if (!currentPath.empty() && normalized_path(currentPath) != normalized_path(resolvedTargetPath))
{
descriptor.previousPath = currentPath;
}
nlohmann::json json;
json["version"] = 1;
json["mode"] = location_mode_id(descriptor.mode);
if (descriptor.mode == LocationMode::Custom && !descriptor.customPath.empty()) {
json["customPath"] = io::fs_path_to_string(descriptor.customPath);
}
if (!descriptor.previousPath.empty()) {
json["previousPath"] = io::fs_path_to_string(descriptor.previousPath);
}
const auto prefPath = get_pref_path();
for (const auto& path : descriptor_write_paths(prefPath)) {
if (write_descriptor_json(path, json)) {
sActiveDescriptorPath = path;
sConfiguredDataPath = resolvedTargetPath;
return true;
}
}
return false;
}
std::uintmax_t remove_empty_directories(const std::filesystem::path& root, bool includeRoot) {
std::error_code ec;
std::vector<std::filesystem::path> directories;
for (std::filesystem::recursive_directory_iterator it(
root, std::filesystem::directory_options::skip_permission_denied, ec);
it != std::filesystem::recursive_directory_iterator(); it.increment(ec))
{
if (ec) {
Log.warn("Failed to scan empty directories under '{}': {}", io::fs_path_to_string(root),
ec.message());
return 0;
}
const auto status = it->symlink_status(ec);
if (ec) {
Log.warn("Failed to inspect '{}' while pruning empty directories: {}",
io::fs_path_to_string(it->path()), ec.message());
ec.clear();
continue;
}
if (std::filesystem::is_directory(status)) {
directories.push_back(it->path());
}
}
std::uintmax_t removed = 0;
for (auto& dir : std::views::reverse(directories)) {
if (!std::filesystem::is_empty(dir, ec)) {
ec.clear();
continue;
}
if (std::filesystem::remove(dir, ec)) {
++removed;
} else if (ec) {
Log.warn("Failed to remove empty migrated source directory '{}': {}",
io::fs_path_to_string(dir), ec.message());
}
ec.clear();
}
if (includeRoot) {
if (std::filesystem::is_empty(root, ec)) {
if (std::filesystem::remove(root, ec)) {
++removed;
} else if (ec) {
Log.warn("Failed to remove empty migrated source root '{}': {}",
io::fs_path_to_string(root), ec.message());
}
}
ec.clear();
}
return removed;
}
bool ensure_parent_directory(const std::filesystem::path& targetPath, MigrationStats& stats) {
std::error_code ec;
std::filesystem::create_directories(targetPath.parent_path(), ec);
if (ec) {
++stats.failures;
Log.warn("Failed to create migration target parent '{}': {}",
io::fs_path_to_string(targetPath.parent_path()), ec.message());
return false;
}
return true;
}
bool remove_migrated_source(const std::filesystem::path& sourcePath, MigrationStats& stats) {
std::error_code ec;
std::filesystem::remove(sourcePath, ec);
if (ec) {
++stats.failures;
Log.warn("Migrated '{}' but failed to remove source: {}", io::fs_path_to_string(sourcePath),
ec.message());
return false;
}
++stats.sourcesRemoved;
return true;
}
bool try_rename_migration_entry(
const std::filesystem::path& sourcePath, const std::filesystem::path& targetPath) {
std::error_code ec;
if (std::filesystem::exists(targetPath, ec) || std::filesystem::is_symlink(targetPath, ec)) {
return false;
}
ec.clear();
if (!std::filesystem::exists(sourcePath, ec)) {
return false;
}
ec.clear();
std::filesystem::create_directories(targetPath.parent_path(), ec);
if (ec) {
Log.debug("Could not create migration target parent '{}' before rename: {}",
io::fs_path_to_string(targetPath.parent_path()), ec.message());
return false;
}
std::filesystem::rename(sourcePath, targetPath, ec);
if (ec) {
Log.debug("Could not rename migration entry '{}' to '{}': {}",
io::fs_path_to_string(sourcePath), io::fs_path_to_string(targetPath), ec.message());
return false;
}
return true;
}
void migrate_symlink(const std::filesystem::path& sourcePath,
const std::filesystem::path& targetPath, MigrationStats& stats) {
std::error_code ec;
if (std::filesystem::exists(targetPath, ec) || std::filesystem::is_symlink(targetPath, ec)) {
++stats.skippedExistingTargets;
return;
}
ec.clear();
const auto linkTarget = std::filesystem::read_symlink(sourcePath, ec);
if (ec) {
++stats.failures;
Log.warn("Failed to read migration symlink '{}': {}", io::fs_path_to_string(sourcePath),
ec.message());
return;
}
if (!ensure_parent_directory(targetPath, stats)) {
return;
}
const bool targetIsDirectory = std::filesystem::is_directory(sourcePath, ec);
if (ec) {
Log.debug("Could not resolve symlink target type for '{}': {}",
io::fs_path_to_string(sourcePath), ec.message());
ec.clear();
}
if (targetIsDirectory) {
std::filesystem::create_directory_symlink(linkTarget, targetPath, ec);
} else {
std::filesystem::create_symlink(linkTarget, targetPath, ec);
}
if (ec) {
++stats.failures;
Log.warn("Failed to migrate symlink '{}' -> '{}' to '{}': {}",
io::fs_path_to_string(sourcePath), io::fs_path_to_string(linkTarget),
io::fs_path_to_string(targetPath), ec.message());
return;
}
++stats.symlinksCopied;
remove_migrated_source(sourcePath, stats);
}
void migrate_regular_file(const std::filesystem::path& sourcePath,
const std::filesystem::path& targetPath, MigrationStats& stats) {
std::error_code ec;
if (std::filesystem::exists(targetPath, ec)) {
++stats.skippedExistingTargets;
return;
}
ec.clear();
if (try_rename_migration_entry(sourcePath, targetPath)) {
++stats.filesCopied;
++stats.sourcesRemoved;
return;
}
if (!ensure_parent_directory(targetPath, stats)) {
return;
}
std::filesystem::copy_file(
sourcePath, targetPath, std::filesystem::copy_options::skip_existing, ec);
if (ec) {
++stats.failures;
Log.warn("Failed to migrate file '{}' to '{}': {}", io::fs_path_to_string(sourcePath),
io::fs_path_to_string(targetPath), ec.message());
return;
}
++stats.filesCopied;
remove_migrated_source(sourcePath, stats);
}
void migrate_directory(const std::filesystem::path& from, const std::filesystem::path& to,
const std::filesystem::path& prefPath) {
if (from.empty() || to.empty() || normalized_path(from) == normalized_path(to)) {
Log.debug("Skipping data migration from '{}' to '{}'", io::fs_path_to_string(from),
io::fs_path_to_string(to));
return;
}
MigrationStats stats;
std::error_code ec;
if (!std::filesystem::exists(from, ec)) {
if (ec) {
Log.warn("Failed to inspect migration source '{}': {}", io::fs_path_to_string(from),
ec.message());
} else {
Log.debug("Migration source '{}' does not exist", io::fs_path_to_string(from));
}
return;
}
if (try_rename_directory_migration(from, to)) {
return;
}
if (try_rename_directory_migration(from, to)) {
return;
}
std::filesystem::create_directories(to, ec);
if (ec) {
++stats.failures;
Log.warn("Failed to create data directory '{}' for migration: {}",
io::fs_path_to_string(to), ec.message());
return;
}
std::filesystem::recursive_directory_iterator it(
from, std::filesystem::directory_options::skip_permission_denied, ec);
if (ec) {
Log.warn("Failed to begin migration scan for '{}': {}", io::fs_path_to_string(from),
ec.message());
return;
}
const std::filesystem::recursive_directory_iterator end;
while (it != end) {
if (ec) {
++stats.failures;
Log.warn(
"Migration scan error under '{}': {}", io::fs_path_to_string(from), ec.message());
ec.clear();
}
const auto sourcePath = it->path();
const auto status = it->symlink_status(ec);
if (ec) {
++stats.failures;
Log.warn("Failed to inspect migration source '{}': {}",
io::fs_path_to_string(sourcePath), ec.message());
ec.clear();
it.increment(ec);
continue;
}
if (should_skip_migration_path(sourcePath, from, to, stats)) {
if (std::filesystem::is_directory(status)) {
it.disable_recursion_pending();
}
ec.clear();
it.increment(ec);
continue;
}
const auto relativePath = sourcePath.lexically_relative(from);
if (relativePath.empty() || relativePath.is_absolute()) {
++stats.failures;
Log.warn("Failed to calculate migration relative path for '{}'",
io::fs_path_to_string(sourcePath));
it.increment(ec);
continue;
}
const auto targetPath = to / relativePath;
if (std::filesystem::is_symlink(status)) {
migrate_symlink(sourcePath, targetPath, stats);
} else if (std::filesystem::is_directory(status)) {
if (try_rename_migration_entry(sourcePath, targetPath)) {
++stats.directoriesCreated;
++stats.sourcesRemoved;
it.disable_recursion_pending();
} else {
std::filesystem::create_directories(targetPath, ec);
if (ec) {
++stats.failures;
Log.warn("Failed to create migration target directory '{}': {}",
io::fs_path_to_string(targetPath), ec.message());
ec.clear();
it.disable_recursion_pending();
} else {
++stats.directoriesCreated;
}
}
} else if (std::filesystem::is_regular_file(status)) {
migrate_regular_file(sourcePath, targetPath, stats);
} else {
++stats.skippedUnsupportedEntries;
}
it.increment(ec);
}
const bool includeRoot = normalized_path(from) != normalized_path(prefPath);
stats.emptyDirectoriesRemoved = remove_empty_directories(from, includeRoot);
const bool migratedAnything = stats.filesCopied > 0 || stats.symlinksCopied > 0 ||
stats.sourcesRemoved > 0 || stats.emptyDirectoriesRemoved > 0 ||
stats.failures > 0;
if (migratedAnything) {
Log.info(
"Finished data migration from '{}' to '{}': {} files copied, {} symlinks copied, {} "
"sources removed, {} empty directories removed, {} existing targets skipped, {} "
"descriptor files skipped, {} nested destination paths skipped, {} unsupported entries "
"skipped, {} failures",
io::fs_path_to_string(from), io::fs_path_to_string(to), stats.filesCopied,
stats.symlinksCopied, stats.sourcesRemoved, stats.emptyDirectoriesRemoved,
stats.skippedExistingTargets, stats.skippedDescriptorFiles, stats.skippedNestedTargets,
stats.skippedUnsupportedEntries, stats.failures);
}
}
void migrate_data(const std::filesystem::path& prefPath, const std::filesystem::path& dataPath,
const LocationDescriptor* descriptor) {
if (descriptor && !descriptor->previousPath.empty()) {
migrate_directory(descriptor->previousPath, dataPath, prefPath);
} else if (const auto legacyPath = get_legacy_path(); !legacyPath.empty()) {
migrate_directory(legacyPath, dataPath, prefPath);
}
}
void ensure_data_directory(const std::filesystem::path& dataPath) {
std::error_code ec;
std::filesystem::create_directories(dataPath, ec);
if (ec) {
Log.fatal("Failed to create data directory '{}': {}", io::fs_path_to_string(dataPath),
ec.message());
}
}
SDL_IOStream* open_initial_pipeline_cache_source(std::string& sourcePathString) {
const auto basePath = base_path_relative(kInitialPipelineCacheName);
sourcePathString = io::fs_path_to_string(basePath);
auto* source = SDL_IOFromFile(sourcePathString.c_str(), "rb");
if (source != nullptr) {
return source;
}
sourcePathString = std::string{kInitialPipelineCacheName};
return SDL_IOFromFile(sourcePathString.c_str(), "rb");
}
void ensure_initial_pipeline_cache(const std::filesystem::path& configDir) {
if (configDir.empty()) {
return;
}
std::error_code ec;
std::filesystem::create_directories(configDir, ec);
if (ec) {
Log.warn("Failed to create config directory '{}' for pipeline cache: {}",
io::fs_path_to_string(configDir), ec.message());
return;
}
const auto pipelineCachePath = configDir / kPipelineCacheName;
if (std::filesystem::exists(pipelineCachePath, ec)) {
return;
}
std::string sourcePathString;
SDL_IOStream* source = open_initial_pipeline_cache_source(sourcePathString);
if (source == nullptr) {
Log.info("No bundled initial pipeline cache found");
return;
}
const auto pipelineCacheString = io::fs_path_to_string(pipelineCachePath);
SDL_IOStream* destination = SDL_IOFromFile(pipelineCacheString.c_str(), "wb");
if (destination == nullptr) {
Log.warn("Failed to open '{}' for seeded pipeline cache: {}", pipelineCacheString,
SDL_GetError());
SDL_CloseIO(source);
return;
}
bool copied = true;
std::array<char, 64 * 1024> 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) {
Log.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;
}
Log.warn(
"Failed to read bundled pipeline cache '{}': {}", sourcePathString, SDL_GetError());
copied = false;
break;
}
}
if (!SDL_CloseIO(destination)) {
Log.warn(
"Failed to close seeded pipeline cache '{}': {}", pipelineCacheString, SDL_GetError());
copied = false;
}
SDL_CloseIO(source);
if (!copied) {
std::filesystem::remove(pipelineCachePath, ec);
return;
}
Log.info("Seeded pipeline cache from '{}'", sourcePathString);
}
} // namespace
bool open_data_path() {
#if DUSK_CAN_OPEN_DATA_FOLDER
std::error_code ec;
std::filesystem::path path = std::filesystem::absolute(ConfigPath, ec);
if (ec) {
Log.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())) {
Log.warn(
"Failed to open data folder '{}': {}", io::fs_path_to_string(path), SDL_GetError());
return false;
}
return true;
#else
return false;
#endif
}
bool set_custom_data_path(const std::filesystem::path& path) {
if (path.empty()) {
Log.warn("Ignoring empty custom data path");
return false;
}
return write_location_descriptor(LocationMode::Custom, path);
}
bool set_custom_data_path(const char* path) {
return set_custom_data_path(path_from_utf8(path));
}
bool set_portable_data_path() {
return write_location_descriptor(LocationMode::Portable, portable_data_path());
}
bool reset_data_path() {
const auto prefPath = get_pref_path();
return write_location_descriptor(LocationMode::Default, default_data_path(prefPath));
}
bool is_default_data_path() {
const auto prefPath = get_pref_path();
return normalized_path(configured_data_path()) == normalized_path(default_data_path(prefPath));
}
std::filesystem::path configured_data_path() {
if (sConfiguredDataPath) {
return *sConfiguredDataPath;
}
const auto prefPath = get_pref_path();
const auto descriptor = read_location_descriptor(prefPath);
if (descriptor) {
sActiveDescriptorPath = descriptor->path;
}
sConfiguredDataPath =
resolve_data_path(prefPath, descriptor ? &descriptor->descriptor : nullptr);
return *sConfiguredDataPath;
}
bool is_data_path_restart_pending() {
if (ConfigPath.empty()) {
return false;
}
return normalized_path(ConfigPath) != normalized_path(configured_data_path());
}
std::filesystem::path initialize_data() {
const auto prefPath = get_pref_path();
const auto descriptor = read_location_descriptor(prefPath);
if (descriptor) {
sActiveDescriptorPath = descriptor->path;
} else {
sActiveDescriptorPath.reset();
}
const auto dataPath =
resolve_data_path(prefPath, descriptor ? &descriptor->descriptor : nullptr);
sConfiguredDataPath = dataPath;
migrate_data(prefPath, dataPath, descriptor ? &descriptor->descriptor : nullptr);
ensure_data_directory(dataPath);
ensure_initial_pipeline_cache(dataPath);
return dataPath;
}
} // namespace dusk::data
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <filesystem>
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
#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
#if defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST
#define DUSK_CAN_CHANGE_DATA_FOLDER 0
#else
#define DUSK_CAN_CHANGE_DATA_FOLDER 1
#endif
namespace dusk::data {
std::filesystem::path initialize_data();
std::filesystem::path configured_data_path();
bool open_data_path();
bool set_custom_data_path(const char* path);
bool set_custom_data_path(const std::filesystem::path& path);
bool set_portable_data_path();
bool reset_data_path();
bool is_default_data_path();
bool is_data_path_restart_pending();
} // namespace dusk::data
+1 -1
View File
@@ -81,7 +81,7 @@ void update_presence() {
rpc::Presence presence{};
presence.startTimestamp = g_startTime;
presence.largeImageKey = "icon";
presence.largeImageText = "Dusk";
presence.largeImageText = "Dusklight";
if (IsGameLaunched) {
const char* stageName = dComIfGp_getLastPlayStageName();
+124 -6
View File
@@ -5,6 +5,7 @@
#include <SDL3/SDL_dialog.h>
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_init.h>
#include <SDL3/SDL_stdinc.h>
#if defined(__ANDROID__) || defined(ANDROID)
@@ -16,6 +17,12 @@
#include <TargetConditionals.h>
#endif
#if defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_TV && !TARGET_OS_MACCATALYST
#define USE_MACOS_FOLDER_DIALOG 1
#else
#define USE_MACOS_FOLDER_DIALOG 0
#endif
#if defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST
#define USE_IOS_DIALOG 1
#include "ios/FileSelectDialog.h"
@@ -23,6 +30,13 @@
#define USE_IOS_DIALOG 0
#endif
#if USE_MACOS_FOLDER_DIALOG
namespace dusk {
bool ShowMacOSFolderSelect(
FileCallback callback, void* userdata, SDL_Window* window, const char* default_location);
} // namespace dusk
#endif
namespace dusk {
namespace {
@@ -32,6 +46,10 @@ std::string fallback_display_name(std::string_view path) {
}
std::string pathString(path);
while (pathString.size() > 1 && (pathString.back() == '/' || pathString.back() == '\\')) {
pathString.pop_back();
}
const std::size_t slash = pathString.find_last_of("/\\");
if (slash == std::string::npos || slash + 1 >= pathString.size()) {
return pathString;
@@ -98,8 +116,7 @@ std::string android_display_name(std::string_view path) {
return {};
}
auto* displayName =
static_cast<jstring>(env->CallObjectMethod(activity, getDisplayName, uri));
auto* displayName = static_cast<jstring>(env->CallObjectMethod(activity, getDisplayName, uri));
env->DeleteLocalRef(uri);
env->DeleteLocalRef(activity);
if (displayName == nullptr || clear_pending_exception(env)) {
@@ -110,6 +127,76 @@ std::string android_display_name(std::string_view path) {
env->DeleteLocalRef(displayName);
return result;
}
struct AndroidFolderDialogState {
FileCallback callback;
void* userdata;
std::string path;
std::string error;
};
void onAndroidFolderDialogFinished(void* userdata) {
std::unique_ptr<AndroidFolderDialogState> state(
static_cast<AndroidFolderDialogState*>(userdata));
const char* path = state->path.empty() ? nullptr : state->path.c_str();
const char* error = state->error.empty() ? nullptr : state->error.c_str();
state->callback(state->userdata, path, error);
}
bool show_android_folder_select(AndroidFolderDialogState* state) {
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
if (env == nullptr) {
return false;
}
jobject activity = static_cast<jobject>(SDL_GetAndroidActivity());
if (activity == nullptr || clear_pending_exception(env)) {
if (activity != nullptr) {
env->DeleteLocalRef(activity);
}
return false;
}
jclass activityClass = env->GetObjectClass(activity);
if (activityClass == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(activity);
return false;
}
jmethodID showFolderDialog =
env->GetMethodID(activityClass, "showFolderDialog", "(J)Z");
env->DeleteLocalRef(activityClass);
if (showFolderDialog == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(activity);
return false;
}
const jboolean shown = env->CallBooleanMethod(
activity, showFolderDialog, reinterpret_cast<jlong>(state));
env->DeleteLocalRef(activity);
if (clear_pending_exception(env)) {
return false;
}
return shown == JNI_TRUE;
}
extern "C" JNIEXPORT void JNICALL
Java_dev_twilitrealm_dusk_DuskActivity_nativeFolderDialogResult(
JNIEnv* env, jclass, jlong userdata, jstring path, jstring error) {
auto* state = reinterpret_cast<AndroidFolderDialogState*>(userdata);
if (state == nullptr) {
return;
}
state->path = to_string(env, path);
state->error = to_string(env, error);
if (!SDL_RunOnMainThread(&onAndroidFolderDialogFinished, state, false)) {
onAndroidFolderDialogFinished(state);
}
}
#endif
#if USE_IOS_DIALOG
@@ -159,8 +246,8 @@ void onSDLDialogFinished(void* userdata, const char* const* filelist, [[maybe_un
} // namespace
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) {
if (callback == nullptr) {
return;
}
@@ -171,14 +258,45 @@ void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window,
state->userdata = userdata;
Dusk_iOS_ShowFileSelect(&onIOSDialogFinished, state.release(), window, filters, nfilters,
default_location, allow_many);
default_location, allow_many);
#else
auto state = std::make_unique<SDLDialogCallbackState>();
state->callback = callback;
state->userdata = userdata;
SDL_ShowOpenFileDialog(&onSDLDialogFinished, state.release(), window, filters, nfilters,
default_location, allow_many);
default_location, allow_many);
#endif
}
void ShowFolderSelect(
FileCallback callback, void* userdata, SDL_Window* window, const char* default_location) {
if (callback == nullptr) {
return;
}
#if USE_IOS_DIALOG
callback(userdata, nullptr, "Folder selection is not supported on this platform");
#elif USE_MACOS_FOLDER_DIALOG
ShowMacOSFolderSelect(callback, userdata, window, default_location);
#elif defined(__ANDROID__) || defined(ANDROID)
auto state = std::make_unique<AndroidFolderDialogState>();
state->callback = callback;
state->userdata = userdata;
if (show_android_folder_select(state.get())) {
state.release();
return;
}
callback(userdata, nullptr, "Folder selection is not supported on this platform");
#else
auto state = std::make_unique<SDLDialogCallbackState>();
state->callback = callback;
state->userdata = userdata;
SDL_ShowOpenFolderDialog(
&onSDLDialogFinished, state.release(), window, default_location, false);
#endif
}
+2
View File
@@ -14,6 +14,8 @@ using FileCallback = void (*)(void* userdata, const char* path, const char* erro
void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window,
const SDL_DialogFileFilter* filters, int nfilters, const char* default_location,
bool allow_many);
void ShowFolderSelect(
FileCallback callback, void* userdata, SDL_Window* window, const char* default_location);
std::string display_name_for_path(std::string_view path);
+102
View File
@@ -0,0 +1,102 @@
#include "file_select.hpp"
#include <SDL3/SDL_properties.h>
#include <SDL3/SDL_video.h>
#import <AppKit/AppKit.h>
namespace dusk {
namespace {
struct MacOSFolderDialogState {
FileCallback callback;
void* userdata;
};
void finish_folder_dialog(MacOSFolderDialogState* state, NSURL* url, const char* error) {
if (state == nullptr) {
return;
}
if (error != nullptr) {
state->callback(state->userdata, nullptr, error);
delete state;
return;
}
if (url == nil) {
state->callback(state->userdata, nullptr, nullptr);
delete state;
return;
}
state->callback(state->userdata, [[url path] UTF8String], nullptr);
delete state;
}
void configure_default_location(NSOpenPanel* panel, const char* defaultLocation) {
if (panel == nil || defaultLocation == nullptr || defaultLocation[0] == '\0') {
return;
}
NSString* path = [NSString stringWithUTF8String:defaultLocation];
if (path == nil) {
return;
}
BOOL isDirectory = NO;
NSFileManager* fileManager = [NSFileManager defaultManager];
NSURL* url = [NSURL fileURLWithPath:path];
if ([fileManager fileExistsAtPath:path isDirectory:&isDirectory] && isDirectory) {
[panel setDirectoryURL:url];
} else {
[panel setDirectoryURL:[url URLByDeletingLastPathComponent]];
}
}
NSWindow* window_for_sdl_window(SDL_Window* window) {
if (window == nullptr) {
return nil;
}
auto props = SDL_GetWindowProperties(window);
return (__bridge NSWindow*)SDL_GetPointerProperty(
props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr);
}
} // namespace
bool ShowMacOSFolderSelect(
FileCallback callback, void* userdata, SDL_Window* window, const char* defaultLocation) {
if (callback == nullptr) {
return false;
}
auto* state = new MacOSFolderDialogState{
.callback = callback,
.userdata = userdata,
};
NSOpenPanel* panel = [NSOpenPanel openPanel];
[panel setCanChooseFiles:NO];
[panel setCanChooseDirectories:YES];
[panel setAllowsMultipleSelection:NO];
[panel setCanCreateDirectories:YES];
configure_default_location(panel, defaultLocation);
NSWindow* modalWindow = window_for_sdl_window(window);
if (modalWindow != nil) {
[panel beginSheetModalForWindow:modalWindow
completionHandler:^(NSModalResponse result) {
finish_folder_dialog(
state, result == NSModalResponseOK ? [panel URL] : nil, nullptr);
}];
return true;
}
const NSModalResponse result = [panel runModal];
finish_folder_dialog(state, result == NSModalResponseOK ? [panel URL] : nil, nullptr);
return true;
}
} // namespace dusk
+4 -4
View File
@@ -140,8 +140,8 @@ void read(float dt) {
float my_rel = 0.0f;
SDL_GetRelativeMouseState(&mx_rel, &my_rel);
// Convert pixels to radians
s_pitch_rad = my_rel * kMousePixelToRad * getSettings().game.gyroSensitivityX;
s_yaw_rad = -mx_rel * kMousePixelToRad * getSettings().game.gyroSensitivityY;
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;
@@ -184,7 +184,7 @@ 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 * getSettings().game.gyroSensitivityX;
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;
@@ -223,7 +223,7 @@ void read(float dt) {
}
}
s_yaw_rad = horizontal_rate * dt * getSettings().game.gyroSensitivityY;
s_yaw_rad = horizontal_rate * dt * getSettings().game.gyroSensitivityX;
s_pitch_rad = getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad;
s_yaw_rad = getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad;

Some files were not shown because too many files have changed in this diff Show More