Compare commits

..

85 Commits

Author SHA1 Message Date
Luke Street 625f752fb9 Try Android CI 2026-05-10 00:26:00 -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
Howard Luck 65e8577253 frame interp: fix b&c chain links (#724) 2026-05-08 17:26:33 -06:00
Luke Street a4fcc10f5f Make version logic tolerate prerelease semver 2026-05-08 17:25:09 -06:00
Luke Street a2c2988666 Disable Sentry in CI builds 2026-05-08 17:12:58 -06:00
Irastris abec043249 Add mouse as a gyro input source (#720)
* Add mouse as a gyro input source

* Revisions

* Grammar
2026-05-08 17:11:13 -06:00
SuperDude88 699d069b0a Fix Letter Menu Page Numbers (#722)
This bug comes from `getBounds()` returning a reference to a static variable -- the compiler can load the float right away or later (after the second call), so the value it is referencing may have changed. If it waits to load the first operand, the subtraction results in 0. Further discussion at https://discord.com/channels/1446645861529550869/1446648842387722411/1502415404386091118.

Resolves #719
2026-05-08 17:10:37 -06:00
Tom Lube 4d67033ff8 Remove reset via CTRL + R 2026-05-08 17:08:03 -06:00
Irastris 84ffd67622 Refactor notification settings, allow disabling controller toasts (#721)
* Refactor notification settings, allow disabling controller toasts

* "Toasts" to "Notifications"
2026-05-08 17:01:19 -06:00
Luke Street 1c85ee63eb Many mobile fixes, Android update check, "Background Input" & "Pause On Focus Lost" options 2026-05-08 16:57:33 -06:00
qwertyquerty 81c7213993 Merge pull request #599 from TwilitRealm/50_achievements
More Achievements
2026-05-08 12:27:25 -07:00
madeline be82e606b2 45 achievements 2026-05-08 11:20:17 -07:00
MelonSpeedruns 44da1a9f7d Added sounds to Reset & Quit modals (#718)
Co-authored-by: MelonSpeedruns <melonspeedruns@stratobox.net>
2026-05-08 11:51:13 -06:00
doop d0f8ea56f9 Interpolate more things (#699)
* Interpolate trim height

* Interpolate (some) fades
2026-05-08 11:50:44 -06:00
Howard Luck e472b36cef Merge pull request #717 from TwilitRealm/fix/super_clawshot 2026-05-08 10:26:46 -06:00
MelonSpeedruns 3934e09c8f remove mention of chains not rendering 2026-05-08 12:07:00 -04:00
MelonSpeedruns 3136816ce9 Merge remote-tracking branch 'origin/main' into fix/super_clawshot 2026-05-08 12:06:47 -04:00
MelonSpeedruns 6fd3762ffc optimized code 2026-05-08 12:06:38 -04:00
Pheenoh 97a1190713 set max chain links to 600 2026-05-08 08:59:13 -06:00
MelonSpeedruns 73a3bd9ae8 super clawshot warning about chains (#716)
Co-authored-by: MelonSpeedruns <melonspeedruns@stratobox.net>
2026-05-08 08:53:01 -06:00
Luke Street fc533dbdc7 UI: Add update checks (#715) 2026-05-08 08:52:36 -06:00
madeline 6217e071d2 Merge branch '50_achievements' of https://github.com/TakaRikka/dusk into 50_achievements 2026-05-08 05:17:48 -07:00
Luke Street 673ca7f686 Update Linux icons & .desktop
Resolves #714
2026-05-08 01:23:53 -06:00
CraftyBoss 29a1cff7ea update aurora, add comment explaining king bulblin 1 strange behavior
the game saves a flag when the player hits king bulblin at least once, which makes subsequent loads (including king bulblin 2) have 1 less "health" as lap_num for the boar is set to 1 during creation, as it checks the flag.
2026-05-08 00:05:16 -07:00
Luke Street 3c5152a67b Disable autosave in speedrun mode 2026-05-08 00:46:02 -06:00
MelonSpeedruns 1c1a849095 Recording Mode & Enable Autosave in Dusk preset (#685)
* branch for recording without a HUD

* no more black bars thanks maddie

* Backslash hotkey

* mute music

* dont check for null twice

* renamed option to recording mode, and added it to the rmlui menu

* Move recording mode to Interface

* re-added & renamed minimal hud option

* Mute all BGM if recording mode is on

* Update Interface section

* Un-experimentalize Autosave & enable in Dusk preset

---------

Co-authored-by: MelonSpeedruns <melonspeedruns@stratobox.net>
Co-authored-by: Irastris <irastris15@gmail.com>
Co-authored-by: Luke Street <luke@street.dev>
2026-05-07 23:26:22 -06:00
Phillip Stephens 89d393e57a Add missing climits include 2026-05-07 22:24:31 -07:00
Phillip Stephens 2f27687d80 Fix OSReport strings corruption with non-trivial format (#709)
* Fix OSReport strings corruption with non-trivial format

* Limit format attempts to 3
2026-05-07 23:10:23 -06:00
SuperDude88 167a50c01d Deselect Sub-Menu On Focus Change (#710)
- Unselect any previous items when focusing a new one

This might be way over-inclusive so it should be scrutinized harshly (but fixes #672)
2026-05-07 23:09:25 -06:00
SuperDude88 313f03f5e5 Dim Carousel Arrows
- Dim carousel arrows if there are no further options in that direction (closes #708)

This sets the color property constantly, not sure if that is a concern for perf (would imagine not since it already sets the rml to mValueElem every frame also?)
2026-05-07 23:05:08 -06:00
Howard Luck 6cfdc3d8a3 add keyboard controls + roll support to fly cam (#705) 2026-05-07 23:04:29 -06:00
Luke Street 9c24a0bc4b Disable Development Mode & move Input Viewer
Resolves #712
2026-05-07 23:03:26 -06:00
TakaRikka b99ad920c4 Merge pull request #704 from TwilitRealm/ira/rmlui-confirmation
Add confirmations modals to Reset and Quit
2026-05-07 13:57:03 -07:00
TakaRikka 912b18eca1 Merge pull request #703 from TwilitRealm/ui/advanced-settings
Advanced Settings
2026-05-07 13:55:13 -07:00
TakaRikka 9e651a51db Merge pull request #700 from TwilitRealm/ira/rmlui-fps
Add FPS counter to RmlUi
2026-05-07 13:51:51 -07:00
TakaRikka c8b6e997a7 Merge pull request #702 from TwilitRealm/ira/rmlui-inputconfig
Add remaining controller config options to RmlUi
2026-05-07 13:45:35 -07:00
Irastris 928e187524 Add mobile layout for prelaunch 2026-05-07 15:57:52 -04:00
Irastris cf12d19860 Resolve RmlUi deprecation warning, add L1/R1 tabbing to MenuBar 2026-05-07 13:48:12 -04:00
Irastris e3a3ac56fb Add confirmations modals to Reset and Quit 2026-05-07 13:32:59 -04:00
MelonSpeedruns d15ed81cd5 Only show Editor if Advanced Settings is enabled 2026-05-07 12:39:37 -04:00
TakaRikka ff054f6f47 update aurora and enable MTX_USE_PS 2026-05-07 09:36:32 -07:00
MelonSpeedruns 667cf70fa0 Add Advanced Settings option & Disable debug hotkeys if it's off 2026-05-07 12:01:32 -04:00
Irastris 73eb401c93 Add remaining controller config options, remove ImGui config 2026-05-07 12:00:35 -04:00
Irastris b11f3add06 Don't immediately dismiss prelaunch if skip config is toggled
Prevents a situation where memcard is not init until restarting after the deferral change
2026-05-07 09:26:41 -04:00
Irastris 92f888a152 Remove blur and shadow from counter, consolidate options 2026-05-07 03:28:30 -04:00
Irastris 78ed5cc716 Add FPS counter 2026-05-07 03:13:08 -04:00
madeline 44f3828f68 fix morpheel logic 2026-04-30 02:48:30 -07:00
madeline 3f560b060c 40 achievements 2026-04-30 01:48:03 -07:00
140 changed files with 5993 additions and 1194 deletions
+80 -20
View File
@@ -8,9 +8,9 @@ on:
pull_request: pull_request:
env: env:
# SCCACHE_GHA_ENABLED: "true" SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: "sccache" RUSTC_WRAPPER: "sccache"
SENTRY_DSN: ${{ secrets.SENTRY_DSN }} # SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
jobs: jobs:
build-linux: build-linux:
@@ -22,7 +22,7 @@ jobs:
matrix: matrix:
include: include:
- name: GCC x86_64 - name: GCC x86_64
runner: [self-hosted, Linux] runner: ubuntu-latest
preset: gcc preset: gcc
artifact_arch: x86_64 artifact_arch: x86_64
# - name: GCC aarch64 # - name: GCC aarch64
@@ -41,7 +41,6 @@ jobs:
submodules: recursive submodules: recursive
- name: Install dependencies - name: Install dependencies
if: 'false' # disabled for self-hosted
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get -y install ninja-build clang lld openssl libcurl4-openssl-dev \ sudo apt-get -y install ninja-build clang lld openssl libcurl4-openssl-dev \
@@ -51,7 +50,6 @@ jobs:
libxss-dev libfuse2 libusb-1.0-0-dev libdecor-0-dev libpipewire-0.3-dev libunwind-dev libxss-dev libfuse2 libusb-1.0-0-dev libdecor-0-dev libpipewire-0.3-dev libunwind-dev
- name: Setup sccache - name: Setup sccache
if: 'false' # disabled for self-hosted
uses: mozilla-actions/sccache-action@v0.0.9 uses: mozilla-actions/sccache-action@v0.0.9
- name: Print sccache stats - name: Print sccache stats
@@ -67,7 +65,6 @@ jobs:
run: ci/build-appimage.sh run: ci/build-appimage.sh
- name: Upload artifacts - name: Upload artifacts
if: startsWith(github.event.ref, 'refs/tags/v')
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: dusk-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}} name: dusk-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}}
@@ -77,7 +74,7 @@ jobs:
build-apple: build-apple:
name: Build Apple (${{matrix.name}}) name: Build Apple (${{matrix.name}})
runs-on: [self-hosted, macOS] runs-on: macos-latest
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -86,14 +83,14 @@ jobs:
platform: macos platform: macos
preset: x-macos-ci-arm64 preset: x-macos-ci-arm64
artifact_name: macos-appleclang-arm64 artifact_name: macos-appleclang-arm64
# - name: AppleClang macOS x86_64 - name: AppleClang macOS x86_64
# platform: macos platform: macos
# preset: x-macos-ci-x86_64 preset: x-macos-ci-x86_64
# artifact_name: macos-appleclang-x86_64 artifact_name: macos-appleclang-x86_64
# - name: AppleClang iOS arm64 - name: AppleClang iOS arm64
# platform: ios platform: ios
# preset: x-ios-ci preset: x-ios-ci
# artifact_name: ios-appleclang-arm64 artifact_name: ios-appleclang-arm64
# - name: AppleClang tvOS arm64 # - name: AppleClang tvOS arm64
# platform: tvos # platform: tvos
# preset: x-tvos-ci # preset: x-tvos-ci
@@ -106,7 +103,6 @@ jobs:
submodules: recursive submodules: recursive
- name: Install dependencies - name: Install dependencies
if: 'false'
run: brew install cmake ninja run: brew install cmake ninja
- name: Install Rust iOS target - name: Install Rust iOS target
@@ -137,7 +133,6 @@ jobs:
run: cmake --build --preset ${{matrix.preset}} run: cmake --build --preset ${{matrix.preset}}
- name: Upload artifacts - name: Upload artifacts
if: startsWith(github.event.ref, 'refs/tags/v')
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: dusk-${{env.DUSK_VERSION}}-${{matrix.artifact_name}} name: dusk-${{env.DUSK_VERSION}}-${{matrix.artifact_name}}
@@ -145,6 +140,73 @@ jobs:
build/install/Dusk.app build/install/Dusk.app
build/install/debug.tar.* 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@v3
- 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.9
- name: Configure CMake
run: cmake --preset ${{matrix.preset}}
- name: Build native library
run: cmake --build --preset ${{matrix.preset}} --target dusk
- name: Stage stripped JNI library
run: ANDROID_STAGE_ABIS="${{matrix.abi}}" platforms/android/scripts/stage-jni-libs.sh
- name: Build APK
working-directory: platforms/android
run: ./gradlew :app:assembleRelease --rerun-tasks
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: dusk-${{env.DUSK_VERSION}}-android-${{matrix.artifact_arch}}
path: platforms/android/app/build/outputs/apk/release/app-${{matrix.abi}}-release-unsigned.apk
build-windows: build-windows:
name: Build Windows (${{matrix.name}}) name: Build Windows (${{matrix.name}})
runs-on: ${{matrix.runner}} runs-on: ${{matrix.runner}}
@@ -154,7 +216,7 @@ jobs:
matrix: matrix:
include: include:
- name: MSVC x86_64 - name: MSVC x86_64
runner: [self-hosted, Windows] runner: windows-latest
preset: msvc preset: msvc
msvc_arch: amd64 msvc_arch: amd64
vcpkg_arch: x64 vcpkg_arch: x64
@@ -191,7 +253,6 @@ jobs:
uses: mozilla-actions/sccache-action@v0.0.9 uses: mozilla-actions/sccache-action@v0.0.9
- name: Install dependencies - name: Install dependencies
if: 'false' # disabled for self-hosted
run: | run: |
choco install ninja choco install ninja
vcpkg install freetype:${{matrix.vcpkg_arch}}-windows-static zstd:${{matrix.vcpkg_arch}}-windows-static vcpkg install freetype:${{matrix.vcpkg_arch}}-windows-static zstd:${{matrix.vcpkg_arch}}-windows-static
@@ -203,7 +264,6 @@ jobs:
run: cmake --build --preset x-windows-ci-${{matrix.preset}} run: cmake --build --preset x-windows-ci-${{matrix.preset}}
- name: Upload artifacts - name: Upload artifacts
if: startsWith(github.event.ref, 'refs/tags/v')
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7
with: with:
name: dusk-${{env.DUSK_VERSION}}-win32-msvc-${{matrix.artifact_arch}} name: dusk-${{env.DUSK_VERSION}}-win32-msvc-${{matrix.artifact_arch}}
+58 -22
View File
@@ -48,13 +48,15 @@ else ()
message(STATUS "Unable to find git, commit information will not be available") message(STATUS "Unable to find git, commit information will not be available")
endif () endif ()
if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)(-([0-9]+).*)?$") if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$")
set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}") set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
if (CMAKE_MATCH_5) set(DUSK_VERSION_TWEAK "0")
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${CMAKE_MATCH_5}") if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$")
else () set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.0") elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$")
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
endif () endif ()
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}")
else () else ()
set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION") set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION")
set(DUSK_VERSION_STRING "0.0.0.0") set(DUSK_VERSION_STRING "0.0.0.0")
@@ -69,7 +71,7 @@ message(STATUS "Dusk version set to ${DUSK_WC_DESCRIBE}")
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
project(dusk LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING}) project(dusk LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING})
if (APPLE) if (APPLE)
enable_language(OBJC) enable_language(OBJC OBJCXX)
endif () endif ()
if (APPLE AND NOT TVOS AND CMAKE_SYSTEM_NAME STREQUAL tvOS) if (APPLE AND NOT TVOS AND CMAKE_SYSTEM_NAME STREQUAL tvOS)
# ios.toolchain.cmake hack for SDL # ios.toolchain.cmake hack for SDL
@@ -102,25 +104,33 @@ set(AURORA_ENABLE_DVD ON CACHE BOOL "Enable DVD API support" FORCE)
set(AURORA_ENABLE_CARD ON CACHE BOOL "Enable CARD API support" FORCE) set(AURORA_ENABLE_CARD ON CACHE BOOL "Enable CARD API support" FORCE)
set(AURORA_ENABLE_RMLUI ON CACHE BOOL "Enable RmlUi UI support" FORCE) set(AURORA_ENABLE_RMLUI ON CACHE BOOL "Enable RmlUi UI support" FORCE)
add_subdirectory(extern/aurora EXCLUDE_FROM_ALL) add_subdirectory(extern/aurora EXCLUDE_FROM_ALL)
target_compile_definitions(aurora_mtx PRIVATE MTX_USE_PS=1)
add_subdirectory(libs/freeverb) add_subdirectory(libs/freeverb)
option(DUSK_BUILD_WARNINGS "Enable compiler warnings (off by default)") option(DUSK_BUILD_WARNINGS "Enable compiler warnings (off by default)")
option(DUSK_SELECTED_OPT "If on, selected parts of the project will be compiled with optimizations on Debug, intending to make the game run at 30 FPS. Note for MSVC: you will need to remove '/RTC1' from your debug flags in CMake.") option(DUSK_SELECTED_OPT "If on, selected parts of the project will be compiled with optimizations on Debug, intending to make the game run at 30 FPS. Note for MSVC: you will need to remove '/RTC1' from your debug flags in CMake.")
option(DUSK_MOVIE_SUPPORT "If on, compile against libjpeg-turbo to enable THP file decoding" ON) option(DUSK_MOVIE_SUPPORT "If on, compile against libjpeg-turbo to enable THP file decoding" ON)
option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking support" ON)
if(ANDROID) if(ANDROID)
set(DUSK_MOVIE_SUPPORT OFF) 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 () endif ()
option(DUSK_ENABLE_SENTRY_NATIVE "Enable sentry-native crash reporting support" OFF) option(DUSK_ENABLE_SENTRY_NATIVE "Enable sentry-native crash reporting support" OFF)
set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN") set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN")
set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment") 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) if (DUSK_MOVIE_SUPPORT)
find_package(libjpeg-turbo 3.0 CONFIG QUIET) find_package(libjpeg-turbo 3.0 CONFIG QUIET)
if (libjpeg-turbo_FOUND) if (libjpeg-turbo_FOUND)
@@ -150,6 +160,7 @@ if (DUSK_MOVIE_SUPPORT)
CMAKE_C_COMPILER_LAUNCHER CMAKE_C_COMPILER_LAUNCHER
CMAKE_MAKE_PROGRAM CMAKE_MAKE_PROGRAM
CMAKE_MSVC_RUNTIME_LIBRARY CMAKE_MSVC_RUNTIME_LIBRARY
CMAKE_MSVC_DEBUG_INFORMATION_FORMAT
CMAKE_OSX_ARCHITECTURES CMAKE_OSX_ARCHITECTURES
DEPLOYMENT_TARGET DEPLOYMENT_TARGET
ENABLE_ARC ENABLE_ARC
@@ -283,9 +294,9 @@ set(DUSK_PRODUCT_NAME "Dusk")
set(DUSK_COPYRIGHT "Copyright (C) Twilit Realm contributors") set(DUSK_COPYRIGHT "Copyright (C) Twilit Realm contributors")
source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${REL_FILES}) source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${REL_FILES})
source_group("dusk" FILES ${DUSK_FILES}) source_group("dusk" FILES ${DUSK_FILES} ${DUSK_HTTP_BACKEND_FILES})
set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0) set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 MTX_USE_PS=1)
set(GAME_INCLUDE_DIRS set(GAME_INCLUDE_DIRS
include include
@@ -313,6 +324,41 @@ if (WIN32)
list(APPEND GAME_LIBS Ws2_32) list(APPEND GAME_LIBS Ws2_32)
endif () endif ()
set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/no_backend.cpp)
if (DUSK_ENABLE_UPDATE_CHECKER)
list(APPEND GAME_COMPILE_DEFS DUSK_ENABLE_UPDATE_CHECKER=1)
if (WIN32)
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)")
elseif (ANDROID)
set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/android.cpp)
list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_ANDROID=1)
message(STATUS "dusk: Enabled update checker (Android)")
elseif (APPLE)
find_library(FOUNDATION_FRAMEWORK Foundation REQUIRED)
set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/url_session.mm)
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)")
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)")
else ()
message(STATUS "dusk: Disabled update checker (libcurl + HTTPS/SSL not found)")
endif ()
else ()
message(STATUS "dusk: Disabled update checker (unsupported platform)")
endif ()
endif ()
list(APPEND DUSK_FILES ${DUSK_HTTP_BACKEND_SOURCE})
if (DUSK_MOVIE_SUPPORT) if (DUSK_MOVIE_SUPPORT)
if (TARGET libjpeg-turbo::turbojpeg-static) if (TARGET libjpeg-turbo::turbojpeg-static)
list(APPEND GAME_LIBS libjpeg-turbo::turbojpeg-static) list(APPEND GAME_LIBS libjpeg-turbo::turbojpeg-static)
@@ -331,16 +377,6 @@ if (DUSK_ENABLE_DISCORD AND NOT ANDROID AND NOT IOS AND NOT TVOS)
list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD=1) list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD=1)
endif () 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) if(ANDROID)
list(APPEND GAME_COMPILE_DEFS TARGET_ANDROID=1) list(APPEND GAME_COMPILE_DEFS TARGET_ANDROID=1)
endif () endif ()
+30 -1
View File
@@ -30,7 +30,7 @@
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache", "CMAKE_CXX_COMPILER_LAUNCHER": "sccache",
"DUSK_ENABLE_SENTRY_NATIVE": { "DUSK_ENABLE_SENTRY_NATIVE": {
"type": "BOOL", "type": "BOOL",
"value": true "value": false
}, },
"DUSK_SENTRY_DSN": "$env{SENTRY_DSN}", "DUSK_SENTRY_DSN": "$env{SENTRY_DSN}",
"DUSK_SENTRY_ENVIRONMENT": "production" "DUSK_SENTRY_ENVIRONMENT": "production"
@@ -352,6 +352,25 @@
"ANDROID_ABI": "x86_64" "ANDROID_ABI": "x86_64"
} }
}, },
{
"name": "x-android-ci",
"hidden": true,
"inherits": [
"android-base",
"ci"
]
},
{
"name": "x-android-ci-arm64",
"binaryDir": "${sourceDir}/build/android-arm64",
"inherits": [
"x-android-ci"
],
"cacheVariables": {
"ANDROID_ABI": "arm64-v8a",
"Rust_CARGO_TARGET": "aarch64-linux-android"
}
},
{ {
"name": "x-linux-ci", "name": "x-linux-ci",
"hidden": true, "hidden": true,
@@ -412,6 +431,7 @@
"x-macos-ci" "x-macos-ci"
], ],
"cacheVariables": { "cacheVariables": {
"AURORA_DAWN_PROVIDER": "vendor",
"CMAKE_OSX_ARCHITECTURES": "x86_64", "CMAKE_OSX_ARCHITECTURES": "x86_64",
"Rust_CARGO_TARGET": "x86_64-apple-darwin" "Rust_CARGO_TARGET": "x86_64-apple-darwin"
} }
@@ -555,6 +575,15 @@
"dusk" "dusk"
] ]
}, },
{
"name": "x-android-ci-arm64",
"configurePreset": "x-android-ci-arm64",
"description": "(Internal) Android CI arm64-v8a",
"displayName": "(Internal) Android CI arm64-v8a",
"targets": [
"dusk"
]
},
{ {
"name": "windows-msvc-debug", "name": "windows-msvc-debug",
"configurePreset": "windows-msvc-debug", "configurePreset": "windows-msvc-debug",
+14 -3
View File
@@ -4,7 +4,7 @@
<p align="center"> <p align="center">
<a href="https://twilitrealm.dev">Official Website</a> <a href="https://twilitrealm.dev">Official Website</a>
<a href="https://discord.gg/QACynxeyna">Discord</a> <a href="https://discord.gg/dusktp">Discord</a>
</p> </p>
</div> </div>
@@ -28,13 +28,24 @@ First, make sure your dump of the game is clean and supported by Dusk. You can d
| GameCube USA | `75edd3ddff41f125d1b4ce1a40378f1b565519e7` | | GameCube USA | `75edd3ddff41f125d1b4ce1a40378f1b565519e7` |
| GameCube EUR | `2601822a488eeb86fb89db16ca8f29c2c953e1ca` | | GameCube EUR | `2601822a488eeb86fb89db16ca8f29c2c953e1ca` |
*Support for other versions of the game is planned in the future.
### 2. Download [Dusk](https://github.com/TwilitRealm/dusk/releases) ### 2. Download [Dusk](https://github.com/TwilitRealm/dusk/releases)
### 3. Setup the game ### 3. Setup the game
**Windows / macOS / Linux**
- Extract the .zip file - Extract the .zip file
- Launch Dusk - Launch Dusk
- Press **Select Disc Image** and provide the path to your supported game dump. - Press **Select Disc Image** and provide the path to your supported game dump
- Press **Play**!
**iOS**
- Follow the [iOS setup guide](docs/ios-install-altstore.md)
**Android**
- Install the Dusk apk
- Launch Dusk
- Press **Select Disc Image** and provide the path to your supported game dump
- Press **Play**! - Press **Play**!
# Building # Building
+46
View File
@@ -0,0 +1,46 @@
# Installing Dusk on iOS via AltStore
## Prerequisites
- Mac with Homebrew installed
- iPhone connected via USB
- Dusk IPA file (download the latest `Dusk-vX.X.X-ios-arm64.ipa` from the [releases page](https://github.com/TwilitRealm/dusk/releases))
- Game disc - `GZ2E01` (Gamecube USA) or `GZ2PE01` (Gamecube PAL)
## 1. Install AltServer
```sh
brew install altserver
open -a AltServer
```
AltServer will appear in your menu bar.
## 2. Enable Developer Mode (iOS 16+)
- On your iPhone, go to **Settings > Privacy & Security > Developer Mode**
- Toggle it on and restart when prompted
## 3. Install AltStore on Your iPhone
- Click AltServer in the menu bar
- Click **Install AltStore > [Your iPhone]**
- Enter your Apple ID credentials when prompted
- On your iPhone, go to **Settings > General > VPN & Device Management**
- Tap your Apple ID under "Developer App" and tap **Trust**
## 4. Copy Files to Your iPhone
Transfer the IPA and game disc to your iPhone so they're accessible in the Files app. A few ways to do this:
- **AirDrop** - Right-click the files on your Mac and choose Share > AirDrop
- **iCloud Drive** - Place files in iCloud Drive on your Mac and they'll sync to Files on your iPhone
- **USB transfer** - Connect your iPhone and drag files via Finder's sidebar
- **Cloud storage** - Upload to Google Drive, Dropbox, etc. and download on your iPhone
## 5. Install via AltStore
- Open **AltStore** on your iPhone
- Go to the **My Apps** tab
- Tap the **+** button (top left)
- Open the **Files** app and select the `.ipa` file
+1 -1
+10
View File
@@ -1430,11 +1430,14 @@ set(DUSK_FILES
src/dusk/gyro.cpp src/dusk/gyro.cpp
src/dusk/gamepad_color.cpp src/dusk/gamepad_color.cpp
src/dusk/autosave.cpp src/dusk/autosave.cpp
src/dusk/http/http.hpp
src/dusk/io.cpp src/dusk/io.cpp
src/dusk/layout.cpp src/dusk/layout.cpp
src/dusk/logging.cpp src/dusk/logging.cpp
src/dusk/settings.cpp src/dusk/settings.cpp
src/dusk/stubs.cpp src/dusk/stubs.cpp
src/dusk/update_check.cpp
src/dusk/update_check.hpp
#src/dusk/m_Do_ext_dusk.cpp #src/dusk/m_Do_ext_dusk.cpp
src/dusk/imgui/ImGuiConfig.hpp src/dusk/imgui/ImGuiConfig.hpp
src/dusk/imgui/ImGuiConsole.hpp src/dusk/imgui/ImGuiConsole.hpp
@@ -1516,3 +1519,10 @@ set(DUSK_FILES
src/dusk/discord_presence.cpp src/dusk/discord_presence.cpp
src/dusk/version.cpp src/dusk/version.cpp
) )
set(DUSK_HTTP_BACKEND_FILES
src/dusk/http/no_backend.cpp
src/dusk/http/curl.cpp
src/dusk/http/winhttp.cpp
src/dusk/http/url_session.mm
)
+12
View File
@@ -4552,6 +4552,18 @@ public:
void handleWolfHowl(); void handleWolfHowl();
void handleQuickTransform(); void handleQuickTransform();
bool checkGyroAimContext(); bool checkGyroAimContext();
void onIronBallChainInterpCallback();
static const int IRON_BALL_CHAIN_COUNT = 102;
cXyz mIBChainInterpPrevPos[IRON_BALL_CHAIN_COUNT];
cXyz mIBChainInterpCurrPos[IRON_BALL_CHAIN_COUNT];
csXyz mIBChainInterpPrevAngle[IRON_BALL_CHAIN_COUNT];
csXyz mIBChainInterpCurrAngle[IRON_BALL_CHAIN_COUNT];
cXyz mIBChainInterpPrevHandRoot;
cXyz mIBChainInterpCurrHandRoot;
bool mIBChainInterpPrevValid;
bool mIBChainInterpCurrValid;
#endif #endif
}; // Size: 0x385C }; // Size: 0x385C
+6
View File
@@ -80,6 +80,12 @@ public:
/* 0x125C */ u32 field_0x125c; /* 0x125C */ u32 field_0x125c;
/* 0x1260 */ u8 field_0x1260[0x126C - 0x1260]; /* 0x1260 */ u8 field_0x1260[0x126C - 0x1260];
/* 0x126C */ u8 HIOInit; /* 0x126C */ u8 HIOInit;
#if TARGET_PC
cXyz mStalkLineInterpPrev[12];
cXyz mStalkLineInterpCurr[12];
bool mStalkLineInterpPrevValid;
bool mStalkLineInterpCurrValid;
#endif
}; };
STATIC_ASSERT(sizeof(e_db_class) == 0x1270); STATIC_ASSERT(sizeof(e_db_class) == 0x1270);
+6
View File
@@ -73,6 +73,12 @@ public:
/* 0x124C */ f32 field_0x124c; /* 0x124C */ f32 field_0x124c;
/* 0x1250 */ u8 field_0x1250[0x1264 - 0x1250]; /* 0x1250 */ u8 field_0x1250[0x1264 - 0x1250];
/* 0x1264 */ u8 HIOInit; /* 0x1264 */ u8 HIOInit;
#if TARGET_PC
cXyz mStalkLineInterpPrev[12];
cXyz mStalkLineInterpCurr[12];
bool mStalkLineInterpPrevValid;
bool mStalkLineInterpCurrValid;
#endif
}; };
STATIC_ASSERT(sizeof(e_hb_class) == 0x1268); STATIC_ASSERT(sizeof(e_hb_class) == 0x1268);
+9
View File
@@ -81,6 +81,15 @@ public:
/* 0x306D */ u8 field_0x306D[0x307C - 0x306D]; /* 0x306D */ u8 field_0x306D[0x307C - 0x306D];
/* 0x307C */ u32 mBodyEffEmtrID; /* 0x307C */ u32 mBodyEffEmtrID;
/* 0x3080 */ u8 mInitHIO; /* 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); STATIC_ASSERT(sizeof(e_s1_class) == 0x3084);
+6
View File
@@ -74,6 +74,12 @@ public:
/* 0x1250 */ f32 field_0x1250; /* 0x1250 */ f32 field_0x1250;
/* 0x1254 */ u8 field_0x1254[0x1268 - 0x1254]; /* 0x1254 */ u8 field_0x1254[0x1268 - 0x1254];
/* 0x1268 */ u8 field_0x1268; /* 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); STATIC_ASSERT(sizeof(e_yd_class) == 0x126c);
+9
View File
@@ -63,6 +63,15 @@ public:
/* 0x0BB4 */ yg_ke_s mYgKes[13]; /* 0x0BB4 */ yg_ke_s mYgKes[13];
/* 0x1880 */ mDoExt_3DlineMat0_c mLineMat; /* 0x1880 */ mDoExt_3DlineMat0_c mLineMat;
/* 0x189C */ u8 mIsFirstSpawn; /* 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); STATIC_ASSERT(sizeof(e_yg_class) == 0x18a0);
+6
View File
@@ -77,6 +77,12 @@ public:
/* 0x1260 */ u32 field_0x1260; /* 0x1260 */ u32 field_0x1260;
/* 0x1260 */ u8 field_0x1264[0x1270 - 0x1264]; /* 0x1260 */ u8 field_0x1264[0x1270 - 0x1264];
/* 0x1270 */ bool mIsHIOOwner; /* 0x1270 */ bool mIsHIOOwner;
#if TARGET_PC
cXyz mLineInterpPrev[12];
cXyz mLineInterpCurr[12];
bool mLineInterpPrevValid;
bool mLineInterpCurrValid;
#endif
}; };
STATIC_ASSERT(sizeof(e_yh_class) == 0x1274); STATIC_ASSERT(sizeof(e_yh_class) == 0x1274);
+12
View File
@@ -31,6 +31,10 @@ public:
csXyz* getAngle() { return field_0x8a4; } csXyz* getAngle() { return field_0x8a4; }
J3DModelData* getModelData() { return mModelData; } J3DModelData* getModelData() { return mModelData; }
#if TARGET_PC
void onInterpCallback();
#endif
private: private:
/* 0x568 */ request_of_phase_process_class mPhase; /* 0x568 */ request_of_phase_process_class mPhase;
/* 0x570 */ J3DModelData* mModelData; /* 0x570 */ J3DModelData* mModelData;
@@ -42,6 +46,14 @@ private:
/* 0x694 */ cXyz field_0x694[22]; /* 0x694 */ cXyz field_0x694[22];
/* 0x79C */ cXyz field_0x79c[22]; /* 0x79C */ cXyz field_0x79c[22];
/* 0x8A4 */ csXyz field_0x8a4[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); STATIC_ASSERT(sizeof(daObjFchain_c) == 0x928);
+6
View File
@@ -1845,6 +1845,12 @@ inline void dComIfGs_addDeathCount() {
g_dComIfG_gameInfo.info.getPlayer().getPlayerInfo().addDeathCount(); g_dComIfG_gameInfo.info.getPlayer().getPlayerInfo().addDeathCount();
} }
#if TARGET_PC
inline u16 dComIfGs_getDeathCount() {
return g_dComIfG_gameInfo.info.getPlayer().getPlayerInfo().getDeathCount();
}
#endif
inline char* dComIfGs_getPlayerName() { inline char* dComIfGs_getPlayerName() {
return g_dComIfG_gameInfo.info.getPlayer().getPlayerInfo().getPlayerName(); return g_dComIfG_gameInfo.info.getPlayer().getPlayerInfo().getPlayerName();
} }
+3
View File
@@ -486,6 +486,9 @@ public:
mDeathCount++; mDeathCount++;
} }
} }
#if TARGET_PC
u16 getDeathCount() const { return mDeathCount; }
#endif
char* getPlayerName() const { return const_cast<char*>(mPlayerName); } char* getPlayerName() const { return const_cast<char*>(mPlayerName); }
void setPlayerName(const char* i_name) { void setPlayerName(const char* i_name) {
#if AVOID_UB #if AVOID_UB
+1 -2
View File
@@ -12,9 +12,8 @@
namespace dusk { namespace dusk {
enum class AchievementCategory : uint8_t { enum class AchievementCategory : uint8_t {
Story,
Collection,
Challenge, Challenge,
Collection,
Minigame, Minigame,
Misc, Misc,
Glitched Glitched
+1
View File
@@ -12,6 +12,7 @@ void rollgoalTableOffset(s16& out_ax, s16& out_az);
extern bool s_sensor_keep_alive; extern bool s_sensor_keep_alive;
bool get_sensor_keep_alive(); bool get_sensor_keep_alive();
void set_sensor_keep_alive(bool value); void set_sensor_keep_alive(bool value);
bool rollgoal_gyro_enabled();
} // namespace dusk::gyro } // namespace dusk::gyro
#endif #endif
+28 -2
View File
@@ -2,11 +2,13 @@
#define DUSK_IO_HPP #define DUSK_IO_HPP
#include <vector> #include <vector>
#include <filesystem>
// I can't believe it's 2026 and neither SDL (no error codes) nor // I can't believe it's 2026 and neither SDL (no error codes) nor
// C++ (no error codes) have a file system API functional enough for me to use. // C++ (no error codes) have a file system API functional enough for me to use.
// Here you go, this one's inspired by C#. I only wrote the functions I need. // Here you go, this one's inspired by C#. I only wrote the functions I need.
namespace dusk::io { namespace dusk::io {
/** /**
@@ -15,7 +17,7 @@ namespace dusk::io {
* Methods on this class throw appropriate C++ exceptions when an error occurs. * Methods on this class throw appropriate C++ exceptions when an error occurs.
*/ */
class FileStream { class FileStream {
void* file; FILE* file;
public: public:
FileStream() noexcept; FileStream() noexcept;
@@ -23,7 +25,7 @@ public:
/** /**
* \brief Take ownership of a FILE* handle. * \brief Take ownership of a FILE* handle.
*/ */
explicit FileStream(void* file); explicit FileStream(FILE* file);
FileStream(const FileStream& other) = delete; FileStream(const FileStream& other) = delete;
FileStream(FileStream&& other) noexcept; FileStream(FileStream&& other) noexcept;
@@ -34,6 +36,11 @@ public:
*/ */
static FileStream OpenRead(const char* utf8Path); 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. * \brief Create a file for writing.
* *
@@ -41,16 +48,33 @@ public:
*/ */
static FileStream Create(const char* utf8Path); 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. * \brief Read the byte contents of a file directly into a vector.
*/ */
static std::vector<u8> ReadAllBytes(const char* utf8Path); 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. * \brief Read the byte contents of a file directly into a vector.
*/ */
static void WriteAllText(const char* utf8Path, std::string_view text); 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. * \brief Read the remaining contents of the file directly into a vector.
*/ */
@@ -67,6 +91,8 @@ public:
* Write data to the file. * Write data to the file.
*/ */
void Write(const char* data, size_t dataLen); void Write(const char* data, size_t dataLen);
FILE* ToInner();
}; };
} }
+9 -1
View File
@@ -7,11 +7,18 @@
#include <filesystem> #include <filesystem>
#if defined(_WIN32) || \
(defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_TV && !TARGET_OS_MACCATALYST) || \
(defined(__linux__) && !defined(__ANDROID__))
#define DUSK_CAN_OPEN_DATA_FOLDER 1
#else
#define DUSK_CAN_OPEN_DATA_FOLDER 0
#endif
namespace dusk { namespace dusk {
extern bool IsRunning; extern bool IsRunning;
extern bool IsShuttingDown; extern bool IsShuttingDown;
extern bool IsGameLaunched; extern bool IsGameLaunched;
extern bool IsFocusPaused;
extern bool RestartRequested; extern bool RestartRequested;
extern std::filesystem::path ConfigPath; extern std::filesystem::path ConfigPath;
@@ -23,6 +30,7 @@ namespace dusk {
#endif #endif
void RequestRestart() noexcept; void RequestRestart() noexcept;
bool OpenDataFolder();
} }
#endif // DUSK_MAIN_H #endif // DUSK_MAIN_H
+22 -3
View File
@@ -27,6 +27,11 @@ enum class DiscVerificationState : u8 {
HashMismatch, HashMismatch,
}; };
enum class GyroMode : u8 {
Sensor = 0,
Mouse = 1,
};
namespace config { namespace config {
template <> template <>
struct ConfigEnumRange<BloomMode> { struct ConfigEnumRange<BloomMode> {
@@ -45,6 +50,12 @@ struct ConfigEnumRange<DiscVerificationState> {
static constexpr auto min = DiscVerificationState::Unknown; static constexpr auto min = DiscVerificationState::Unknown;
static constexpr auto max = DiscVerificationState::HashMismatch; static constexpr auto max = DiscVerificationState::HashMismatch;
}; };
template <>
struct ConfigEnumRange<GyroMode> {
static constexpr auto min = GyroMode::Sensor;
static constexpr auto max = GyroMode::Mouse;
};
} }
// Persistent user settings // Persistent user settings
@@ -57,6 +68,8 @@ struct UserSettings {
ConfigVar<bool> enableFullscreen; ConfigVar<bool> enableFullscreen;
ConfigVar<bool> enableVsync; ConfigVar<bool> enableVsync;
ConfigVar<bool> lockAspectRatio; ConfigVar<bool> lockAspectRatio;
ConfigVar<bool> enableFpsOverlay;
ConfigVar<int> fpsOverlayCorner;
} video; } video;
struct { struct {
@@ -97,11 +110,11 @@ struct UserSettings {
// Preferences // Preferences
ConfigVar<bool> enableMirrorMode; ConfigVar<bool> enableMirrorMode;
ConfigVar<bool> disableMainHUD; ConfigVar<bool> minimalHUD;
ConfigVar<bool> pauseOnFocusLost; ConfigVar<bool> pauseOnFocusLost;
ConfigVar<bool> enableLinkDollRotation; ConfigVar<bool> enableLinkDollRotation;
ConfigVar<bool> enableAchievementNotifications; ConfigVar<bool> enableAchievementToasts;
ConfigVar<bool> enableControllerToasts;
// Graphics // Graphics
ConfigVar<BloomMode> bloomMode; ConfigVar<BloomMode> bloomMode;
@@ -118,6 +131,7 @@ struct UserSettings {
ConfigVar<bool> midnasLamentNonStop; ConfigVar<bool> midnasLamentNonStop;
// Input // Input
ConfigVar<GyroMode> gyroMode;
ConfigVar<bool> enableGyroAim; ConfigVar<bool> enableGyroAim;
ConfigVar<bool> enableGyroRollgoal; ConfigVar<bool> enableGyroRollgoal;
ConfigVar<float> gyroSensitivityX; ConfigVar<float> gyroSensitivityX;
@@ -132,6 +146,8 @@ struct UserSettings {
ConfigVar<bool> invertCameraYAxis; ConfigVar<bool> invertCameraYAxis;
ConfigVar<float> freeCameraSensitivity; ConfigVar<float> freeCameraSensitivity;
ConfigVar<bool> debugFlyCam; ConfigVar<bool> debugFlyCam;
ConfigVar<bool> debugFlyCamLockEvents;
ConfigVar<bool> allowBackgroundInput;
// Cheats // Cheats
ConfigVar<bool> infiniteHearts; ConfigVar<bool> infiniteHearts;
@@ -158,6 +174,7 @@ struct UserSettings {
// Tools // Tools
ConfigVar<bool> speedrunMode; ConfigVar<bool> speedrunMode;
ConfigVar<bool> liveSplitEnabled; ConfigVar<bool> liveSplitEnabled;
ConfigVar<bool> recordingMode;
} game; } game;
struct { struct {
@@ -168,7 +185,9 @@ struct UserSettings {
ConfigVar<bool> showPipelineCompilation; ConfigVar<bool> showPipelineCompilation;
ConfigVar<bool> wasPresetChosen; ConfigVar<bool> wasPresetChosen;
ConfigVar<bool> enableCrashReporting; ConfigVar<bool> enableCrashReporting;
ConfigVar<bool> checkForUpdates;
ConfigVar<int> cardFileType; ConfigVar<int> cardFileType;
ConfigVar<bool> enableAdvancedSettings;
} backend; } backend;
}; };
+17 -3
View File
@@ -8,6 +8,10 @@
#include "JSystem/JUtility/JUTFader.h" #include "JSystem/JUtility/JUTFader.h"
#include "JSystem/J2DGraph/J2DOrthoGraph.h" #include "JSystem/J2DGraph/J2DOrthoGraph.h"
#ifdef TARGET_PC
#include <algorithm>
#endif
JUTFader::JUTFader(int x, int y, int width, int height, JUtility::TColor pColor) JUTFader::JUTFader(int x, int y, int width, int height, JUtility::TColor pColor)
: mColor(pColor), mBox(x, y, x + width, y + height) { : mColor(pColor), mBox(x, y, x + width, y + height) {
mStatus = None; mStatus = None;
@@ -63,14 +67,24 @@ void JUTFader::advance() {
void JUTFader::control() { void JUTFader::control() {
advance(); advance();
#ifndef TARGET_PC
// FRAME INTERP NOTE: Draw is called by JFWDisplay when interpolation is active
draw(); draw();
#endif
} }
void JUTFader::draw() { void JUTFader::draw() {
if (mColor.a != 0) { if (mColor.a != 0) {
#ifdef TARGET_PC
if (dusk::frame_interp::is_enabled() && mDuration != 0) {
const auto step = dusk::frame_interp::get_interpolation_step();
const auto progress = static_cast<f32>(mTimer) / static_cast<f32>(mDuration);
const auto timer = mTimer - 1 + step + progress;
auto alpha = timer / mDuration;
if (mStatus == FadeIn) {
alpha = 1.0f - alpha;
}
alpha = std::clamp(alpha, 0.0f, 1.0f);
mColor.a = static_cast<u8>(alpha * 255.0f);
}
#endif
J2DOrthoGraph orthograph; J2DOrthoGraph orthograph;
orthograph.setColor(mColor); orthograph.setColor(mColor);
orthograph.fillBox(mBox); orthograph.fillBox(mBox);
+2 -3
View File
@@ -66,12 +66,11 @@ Output APK:
You can pass command-line args through the activity intent: You can pass command-line args through the activity intent:
```bash ```bash
adb shell am start -n com.twilitrealm.dusk/.DuskActivity \ adb shell am start -n dev.twilitrealm.dusk/.DuskActivity \
--es dusk_args "'/sdcard/Download/The Legend of Zelda: Twilight Princess (USA).iso'" --es dusk_args "--backend vulkan"
``` ```
Supported extras: Supported extras:
- `dusk_args`: single shell-like argument string - `dusk_args`: single shell-like argument string
- `dusk_argv`: string-array argv - `dusk_argv`: string-array argv
- `dusk_disc`: compatibility shortcut (single ISO path)
+20 -3
View File
@@ -2,12 +2,22 @@ plugins {
id 'com.android.application' id 'com.android.application'
} }
def duskRepoDir = rootProject.projectDir.parentFile.parentFile
def duskGeneratedAssetsDir = layout.buildDirectory.dir('generated/assets/dusk').get().asFile
def syncDuskAssets = tasks.register('syncDuskAssets', Sync) {
from(new File(duskRepoDir, 'res')) {
into 'res'
exclude '**/.DS_Store'
}
into duskGeneratedAssetsDir
}
android { android {
namespace 'com.twilitrealm.dusk' namespace 'dev.twilitrealm.dusk'
compileSdk 36 compileSdk 36
defaultConfig { defaultConfig {
applicationId 'com.twilitrealm.dusk' applicationId 'dev.twilitrealm.dusk'
minSdk 26 minSdk 26
targetSdk 36 targetSdk 36
versionCode 1 versionCode 1
@@ -27,7 +37,7 @@ android {
sourceSets { sourceSets {
main { main {
jniLibs.srcDirs = ['src/main/jniLibs'] jniLibs.srcDirs = ['src/main/jniLibs']
assets.srcDirs = ['../../assets'] assets.srcDirs = [duskGeneratedAssetsDir]
} }
} }
@@ -48,3 +58,10 @@ android {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
} }
tasks.configureEach { task ->
if ((task.name.startsWith('merge') && task.name.endsWith('Assets')) ||
task.name.toLowerCase().contains('lint')) {
task.dependsOn(syncDuskAssets)
}
}
+2
View File
@@ -1,2 +1,4 @@
# Keep SDL activity and related JNI bridge methods. # Keep SDL activity and related JNI bridge methods.
-keep class org.libsdl.app.** { *; } -keep class org.libsdl.app.** { *; }
-keep class dev.twilitrealm.dusk.DuskHttpClient { *; }
-keep class dev.twilitrealm.dusk.DuskHttpClient$Response { *; }
@@ -10,6 +10,7 @@
<uses-feature android:name="android.hardware.type.pc" android:required="false" /> <uses-feature android:name="android.hardware.type.pc" android:required="false" />
<uses-permission android:name="android.permission.VIBRATE" /> <uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.INTERNET" />
<application <application
android:allowBackup="true" android:allowBackup="true"
@@ -23,8 +24,19 @@
<meta-data android:name="android.game_mode_config" <meta-data android:name="android.game_mode_config"
android:resource="@xml/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 <activity
android:name="com.twilitrealm.dusk.DuskActivity" android:name="dev.twilitrealm.dusk.DuskActivity"
android:alwaysRetainTaskState="true" android:alwaysRetainTaskState="true"
android:configChanges="layoutDirection|locale|grammaticalGender|fontScale|fontWeightAdjustment|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation" android:configChanges="layoutDirection|locale|grammaticalGender|fontScale|fontWeightAdjustment|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
android:exported="true" android:exported="true"
@@ -1,18 +1,28 @@
package com.twilitrealm.dusk; package dev.twilitrealm.dusk;
import android.app.ActionBar; import android.app.ActionBar;
import android.content.ClipData;
import android.content.Intent; import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.provider.OpenableColumns;
import android.util.Log;
import android.view.View; import android.view.View;
import android.view.Window;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.view.WindowInsetsController;
import org.libsdl.app.SDLActivity; import org.libsdl.app.SDLActivity;
import java.io.File;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
public class DuskActivity extends SDLActivity { public class DuskActivity extends SDLActivity {
private static final String TAG = "DuskActivity";
private static String[] splitArgs(String raw) { private static String[] splitArgs(String raw) {
List<String> out = new ArrayList<>(); List<String> out = new ArrayList<>();
StringBuilder current = new StringBuilder(); StringBuilder current = new StringBuilder();
@@ -61,18 +71,46 @@ public class DuskActivity extends SDLActivity {
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(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) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
getWindow().getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars()); window.setDecorFitsSystemWindows(false);
}else { WindowInsetsController ctrl = window.getDecorView().getWindowInsetsController();
View decorView = getWindow().getDecorView(); if (ctrl != null) {
// Hide the status bar. ctrl.setSystemBarsBehavior(
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN; WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
ctrl.hide(WindowInsets.Type.systemBars());
}
} else {
View decorView = window.getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(uiOptions); decorView.setSystemUiVisibility(uiOptions);
// Remember that you should never show the action bar if the
// status bar is hidden, so hide that too if necessary.
ActionBar actionBar = getActionBar(); ActionBar actionBar = getActionBar();
actionBar.hide(); if (actionBar != null) {
actionBar.hide();
}
} }
} }
@@ -100,12 +138,96 @@ public class DuskActivity extends SDLActivity {
return splitArgs(trimmed); return splitArgs(trimmed);
} }
} }
String discPath = intent.getStringExtra("dusk_disc");
if (discPath != null && !discPath.isEmpty()) {
return new String[] { discPath };
}
} }
return new String[0]; return new String[0];
} }
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
persistUriPermissions(data);
}
super.onActivityResult(requestCode, resultCode, data);
}
private void persistUriPermissions(Intent data) {
if (data == null) {
return;
}
int permissionFlags =
data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (permissionFlags == 0) {
return;
}
Uri uri = data.getData();
if (uri != null) {
persistUriPermission(uri, permissionFlags);
}
ClipData clipData = data.getClipData();
if (clipData == null) {
return;
}
for (int i = 0; i < clipData.getItemCount(); ++i) {
Uri itemUri = clipData.getItemAt(i).getUri();
if (itemUri != null) {
persistUriPermission(itemUri, permissionFlags);
}
}
}
private void persistUriPermission(Uri uri, int permissionFlags) {
if ((permissionFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
persistUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION, "read");
}
if ((permissionFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
persistUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, "write");
}
}
private void persistUriPermission(Uri uri, int permissionFlag, String permissionName) {
try {
getContentResolver().takePersistableUriPermission(uri, permissionFlag);
} catch (SecurityException | IllegalArgumentException e) {
Log.w(TAG, "Unable to persist " + permissionName + " URI permission for " + uri, e);
}
}
public String getDisplayNameForUri(String uriString) {
if (uriString == null || uriString.isEmpty()) {
return "";
}
Uri uri = Uri.parse(uriString);
if ("content".equals(uri.getScheme())) {
try (Cursor cursor = getContentResolver().query(
uri, new String[] { OpenableColumns.DISPLAY_NAME }, null, null, null))
{
if (cursor != null && cursor.moveToFirst()) {
int displayNameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (displayNameColumn >= 0) {
String displayName = cursor.getString(displayNameColumn);
if (displayName != null && !displayName.isEmpty()) {
return displayName;
}
}
}
} catch (SecurityException | IllegalArgumentException e) {
Log.w(TAG, "Unable to query display name for " + uri, e);
}
} else if ("file".equals(uri.getScheme())) {
String path = uri.getPath();
if (path != null && !path.isEmpty()) {
String name = new File(path).getName();
if (!name.isEmpty()) {
return name;
}
}
}
String lastSegment = uri.getLastPathSegment();
return lastSegment != null ? lastSegment : "";
}
} }
@@ -0,0 +1,413 @@
package dev.twilitrealm.dusk;
import android.content.ContentResolver;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.webkit.MimeTypeMap;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
public class DuskDocumentsProvider extends DocumentsProvider {
public static final String AUTHORITY = "dev.twilitrealm.dusk.documents";
private static final String ROOT_ID = "dusk";
private static final String ROOT_DOCUMENT_ID = "root";
private static final String DIRECTORY_MIME_TYPE = Document.MIME_TYPE_DIR;
private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
Root.COLUMN_ROOT_ID,
Root.COLUMN_FLAGS,
Root.COLUMN_TITLE,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_ICON,
Root.COLUMN_AVAILABLE_BYTES,
Root.COLUMN_SUMMARY
};
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_FLAGS,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_SIZE
};
@Override
public boolean onCreate() {
ensureUserDirectories();
return true;
}
@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
final File root = getRootDirectory();
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, ROOT_ID);
row.add(Root.COLUMN_FLAGS,
Root.FLAG_LOCAL_ONLY |
Root.FLAG_SUPPORTS_CREATE |
Root.FLAG_SUPPORTS_IS_CHILD);
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.app_name));
row.add(Root.COLUMN_DOCUMENT_ID, ROOT_DOCUMENT_ID);
row.add(Root.COLUMN_ICON, R.mipmap.icon);
row.add(Root.COLUMN_AVAILABLE_BYTES, root.getFreeSpace());
row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.documents_provider_summary));
return result;
}
@Override
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
includeDocument(result, documentId, getFileForDocumentId(documentId));
return result;
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
throws FileNotFoundException
{
return queryChildDocumentsInternal(parentDocumentId, projection);
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, Bundle queryArgs)
throws FileNotFoundException
{
return queryChildDocumentsInternal(parentDocumentId, projection);
}
private Cursor queryChildDocumentsInternal(String parentDocumentId, String[] projection)
throws FileNotFoundException
{
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
final File parent = getFileForDocumentId(parentDocumentId);
final File[] files = parent.listFiles();
result.setNotificationUri(getContext().getContentResolver(), getChildDocumentsUri(parentDocumentId));
if (files == null) {
return result;
}
for (File file : files) {
includeDocument(result, getDocumentIdForFile(file), file);
}
return result;
}
@Override
public boolean isChildDocument(String parentDocumentId, String documentId) {
try {
final File parent = getFileForDocumentId(parentDocumentId);
final File child = getFileForDocumentId(documentId);
return isInside(parent, child);
} catch (FileNotFoundException e) {
return false;
}
}
@Override
public String createDocument(String parentDocumentId, String mimeType, String displayName)
throws FileNotFoundException
{
final File parent = getFileForDocumentId(parentDocumentId);
if (!parent.isDirectory()) {
throw new FileNotFoundException("Parent is not a directory: " + parentDocumentId);
}
final String safeDisplayName = sanitizeDisplayName(displayName);
final File file = buildUniqueFile(parent, safeDisplayName);
final boolean created;
if (DIRECTORY_MIME_TYPE.equals(mimeType)) {
created = file.mkdir();
} else {
try {
created = file.createNewFile();
} catch (IOException e) {
throw asFileNotFound("Unable to create document", e);
}
}
if (!created) {
throw new FileNotFoundException("Unable to create document: " + displayName);
}
notifyChildrenChanged(parentDocumentId);
return getDocumentIdForFile(file);
}
@Override
public String renameDocument(String documentId, String displayName) throws FileNotFoundException {
final File file = getFileForDocumentId(documentId);
if (ROOT_DOCUMENT_ID.equals(documentId)) {
throw new FileNotFoundException("Cannot rename root document");
}
final File target = buildUniqueFile(file.getParentFile(), sanitizeDisplayName(displayName));
final String parentDocumentId = getDocumentIdForFile(file.getParentFile());
if (!file.renameTo(target)) {
throw new FileNotFoundException("Unable to rename document: " + documentId);
}
notifyDocumentChanged(documentId);
notifyDocumentChanged(getDocumentIdForFile(target));
notifyChildrenChanged(parentDocumentId);
return getDocumentIdForFile(target);
}
@Override
public void deleteDocument(String documentId) throws FileNotFoundException {
if (ROOT_DOCUMENT_ID.equals(documentId)) {
throw new FileNotFoundException("Cannot delete root document");
}
final File file = getFileForDocumentId(documentId);
final String parentDocumentId = getDocumentIdForFile(file.getParentFile());
deleteRecursively(file);
notifyDocumentChanged(documentId);
notifyChildrenChanged(parentDocumentId);
}
@Override
public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal)
throws FileNotFoundException
{
return ParcelFileDescriptor.open(getFileForDocumentId(documentId), modeToParcelMode(mode));
}
@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, android.graphics.Point sizeHint,
CancellationSignal signal) throws FileNotFoundException
{
throw new FileNotFoundException("Thumbnails are not supported");
}
private void includeDocument(MatrixCursor result, String documentId, File file) throws FileNotFoundException {
final MatrixCursor.RowBuilder row = result.newRow();
final boolean isDirectory = file.isDirectory();
final String displayName = ROOT_DOCUMENT_ID.equals(documentId)
? getContext().getString(R.string.documents_provider_root_name)
: file.getName();
int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME;
if (isDirectory) {
flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
} else if (file.canWrite()) {
flags |= Document.FLAG_SUPPORTS_WRITE;
}
if (ROOT_DOCUMENT_ID.equals(documentId)) {
flags &= ~(Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME);
}
row.add(Document.COLUMN_DOCUMENT_ID, documentId);
row.add(Document.COLUMN_DISPLAY_NAME, displayName);
row.add(Document.COLUMN_FLAGS, flags);
row.add(Document.COLUMN_MIME_TYPE, isDirectory ? DIRECTORY_MIME_TYPE : getMimeType(file));
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
row.add(Document.COLUMN_SIZE, isDirectory ? null : file.length());
}
private File getRootDirectory() throws FileNotFoundException {
final File root = getContext().getFilesDir();
if (root == null) {
throw new FileNotFoundException("Dusk files directory is unavailable");
}
return root;
}
private File getFileForDocumentId(String documentId) throws FileNotFoundException {
final File root = getRootDirectory();
if (ROOT_DOCUMENT_ID.equals(documentId)) {
return root;
}
if (!documentId.startsWith(ROOT_DOCUMENT_ID + "/")) {
throw new FileNotFoundException("Invalid document id: " + documentId);
}
final String relativePath = documentId.substring(ROOT_DOCUMENT_ID.length() + 1);
final File file = new File(root, relativePath);
if (!isInside(root, file)) {
throw new FileNotFoundException("Document escapes Dusk files directory: " + documentId);
}
if (!file.exists()) {
throw new FileNotFoundException("Document does not exist: " + documentId);
}
return file;
}
private String getDocumentIdForFile(File file) throws FileNotFoundException {
final File root = getRootDirectory();
if (sameFile(root, file)) {
return ROOT_DOCUMENT_ID;
}
if (!isInside(root, file)) {
throw new FileNotFoundException("File escapes Dusk files directory: " + file);
}
final String rootPath = canonicalPath(root);
final String filePath = canonicalPath(file);
return ROOT_DOCUMENT_ID + "/" + filePath.substring(rootPath.length() + 1);
}
private void ensureUserDirectories() {
final File root = getContext().getFilesDir();
if (root == null) {
return;
}
new File(root, "texture_replacements").mkdirs();
new File(root, "USA/Card A").mkdirs();
new File(root, "EUR/Card A").mkdirs();
}
private static String[] resolveRootProjection(String[] projection) {
return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
}
private static String[] resolveDocumentProjection(String[] projection) {
return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
}
private static String sanitizeDisplayName(String displayName) throws FileNotFoundException {
if (displayName == null) {
throw new FileNotFoundException("Document name is empty");
}
final String sanitized = displayName.trim();
if (sanitized.isEmpty() || ".".equals(sanitized) || "..".equals(sanitized) ||
sanitized.contains("/") || sanitized.contains("\\"))
{
throw new FileNotFoundException("Invalid document name: " + displayName);
}
return sanitized;
}
private static File buildUniqueFile(File parent, String displayName) {
File file = new File(parent, displayName);
if (!file.exists()) {
return file;
}
final int dot = displayName.lastIndexOf('.');
final String baseName = dot > 0 ? displayName.substring(0, dot) : displayName;
final String extension = dot > 0 ? displayName.substring(dot) : "";
for (int i = 1; i < 100; ++i) {
file = new File(parent, baseName + " (" + i + ")" + extension);
if (!file.exists()) {
return file;
}
}
return new File(parent, baseName + " (" + System.currentTimeMillis() + ")" + extension);
}
private static int modeToParcelMode(String mode) {
if ("r".equals(mode)) {
return ParcelFileDescriptor.MODE_READ_ONLY;
}
if ("w".equals(mode) || "wt".equals(mode)) {
return ParcelFileDescriptor.MODE_WRITE_ONLY |
ParcelFileDescriptor.MODE_CREATE |
ParcelFileDescriptor.MODE_TRUNCATE;
}
if ("wa".equals(mode)) {
return ParcelFileDescriptor.MODE_WRITE_ONLY |
ParcelFileDescriptor.MODE_CREATE |
ParcelFileDescriptor.MODE_APPEND;
}
if ("rw".equals(mode)) {
return ParcelFileDescriptor.MODE_READ_WRITE |
ParcelFileDescriptor.MODE_CREATE;
}
if ("rwt".equals(mode)) {
return ParcelFileDescriptor.MODE_READ_WRITE |
ParcelFileDescriptor.MODE_CREATE |
ParcelFileDescriptor.MODE_TRUNCATE;
}
return ParcelFileDescriptor.MODE_READ_ONLY;
}
private static String getMimeType(File file) {
final int dot = file.getName().lastIndexOf('.');
if (dot >= 0) {
final String extension = file.getName().substring(dot + 1).toLowerCase();
final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mimeType != null) {
return mimeType;
}
}
return "application/octet-stream";
}
private Uri getChildDocumentsUri(String parentDocumentId) {
return DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId);
}
private void notifyChildrenChanged(String parentDocumentId) {
final ContentResolver resolver = getContext().getContentResolver();
resolver.notifyChange(getChildDocumentsUri(parentDocumentId), null, false);
}
private void notifyDocumentChanged(String documentId) {
final ContentResolver resolver = getContext().getContentResolver();
resolver.notifyChange(DocumentsContract.buildDocumentUri(AUTHORITY, documentId), null, false);
}
private static void deleteRecursively(File file) throws FileNotFoundException {
if (file.isDirectory()) {
final File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
deleteRecursively(child);
}
}
}
if (!file.delete()) {
throw new FileNotFoundException("Unable to delete document: " + file);
}
}
private static boolean isInside(File parent, File child) {
try {
final String parentPath = canonicalPath(parent);
final String childPath = canonicalPath(child);
return childPath.equals(parentPath) || childPath.startsWith(parentPath + File.separator);
} catch (FileNotFoundException e) {
return false;
}
}
private static boolean sameFile(File a, File b) {
try {
return canonicalPath(a).equals(canonicalPath(b));
} catch (FileNotFoundException e) {
return false;
}
}
private static String canonicalPath(File file) throws FileNotFoundException {
try {
return file.getCanonicalPath();
} catch (IOException e) {
throw asFileNotFound("Unable to resolve path", e);
}
}
private static FileNotFoundException asFileNotFound(String message, IOException cause) {
final FileNotFoundException exception = new FileNotFoundException(message + ": " + cause.getMessage());
exception.initCause(cause);
return exception;
}
}
@@ -0,0 +1,237 @@
package dev.twilitrealm.dusk;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
public final class DuskHttpClient {
public static final int ERROR_NONE = 0;
public static final int ERROR_INVALID_URL = 1;
public static final int ERROR_UNSUPPORTED_SCHEME = 2;
public static final int ERROR_TIMEOUT = 3;
public static final int ERROR_TOO_LARGE = 4;
public static final int ERROR_NETWORK = 5;
private static final int MAX_REDIRECTS = 5;
public static final class Response {
public int error;
public String message;
public int statusCode;
public String[] headerNames;
public String[] headerValues;
public byte[] body;
Response(int error, String message, int statusCode, String[] headerNames,
String[] headerValues, byte[] body) {
this.error = error;
this.message = message;
this.statusCode = statusCode;
this.headerNames = headerNames != null ? headerNames : new String[0];
this.headerValues = headerValues != null ? headerValues : new String[0];
this.body = body != null ? body : new byte[0];
}
}
private DuskHttpClient() {
}
public static Response get(String url, String[] headerNames, String[] headerValues,
int timeoutMs, long maxBodyBytes) {
if (url == null || url.isEmpty()) {
return fail(ERROR_INVALID_URL, "URL is empty");
}
try {
URL currentUrl = new URL(url);
if (!isHttps(currentUrl)) {
return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported");
}
for (int redirect = 0; redirect <= MAX_REDIRECTS; ++redirect) {
HttpsURLConnection connection =
(HttpsURLConnection) currentUrl.openConnection();
try {
connection.setRequestMethod("GET");
connection.setConnectTimeout(timeoutMs);
connection.setReadTimeout(timeoutMs);
connection.setUseCaches(false);
connection.setInstanceFollowRedirects(false);
applyHeaders(connection, headerNames, headerValues);
int statusCode = connection.getResponseCode();
if (isRedirect(statusCode)) {
String location = connection.getHeaderField("Location");
if (location == null || location.isEmpty()) {
return fail(ERROR_NETWORK, "Redirect response did not include Location",
statusCode, connection, new byte[0]);
}
URL nextUrl = new URL(currentUrl, location);
if (!isHttps(nextUrl)) {
return fail(ERROR_UNSUPPORTED_SCHEME,
"Only https:// redirects are supported", statusCode,
connection, new byte[0]);
}
currentUrl = nextUrl;
continue;
}
byte[] body = readBody(connection, statusCode, maxBodyBytes);
return success(statusCode, connection, body);
} catch (ResponseTooLargeException e) {
return fail(ERROR_TOO_LARGE, "Response body exceeded the configured limit",
safeStatusCode(connection), connection, e.partialBody);
} finally {
connection.disconnect();
}
}
return fail(ERROR_NETWORK, "Too many redirects");
} catch (MalformedURLException e) {
return fail(ERROR_INVALID_URL, "Failed to parse URL");
} catch (SocketTimeoutException e) {
return fail(ERROR_TIMEOUT, "Request timed out");
} catch (IOException e) {
String message = e.getMessage();
return fail(ERROR_NETWORK, message != null ? message : e.toString());
} catch (ClassCastException e) {
return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported");
}
}
private static void applyHeaders(HttpsURLConnection connection, String[] names,
String[] values) {
if (names == null || values == null) {
return;
}
int count = Math.min(names.length, values.length);
for (int i = 0; i < count; ++i) {
if (names[i] != null && values[i] != null) {
connection.setRequestProperty(names[i], values[i]);
}
}
}
private static boolean isHttps(URL url) {
return "https".equalsIgnoreCase(url.getProtocol());
}
private static boolean isRedirect(int statusCode) {
return statusCode == HttpURLConnection.HTTP_MOVED_PERM ||
statusCode == HttpURLConnection.HTTP_MOVED_TEMP ||
statusCode == HttpURLConnection.HTTP_SEE_OTHER ||
statusCode == 307 ||
statusCode == 308;
}
private static byte[] readBody(HttpsURLConnection connection, int statusCode,
long maxBodyBytes) throws IOException,
ResponseTooLargeException {
InputStream stream = statusCode >= HttpURLConnection.HTTP_BAD_REQUEST ?
connection.getErrorStream() : connection.getInputStream();
if (stream == null) {
return new byte[0];
}
try (InputStream bodyStream = stream;
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buffer = new byte[8192];
long total = 0;
while (true) {
int read = bodyStream.read(buffer);
if (read < 0) {
return out.toByteArray();
}
if (read == 0) {
continue;
}
if (read > maxBodyBytes || total > maxBodyBytes - read) {
throw new ResponseTooLargeException(out.toByteArray());
}
out.write(buffer, 0, read);
total += read;
}
}
}
private static int safeStatusCode(HttpsURLConnection connection) {
try {
return connection.getResponseCode();
} catch (IOException e) {
return 0;
}
}
private static Response success(int statusCode, HttpsURLConnection connection, byte[] body) {
HeaderLists headers = readHeaders(connection);
return new Response(ERROR_NONE, "", statusCode, headers.names, headers.values, body);
}
private static Response fail(int error, String message) {
return new Response(error, message, 0, null, null, null);
}
private static Response fail(int error, String message, int statusCode,
HttpsURLConnection connection, byte[] body) {
HeaderLists headers = readHeaders(connection);
return new Response(error, message, statusCode, headers.names, headers.values, body);
}
private static HeaderLists readHeaders(HttpsURLConnection connection) {
List<String> names = new ArrayList<>();
List<String> values = new ArrayList<>();
Map<String, List<String>> headerFields = connection.getHeaderFields();
if (headerFields == null) {
return new HeaderLists(new String[0], new String[0]);
}
for (Map.Entry<String, List<String>> entry : headerFields.entrySet()) {
String name = entry.getKey();
if (name == null) {
continue;
}
List<String> entryValues = entry.getValue();
if (entryValues == null || entryValues.isEmpty()) {
names.add(name);
values.add("");
continue;
}
for (String value : entryValues) {
names.add(name);
values.add(value != null ? value : "");
}
}
return new HeaderLists(names.toArray(new String[0]), values.toArray(new String[0]));
}
private static final class HeaderLists {
final String[] names;
final String[] values;
HeaderLists(String[] names, String[] values) {
this.names = names;
this.values = values;
}
}
private static final class ResponseTooLargeException extends Exception {
final byte[] partialBody;
ResponseTooLargeException(byte[] partialBody) {
this.partialBody = partialBody;
}
}
}
@@ -256,6 +256,7 @@ public class HIDDeviceManager {
0x24c6, // PowerA 0x24c6, // PowerA
0x2c22, // Qanba 0x2c22, // Qanba
0x2dc8, // 8BitDo 0x2dc8, // 8BitDo
0x37d7, // Flydigi
0x9886, // ASTRO Gaming 0x9886, // ASTRO Gaming
}; };
@@ -61,7 +61,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
private static final String TAG = "SDL"; private static final String TAG = "SDL";
private static final int SDL_MAJOR_VERSION = 3; private static final int SDL_MAJOR_VERSION = 3;
private static final int SDL_MINOR_VERSION = 4; private static final int SDL_MINOR_VERSION = 4;
private static final int SDL_MICRO_VERSION = 2; private static final int SDL_MICRO_VERSION = 4;
/* /*
// Display InputType.SOURCE/CLASS of events and devices // Display InputType.SOURCE/CLASS of events and devices
// //
@@ -2032,7 +2032,7 @@ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityCh
try { try {
ParcelFileDescriptor pfd = mSingleton.getContentResolver().openFileDescriptor(Uri.parse(uri), mode); ParcelFileDescriptor pfd = mSingleton.getContentResolver().openFileDescriptor(Uri.parse(uri), mode);
return pfd != null ? pfd.detachFd() : -1; return pfd != null ? pfd.detachFd() : -1;
} catch (FileNotFoundException e) { } catch (FileNotFoundException | SecurityException e) {
e.printStackTrace(); e.printStackTrace();
return -1; return -1;
} }
@@ -2227,4 +2227,3 @@ class SDLClipboardHandler implements
SDLActivity.onNativeClipboardChanged(); SDLActivity.onNativeClipboardChanged();
} }
} }
@@ -65,17 +65,15 @@ class SDLInputConnection extends BaseInputConnection
@Override @Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) { public boolean deleteSurroundingText(int beforeLength, int afterLength) {
if (Build.VERSION.SDK_INT <= 29 /* Android 10.0 (Q) */) { // Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection
// Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection // and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
// and https://bugzilla.libsdl.org/show_bug.cgi?id=2265 if (beforeLength > 0 && afterLength == 0) {
if (beforeLength > 0 && afterLength == 0) { // backspace(s)
// backspace(s) while (beforeLength-- > 0) {
while (beforeLength-- > 0) { nativeGenerateScancodeForUnichar('\b');
nativeGenerateScancodeForUnichar('\b'); }
} return true;
return true; }
}
}
if (!super.deleteSurroundingText(beforeLength, afterLength)) { if (!super.deleteSurroundingText(beforeLength, afterLength)) {
return false; return false;
@@ -35,6 +35,8 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
View.OnApplyWindowInsetsListener, View.OnKeyListener, View.OnTouchListener, View.OnApplyWindowInsetsListener, View.OnKeyListener, View.OnTouchListener,
SensorEventListener, ScaleGestureDetector.OnScaleGestureListener { SensorEventListener, ScaleGestureDetector.OnScaleGestureListener {
private static native void auroraNativeSetSurfaceReady(boolean ready);
// Sensors // Sensors
protected SensorManager mSensorManager; protected SensorManager mSensorManager;
protected Display mDisplay; protected Display mDisplay;
@@ -96,6 +98,7 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
@Override @Override
public void surfaceCreated(SurfaceHolder holder) { public void surfaceCreated(SurfaceHolder holder) {
Log.v("SDL", "surfaceCreated()"); Log.v("SDL", "surfaceCreated()");
auroraNativeSetSurfaceReady(false);
SDLActivity.onNativeSurfaceCreated(); SDLActivity.onNativeSurfaceCreated();
} }
@@ -103,6 +106,7 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
@Override @Override
public void surfaceDestroyed(SurfaceHolder holder) { public void surfaceDestroyed(SurfaceHolder holder) {
Log.v("SDL", "surfaceDestroyed()"); Log.v("SDL", "surfaceDestroyed()");
auroraNativeSetSurfaceReady(false);
// Transition to pause, if needed // Transition to pause, if needed
SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED; SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
@@ -192,6 +196,7 @@ public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
/* Surface is ready */ /* Surface is ready */
mIsSurfaceReady = true; mIsSurfaceReady = true;
auroraNativeSetSurfaceReady(true);
SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED; SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
SDLActivity.handleNativeState(); SDLActivity.handleNativeState();
@@ -1,4 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name">Dusk</string> <string name="app_name">Dusk</string>
<string name="documents_provider_root_name">Dusk Data</string>
<string name="documents_provider_summary">Saves, texture packs, settings, and logs</string>
</resources> </resources>
+35 -16
View File
@@ -14,9 +14,22 @@ if [[ -z "$ANDROID_NDK_VER" ]] && [[ -d "$ANDROID_HOME_DIR/ndk" ]]; then
fi fi
if [[ -n "$ANDROID_NDK_VER" ]]; then if [[ -n "$ANDROID_NDK_VER" ]]; then
TOOLCHAIN_BIN="$ANDROID_HOME_DIR/ndk/$ANDROID_NDK_VER/toolchains/llvm/prebuilt/linux-x86_64/bin" case "$(uname -s)" in
if [[ -x "$TOOLCHAIN_BIN/llvm-strip" ]]; then Darwin) HOST_TAG="darwin-x86_64" ;;
STRIP_TOOL="$TOOLCHAIN_BIN/llvm-strip" Linux) HOST_TAG="linux-x86_64" ;;
*) HOST_TAG="" ;;
esac
PREBUILT_DIR="$ANDROID_HOME_DIR/ndk/$ANDROID_NDK_VER/toolchains/llvm/prebuilt"
if [[ -n "$HOST_TAG" && -x "$PREBUILT_DIR/$HOST_TAG/bin/llvm-strip" ]]; then
STRIP_TOOL="$PREBUILT_DIR/$HOST_TAG/bin/llvm-strip"
else
for candidate in "$PREBUILT_DIR"/*/bin/llvm-strip; do
if [[ -x "$candidate" ]]; then
STRIP_TOOL="$candidate"
break
fi
done
fi fi
fi fi
@@ -25,29 +38,35 @@ copy_lib() {
local src="$2" local src="$2"
local dst_dir="$APP_DIR/$abi" local dst_dir="$APP_DIR/$abi"
local dst="$dst_dir/libmain.so" local dst="$dst_dir/libmain.so"
local tmp="$dst_dir/.libmain.so.$$"
if [[ ! -f "$src" ]]; then
echo "Missing native library for $abi: $src" >&2
exit 1
fi
mkdir -p "$dst_dir" mkdir -p "$dst_dir"
cp -f "$src" "$dst" cp -f "$src" "$tmp"
if [[ "$ANDROID_STAGE_STRIP" != "0" ]] && [[ -n "$STRIP_TOOL" ]]; then if [[ "$ANDROID_STAGE_STRIP" != "0" ]] && [[ -n "$STRIP_TOOL" ]]; then
"$STRIP_TOOL" --strip-debug "$dst" "$STRIP_TOOL" --strip-unneeded "$tmp"
echo "Staged and stripped $src -> $dst" mv -f "$tmp" "$dst"
echo "Stripped and staged $src -> $dst"
else else
mv -f "$tmp" "$dst"
echo "Staged $src -> $dst (strip disabled or strip tool unavailable)" echo "Staged $src -> $dst (strip disabled or strip tool unavailable)"
fi fi
} }
declare -A ABI_TO_LIB=(
["arm64-v8a"]="$ROOT_DIR/build/android-arm64/libmain.so"
["x86_64"]="$ROOT_DIR/build/android-x86_64/libmain.so"
)
# Drop any previously staged ABI directories to avoid stale APK contents. # Drop any previously staged ABI directories to avoid stale APK contents.
rm -rf "$APP_DIR/x86" "$APP_DIR/arm64-v8a" "$APP_DIR/x86_64" rm -rf "$APP_DIR/x86" "$APP_DIR/arm64-v8a" "$APP_DIR/x86_64"
for abi in $ANDROID_STAGE_ABIS; do for abi in $ANDROID_STAGE_ABIS; do
src="${ABI_TO_LIB[$abi]:-}" case "$abi" in
if [[ -z "$src" ]]; then arm64-v8a) src="$ROOT_DIR/build/android-arm64/libmain.so" ;;
echo "Unsupported ABI '$abi'. Supported ABIs: arm64-v8a x86_64" >&2 x86_64) src="$ROOT_DIR/build/android-x86_64/libmain.so" ;;
exit 1 *)
fi echo "Unsupported ABI '$abi'. Supported ABIs: arm64-v8a x86_64" >&2
exit 1
;;
esac
copy_lib "$abi" "$src" copy_lib "$abi" "$src"
done done
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 928 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 B

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 B

After

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 761 B

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 B

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 B

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 179 B

After

Width:  |  Height:  |  Size: 8.7 KiB

+1 -1
View File
@@ -6,4 +6,4 @@ Exec=dusk
Icon=dusk Icon=dusk
Terminal=false Terminal=false
Type=Application Type=Application
Categories=Graphics;3DGraphics;Game Categories=Game;
+4
View File
@@ -79,5 +79,9 @@
<true/> <true/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
</dict> </dict>
</plist> </plist>
+68 -14
View File
@@ -20,18 +20,22 @@ body {
pointer-events: none; pointer-events: none;
} }
fps,
toast { toast {
position: absolute; position: absolute;
border: 1dp #92875B;
background-color: rgba(21, 22, 16, 80%);
}
toast {
top: 40dp; top: 40dp;
right: 40dp; right: 40dp;
display: flex; display: flex;
flex-flow: column; flex-flow: column;
border-radius: 14dp; border-radius: 14dp;
overflow: hidden; overflow: hidden;
border: 1dp #92875B;
backdrop-filter: blur(5dp); backdrop-filter: blur(5dp);
box-shadow: 0 0 15dp 3dp; box-shadow: 0 0 15dp 3dp;
background-color: rgba(21, 22, 16, 80%);
filter: opacity(0); filter: opacity(0);
transform: scale(0.9); transform: scale(0.9);
transform-origin: center; transform-origin: center;
@@ -159,33 +163,68 @@ icon {
} }
icon.arrow-forward { icon.arrow-forward {
width: 24dp; width: 1.2em;
height: 24dp; height: 1.2em;
font-size: 24dp; font-size: 1.2em;
decorator: text("&#xe5c8;" center center); decorator: text("&#xe5c8;" center center);
} }
icon.trophy { icon.trophy {
width: 24dp; width: 1.2em;
height: 24dp; height: 1.2em;
font-size: 24dp; font-size: 1.2em;
decorator: text("&#xe71a;" center center); decorator: text("&#xe71a;" center center);
} }
icon.controller { icon.controller {
width: 24dp; width: 1.2em;
height: 24dp; height: 1.2em;
font-size: 24dp; font-size: 1.2em;
decorator: text("&#xf135;" center center); decorator: text("&#xf135;" center center);
} }
icon.warning { icon.warning {
width: 24dp; width: 1.2em;
height: 24dp; height: 1.2em;
font-size: 24dp; font-size: 1.2em;
decorator: text("&#xe002;" center center); decorator: text("&#xe002;" center center);
} }
fps {
display: none;
z-index: 99;
font-size: 18dp;
font-weight: bold;
padding: 9dp 12dp;
border-radius: 7dp;
pointer-events: none;
white-space: nowrap;
}
fps[open] {
display: block;
}
fps[corner=tl] {
top: 12dp;
left: 12dp;
}
fps[corner=tr] {
top: 12dp;
right: 12dp;
}
fps[corner=bl] {
bottom: 12dp;
left: 12dp;
}
fps[corner=br] {
bottom: 12dp;
right: 12dp;
}
logo { logo {
position: absolute; position: absolute;
width: 100dp; width: 100dp;
@@ -235,3 +274,18 @@ logo img.outer {
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
@media (max-height: 640dp) {
toast {
top: 20dp;
right: 20dp;
}
toast.controller-warning {
bottom: 20dp;
}
toast.menu-notification {
top: 20dp;
}
}
+180 -7
View File
@@ -273,20 +273,60 @@ body.mirrored version-info {
font-size: 20dp; font-size: 20dp;
} }
/* TODO: Hidden until an actual update checker is introduced */
.update { .update {
display: none; display: none;
font-size: 16dp; color: #A6A09B;
font-weight: bold; align-items: center;
cursor: pointer; justify-content: flex-end;
color: #D8F999; gap: 8dp;
font-size: 20dp;
} }
.detail, .update[state=checking],
.update span { .update[state=failed] {
display: block;
}
.update[state=available] {
display: flex;
}
#update-download {
display: none;
margin: 0dp;
padding: 0dp;
border-width: 0dp;
background-color: transparent;
color: #D8F999;
cursor: pointer;
text-transform: uppercase;
font-weight: bold;
decorator: horizontal-gradient(#00000000 #00000000);
}
.update[state=available] #update-download {
display: flex;
align-items: center;
gap: 2dp;
}
#update-download icon {
display: block;
width: 18dp;
height: 18dp;
font-family: "Material Symbols Rounded";
font-weight: normal;
decorator: text("&#xe5c8;" center center);
}
.detail {
color: #A6A09B; color: #A6A09B;
} }
body.mirrored .update {
justify-content: flex-start;
}
/* Startup animation */ /* Startup animation */
.intro-item { .intro-item {
opacity: 0; opacity: 0;
@@ -322,3 +362,136 @@ body.animate-in .intro-item {
.delay-5 { .delay-5 {
transition: opacity transform 0.3s 0.6s cubic-in-out; transition: opacity transform 0.3s 0.6s cubic-in-out;
} }
/* Mobile layout */
@media (max-height: 640dp) {
.gradient {
decorator: horizontal-gradient(#00000000 #000000FF);
}
body.mirrored .gradient {
decorator: horizontal-gradient(#000000FF #00000000);
}
menu {
left: 32dp;
right: 32dp;
width: auto;
min-width: 0;
max-width: none;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 16dp;
}
body.mirrored menu {
left: 32dp;
right: 32dp;
flex-direction: row-reverse;
}
hero {
flex: 1 1 0;
min-width: 0;
max-width: 48%;
margin-left: 32dp;
}
body.mirrored hero {
align-items: flex-end;
}
hero img {
width: 100%;
}
#menu-list {
flex: 1 1 0;
min-width: 0;
max-width: 52%;
align-items: flex-end;
}
#menu-list button {
width: 100%;
max-width: 100%;
text-align: right;
}
#menu-list button:hover,
#menu-list button:focus-visible {
decorator: horizontal-gradient(#FEE68500 #FEE685FF);
}
body.mirrored #menu-list {
align-items: flex-start;
}
body.mirrored #menu-list button {
text-align: left;
}
body.mirrored #menu-list button:hover,
body.mirrored #menu-list button:focus-visible {
decorator: horizontal-gradient(#FEE685FF #FEE68500);
}
.eyebrow {
display: none;
}
disc-info {
right: 32dp;
left: auto;
bottom: 32dp;
top: auto;
text-align: right;
font-size: 16dp;
}
#disc-status {
justify-content: flex-end;
}
#disc-status icon {
font-size: 20dp;
}
#disc-version {
font-size: 16dp;
}
version-info {
right: 32dp;
left: auto;
bottom: auto;
top: 32dp;
text-align: right;
font-size: 16dp;
}
.update {
font-size: 16dp;
}
body.mirrored disc-info {
right: auto;
left: 32dp;
bottom: 32dp;
top: auto;
text-align: left;
}
body.mirrored version-info {
right: auto;
left: 32dp;
bottom: auto;
top: 32dp;
text-align: left;
}
body.mirrored #disc-status {
justify-content: flex-start;
}
}
+27 -5
View File
@@ -17,6 +17,7 @@ body {
window { window {
display: flex; display: flex;
flex-flow: column; flex-flow: column;
position: relative;
height: 100%; height: 100%;
width: 100%; width: 100%;
max-width: 1088dp; max-width: 1088dp;
@@ -287,6 +288,23 @@ icon.celebration {
decorator: text("&#xea65;" center center); decorator: text("&#xea65;" center center);
} }
icon.question-mark {
decorator: text("&#xeb8b;" center center);
}
.achievement-total {
position: absolute;
top: 0;
right: 64dp;
height: 64dp;
line-height: 64dp;
font-family: "Fira Sans Condensed";
font-weight: bold;
font-size: 16dp;
color: rgba(224, 219, 200, 55%);
pointer-events: none;
}
.achievement-row { .achievement-row {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
@@ -343,7 +361,7 @@ icon.celebration {
color: rgba(224, 219, 200, 45%); color: rgba(224, 219, 200, 45%);
} }
progressbar { progress {
display: block; display: block;
width: 100%; width: 100%;
height: 6dp; height: 6dp;
@@ -352,12 +370,12 @@ progressbar {
margin: 6dp 0 2dp 0; margin: 6dp 0 2dp 0;
} }
progressbar.progress-done fill { progress.progress-done fill {
background-color: #44aa22; background-color: #44aa22;
border-radius: 3dp; border-radius: 3dp;
} }
progressbar.progress-ongoing fill { progress.progress-ongoing fill {
background-color: #2255bb; background-color: #2255bb;
border-radius: 3dp; border-radius: 3dp;
} }
@@ -447,6 +465,11 @@ window.modal.danger .modal-header icon {
font-weight: normal; font-weight: normal;
} }
.modal-body span.tip {
font-size: 14dp;
color: #92875B;
}
.verification-progress { .verification-progress {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -460,7 +483,7 @@ window.modal.danger .modal-header icon {
color: #FFFFFF; color: #FFFFFF;
} }
progressbar.verification-progress-bar { progress.verification-progress-bar {
height: 8dp; height: 8dp;
margin: 2dp 0 0 0; margin: 2dp 0 0 0;
} }
@@ -475,7 +498,6 @@ progressbar.verification-progress-bar {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: nowrap; flex-wrap: nowrap;
justify-content: stretch;
align-items: stretch; align-items: stretch;
gap: 12dp; gap: 12dp;
width: 100%; width: 100%;
+26
View File
@@ -51,6 +51,7 @@
#include "d/actor/d_a_ni.h" #include "d/actor/d_a_ni.h"
#include "d/d_s_play.h" #include "d/d_s_play.h"
#include "dusk/frame_interpolation.h"
#include "dusk/settings.h" #include "dusk/settings.h"
#include "res/Object/Alink.h" #include "res/Object/Alink.h"
#include <cstring> #include <cstring>
@@ -14787,6 +14788,10 @@ void daAlink_c::deleteEquipItem(BOOL i_isPlaySound, BOOL i_isDeleteKantera) {
mIronBallChainPos = NULL; mIronBallChainPos = NULL;
mIronBallChainAngle = NULL; mIronBallChainAngle = NULL;
field_0x3848 = NULL; field_0x3848 = NULL;
#if TARGET_PC
mIBChainInterpPrevValid = false;
mIBChainInterpCurrValid = false;
#endif
field_0x0774 = NULL; field_0x0774 = NULL;
field_0x0778 = NULL; field_0x0778 = NULL;
mpHookshotLinChk = NULL; mpHookshotLinChk = NULL;
@@ -19717,6 +19722,27 @@ int daAlink_c::draw() {
) )
{ {
dComIfGd_getOpaListDark()->entryImm(mpHookChain, 0); dComIfGd_getOpaListDark()->entryImm(mpHookChain, 0);
#if TARGET_PC
if (dusk::getSettings().game.enableFrameInterpolation &&
mEquipItem == dItemNo_IRONBALL_e &&
mIronBallChainPos != NULL && mIronBallChainAngle != NULL)
{
if (mIBChainInterpCurrValid) {
memcpy(mIBChainInterpPrevPos, mIBChainInterpCurrPos, IRON_BALL_CHAIN_COUNT * sizeof(cXyz));
memcpy(mIBChainInterpPrevAngle, mIBChainInterpCurrAngle, IRON_BALL_CHAIN_COUNT * sizeof(csXyz));
mIBChainInterpPrevHandRoot = mIBChainInterpCurrHandRoot;
mIBChainInterpPrevValid = true;
}
memcpy(mIBChainInterpCurrPos, mIronBallChainPos, IRON_BALL_CHAIN_COUNT * sizeof(cXyz));
memcpy(mIBChainInterpCurrAngle, mIronBallChainAngle, IRON_BALL_CHAIN_COUNT * sizeof(csXyz));
mIBChainInterpCurrHandRoot = mHookshotTopPos;
mIBChainInterpCurrValid = true;
dusk::frame_interp::add_interpolation_callback(&ironBallChainInterpCallback, this);
}
#endif
} }
} }
+3
View File
@@ -205,6 +205,9 @@ int daAlink_c::setDamagePoint(int i_dmgAmount, BOOL i_checkZoraMag, BOOL i_setDm
dComIfGp_setItemLifeCount(-i_dmgAmount, 0); dComIfGp_setItemLifeCount(-i_dmgAmount, 0);
} }
#if TARGET_PC
dusk::AchievementSystem::get().signal("player_damaged");
#endif
onResetFlg1(RFLG1_DAMAGE_IMPACT); onResetFlg1(RFLG1_DAMAGE_IMPACT);
mSwordUpTimer = 0; mSwordUpTimer = 0;
+48 -6
View File
@@ -8,6 +8,8 @@
#include "d/actor/d_a_obj_swhang.h" #include "d/actor/d_a_obj_swhang.h"
#include "d/actor/d_a_obj_chandelier.h" #include "d/actor/d_a_obj_chandelier.h"
#include "JSystem/J3DGraphBase/J3DMaterial.h" #include "JSystem/J3DGraphBase/J3DMaterial.h"
#include "dusk/frame_interpolation.h"
#include "dusk/settings.h"
enum { enum {
HS_MODE_NONE_e, HS_MODE_NONE_e,
@@ -17,11 +19,11 @@ enum {
HS_MODE_RETURN_e = 6, HS_MODE_RETURN_e = 6,
}; };
void daAlink_c::hsChainShape_c::draw() { #if TARGET_PC
if (dusk::getSettings().game.superClawshot) { static const int HS_CHAIN_MAX_LINKS = 600;
return; #endif
}
void daAlink_c::hsChainShape_c::draw() {
static const int dummy = 0; static const int dummy = 0;
daAlink_c* alink = (daAlink_c*)getUserArea(); daAlink_c* alink = (daAlink_c*)getUserArea();
@@ -165,7 +167,11 @@ void daAlink_c::hsChainShape_c::draw() {
} }
(void)0; (void)0;
while (maxDistanceF > var_f30) { #if TARGET_PC
int chainLinks = 0;
#endif
while (maxDistanceF > var_f30 IF_DUSK(&&chainLinks < HS_CHAIN_MAX_LINKS)) {
temp_f27 = var_f28 * cM_fsin(sp34 * var_f30); temp_f27 = var_f28 * cM_fsin(sp34 * var_f30);
s16 spC = cM_atan2s(temp_f27 - var_f26, 5.0f); s16 spC = cM_atan2s(temp_f27 - var_f26, 5.0f);
sp64.x = sp6C.x + spC; sp64.x = sp6C.x + spC;
@@ -187,6 +193,10 @@ void daAlink_c::hsChainShape_c::draw() {
var_f26 = temp_f27; var_f26 = temp_f27;
var_f30 += fabsf(cM_scos(spC)) * 5.0f; var_f30 += fabsf(cM_scos(spC)) * 5.0f;
#if TARGET_PC
chainLinks++;
#endif
} }
} }
@@ -202,7 +212,11 @@ void daAlink_c::hsChainShape_c::draw() {
sp98 = subChainTopPos; sp98 = subChainTopPos;
sp6C.set(maxDistance.atan2sY_XZ(), maxDistance.atan2sX_Z(), 0); sp6C.set(maxDistance.atan2sY_XZ(), maxDistance.atan2sX_Z(), 0);
while (maxDistanceF > var_f30) { #if TARGET_PC
int subChainLinks = 0;
#endif
while (maxDistanceF > var_f30 IF_DUSK(&&subChainLinks < HS_CHAIN_MAX_LINKS)) {
mDoMtx_stack_c::copy(j3dSys.getViewMtx()); mDoMtx_stack_c::copy(j3dSys.getViewMtx());
mDoMtx_stack_c::transM(sp98); mDoMtx_stack_c::transM(sp98);
mDoMtx_stack_c::ZXYrotM(sp6C); mDoMtx_stack_c::ZXYrotM(sp6C);
@@ -215,11 +229,39 @@ void daAlink_c::hsChainShape_c::draw() {
sp98 += maxDistance * 5.0f; sp98 += maxDistance * 5.0f;
ANGLE_ADD_2(sp6C.z, 0x3000); ANGLE_ADD_2(sp6C.z, 0x3000);
var_f30 += 5.0f; var_f30 += 5.0f;
#if TARGET_PC
subChainLinks++;
#endif
} }
} }
} }
} }
#if TARGET_PC
static void ironBallChainInterpCallback(bool isSimFrame, void* pUserWork) {
static_cast<daAlink_c*>(pUserWork)->onIronBallChainInterpCallback();
}
void daAlink_c::onIronBallChainInterpCallback() {
if (!mIBChainInterpPrevValid || !mIBChainInterpCurrValid) {
return;
}
if (mIronBallChainPos == NULL || mIronBallChainAngle == NULL) {
return;
}
const f32 alpha = dusk::frame_interp::get_interpolation_step();
for (int i = 0; i < IRON_BALL_CHAIN_COUNT; i++) {
mIronBallChainPos[i] = mIBChainInterpPrevPos[i] + (mIBChainInterpCurrPos[i] - mIBChainInterpPrevPos[i]) * alpha;
mIronBallChainAngle[i].x = mIBChainInterpPrevAngle[i].x + (s16)((s16)(mIBChainInterpCurrAngle[i].x - mIBChainInterpPrevAngle[i].x) * alpha);
mIronBallChainAngle[i].y = mIBChainInterpPrevAngle[i].y + (s16)((s16)(mIBChainInterpCurrAngle[i].y - mIBChainInterpPrevAngle[i].y) * alpha);
mIronBallChainAngle[i].z = mIBChainInterpPrevAngle[i].z + (s16)((s16)(mIBChainInterpCurrAngle[i].z - mIBChainInterpPrevAngle[i].z) * alpha);
}
mHookshotTopPos = mIBChainInterpPrevHandRoot + (mIBChainInterpCurrHandRoot - mIBChainInterpPrevHandRoot) * alpha;
}
#endif
void daAlink_c::hookshotAtHitCallBack(dCcD_GObjInf* i_atObjInf, fopAc_ac_c* i_tgActor, void daAlink_c::hookshotAtHitCallBack(dCcD_GObjInf* i_atObjInf, fopAc_ac_c* i_tgActor,
dCcD_GObjInf* i_tgObjInf) { dCcD_GObjInf* i_tgObjInf) {
if (i_tgActor != NULL && fopAcM_IsActor(i_tgActor) && !i_tgObjInf->ChkTgHookshotThrough()) { if (i_tgActor != NULL && fopAcM_IsActor(i_tgActor) && !i_tgObjInf->ChkTgHookshotThrough()) {
+9 -1
View File
@@ -17,6 +17,9 @@
#include "d/actor/d_a_e_pz.h" #include "d/actor/d_a_e_pz.h"
#include "d/actor/d_a_horse.h" #include "d/actor/d_a_horse.h"
#include "d/actor/d_a_hozelda.h" #include "d/actor/d_a_hozelda.h"
#if TARGET_PC
#include "dusk/achievements.h"
#endif
int daArrow_c::createHeap() { int daArrow_c::createHeap() {
J3DModelData* model_data; J3DModelData* model_data;
@@ -92,7 +95,12 @@ void daArrow_c::atHitCallBack(dCcD_GObjInf* i_atObjInf, fopAc_ac_c* i_tgActor, d
if (dist_to_hitpos < field_0x998) { if (dist_to_hitpos < field_0x998) {
field_0x998 = dist_to_hitpos; field_0x998 = dist_to_hitpos;
mHitAcID = fopAcM_GetID(i_tgActor); mHitAcID = fopAcM_GetID(i_tgActor);
#if TARGET_PC
if (fopAcM_GetGroup(i_tgActor) == fopAc_ENEMY_e &&
current.pos.abs(mStartPos) > 10000.0f) {
dusk::AchievementSystem::get().signal("arrow_hit_100m");
}
#endif
if (mArrowType == 1) { if (mArrowType == 1) {
field_0x9a8 = *hit_pos_p; field_0x9a8 = *hit_pos_p;
} else if (i_tgObjInf->ChkTgShield()) { } else if (i_tgObjInf->ChkTgShield()) {
+6
View File
@@ -19,6 +19,9 @@
#include "dusk/frame_interpolation.h" #include "dusk/frame_interpolation.h"
#include "dusk/settings.h" #include "dusk/settings.h"
#if TARGET_PC
#include "dusk/achievements.h"
#endif
class daB_GND_HIO_c : public JORReflexible { class daB_GND_HIO_c : public JORReflexible {
public: public:
@@ -1289,6 +1292,9 @@ static void b_gnd_g_wait(b_gnd_class* i_this) {
if (i_this->mMoveMode < 5 && i_this->mPlayerDistXZ < 600.0f) { if (i_this->mMoveMode < 5 && i_this->mPlayerDistXZ < 600.0f) {
i_this->mMoveMode = 5; i_this->mMoveMode = 5;
i_this->field_0xc44[0] = 10; i_this->field_0xc44[0] = 10;
#if TARGET_PC
dusk::AchievementSystem::get().signal("ganondorf_fishing_rod");
#endif
} }
} else if (i_this->mMoveMode == 5) { } else if (i_this->mMoveMode == 5) {
i_this->mMoveMode = 6; i_this->mMoveMode = 6;
+31
View File
@@ -10,6 +10,10 @@
#include "f_op/f_op_kankyo_mng.h" #include "f_op/f_op_kankyo_mng.h"
#include "f_op/f_op_actor_enemy.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 { class daE_DB_HIO_c : public JORReflexible {
public: public:
daE_DB_HIO_c(); 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; 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) { static int daE_DB_Draw(e_db_class* i_this) {
fopAc_ac_c* actor = &i_this->enemy; 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}; static GXColor l_color = {0x14, 0x0F, 0x00, 0xFF};
i_this->stalkLine.update(12, l_color, &actor->tevStr); i_this->stalkLine.update(12, l_color, &actor->tevStr);
dComIfGd_set3DlineMat(&i_this->stalkLine); 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++) { for (int i = 1; i < 11; i++) {
if (i_this->thornModel[i] != NULL) { if (i_this->thornModel[i] != NULL) {
+31
View File
@@ -9,6 +9,10 @@
#include "d/actor/d_a_e_hb_leaf.h" #include "d/actor/d_a_e_hb_leaf.h"
#include "f_op/f_op_actor_enemy.h" #include "f_op/f_op_actor_enemy.h"
#if TARGET_PC
#include "dusk/frame_interpolation.h"
#endif
enum daE_HB_ACTION { enum daE_HB_ACTION {
ACTION_STAY, ACTION_STAY,
ACTION_APPEAR, 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; 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) { static int daE_HB_Draw(e_hb_class* i_this) {
fopAc_ac_c* actor = &i_this->enemy; 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}; static GXColor l_color = {0x14, 0x0F, 0x00, 0xFF};
i_this->stalkLine.update(12, l_color, &actor->tevStr); i_this->stalkLine.update(12, l_color, &actor->tevStr);
dComIfGd_set3DlineMat(&i_this->stalkLine); 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++) { for (int i = 1; i < 11; i++) {
if (i_this->thornModel[i] != NULL) { if (i_this->thornModel[i] != NULL) {
+42
View File
@@ -14,6 +14,8 @@
#include "d/d_s_play.h" #include "d/d_s_play.h"
#include "f_op/f_op_actor_enemy.h" #include "f_op/f_op_actor_enemy.h"
#include "f_op/f_op_camera_mng.h" #include "f_op/f_op_camera_mng.h"
#include "dusk/frame_interpolation.h"
#include "dusk/settings.h"
#include <cstring> #include <cstring>
class daE_S1_HIO_c { 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; 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) { static int daE_S1_Draw(e_s1_class* i_this) {
if (i_this->field_0x306c != 0) { if (i_this->field_0x306c != 0) {
return 1; 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); i_this->mLineMat.update(16, line_color, &i_this->tevStr);
dComIfGd_set3DlineMatDark(&i_this->mLineMat); 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(); dComIfGd_setList();
return 1; return 1;
} }
@@ -2149,6 +2186,11 @@ static int daE_S1_Create(fopAc_ac_c* i_this) {
return cPhs_ERROR_e; return cPhs_ERROR_e;
} }
#if TARGET_PC
a_this->mHairInterpPrevValid = false;
a_this->mHairInterpCurrValid = false;
#endif
OS_REPORT("//////////////E_S1 SET 2 !!\n"); OS_REPORT("//////////////E_S1 SET 2 !!\n");
if (path_no != 0xFF) { if (path_no != 0xFF) {
+9
View File
@@ -12,6 +12,9 @@
#include "c/c_damagereaction.h" #include "c/c_damagereaction.h"
#include "f_op/f_op_actor_enemy.h" #include "f_op/f_op_actor_enemy.h"
#include "f_op/f_op_camera_mng.h" #include "f_op/f_op_camera_mng.h"
#if TARGET_PC
#include "dusk/achievements.h"
#endif
class daE_TH_HIO_c : public JORReflexible { class daE_TH_HIO_c : public JORReflexible {
public: public:
@@ -542,6 +545,7 @@ static void damage_check(e_th_class* i_this) {
if (i_this->field_0x6a4 == 0 && i_this->mAction != ACTION_SPIN) { if (i_this->field_0x6a4 == 0 && i_this->mAction != ACTION_SPIN) {
daPy_py_c* player = (daPy_py_c*)dComIfGp_getPlayer(0); daPy_py_c* player = (daPy_py_c*)dComIfGp_getPlayer(0);
OS_REPORT("E_th HP1 %d\n", i_this->health); OS_REPORT("E_th HP1 %d\n", i_this->health);
s16 prevHealth = i_this->health;
cc_at_check(i_this, &i_this->mAtInfo); cc_at_check(i_this, &i_this->mAtInfo);
OS_REPORT("E_th HP2 %d\n", i_this->health); OS_REPORT("E_th HP2 %d\n", i_this->health);
@@ -554,6 +558,11 @@ static void damage_check(e_th_class* i_this) {
dComIfGs_onOneZoneSwitch(3, -1); dComIfGs_onOneZoneSwitch(3, -1);
if (i_this->health <= 0) { if (i_this->health <= 0) {
#if TARGET_PC
if (prevHealth == i_this->field_0x560) {
dusk::AchievementSystem::get().signal("dark_hammer_one_hit");
}
#endif
i_this->mAction = ACTION_END; i_this->mAction = ACTION_END;
i_this->mMode = 0; i_this->mMode = 0;
i_this->field_0x68a |= 4; i_this->field_0x68a |= 4;
+2
View File
@@ -5852,6 +5852,8 @@ static int daE_WB_Create(fopAc_ac_c* actor) {
daE_WB_Execute(i_this); daE_WB_Execute(i_this);
c_start = 0; c_start = 0;
// Note: this flag makes king bulblin 1 instant die when set, as it only requires 2 laps
// for insta-kill to trigger.
if (dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[88])) { if (dComIfGs_isEventBit(dSv_event_flag_c::saveBitLabels[88])) {
i_this->lap_num = 1; i_this->lap_num = 1;
} }
+31
View File
@@ -12,6 +12,10 @@
#include "d/d_cc_uty.h" #include "d/d_cc_uty.h"
#include "f_op/f_op_actor_enemy.h" #include "f_op/f_op_actor_enemy.h"
#if TARGET_PC
#include "dusk/frame_interpolation.h"
#endif
class daE_YD_HIO_c { class daE_YD_HIO_c {
public: public:
daE_YD_HIO_c(); 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; 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 s32 daE_YD_Draw(e_yd_class* i_this) {
static GXColor l_color = { 0x14, 0x0F, 0x00, 0xFF }; 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->mpMorf->entryDL();
i_this->mLineMat.update(12, l_color, &i_this->actor.tevStr); i_this->mLineMat.update(12, l_color, &i_this->actor.tevStr);
dComIfGd_set3DlineMat(&i_this->mLineMat); 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++) { for (s32 i = 1; i < 11; i++) {
if (i_this->field_0x77c[i] != 0) { if (i_this->field_0x77c[i] != 0) {
g_env_light.setLightTevColorType_MAJI(i_this->field_0x77c[i], &i_this->actor.tevStr); 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 "f_op/f_op_kankyo_mng.h"
#include "d/actor/d_a_obj_carry.h" #include "d/actor/d_a_obj_carry.h"
#include "Z2AudioLib/Z2Instances.h" #include "Z2AudioLib/Z2Instances.h"
#include "dusk/frame_interpolation.h"
#include "dusk/settings.h"
#include "f_op/f_op_actor_enemy.h" #include "f_op/f_op_actor_enemy.h"
enum E_yg_RES_File_ID { enum E_yg_RES_File_ID {
@@ -134,7 +136,26 @@ static BOOL pl_check(e_yg_class* i_this, f32 i_dist) {
return FALSE; 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) { if (i_this->mDispFlag) {
return 1; return 1;
} }
@@ -160,6 +181,23 @@ static int daE_YG_Draw(e_yg_class* i_this) {
color.a = 0xFF; color.a = 0xFF;
i_this->mLineMat.update(10, color, &actor->tevStr); i_this->mLineMat.update(10, color, &actor->tevStr);
dComIfGd_set3DlineMatDark(&i_this->mLineMat); 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(); dComIfGd_setList();
return 1; return 1;
@@ -1378,6 +1416,11 @@ static cPhs_Step daE_YG_Create(fopAc_ac_c* actor) {
return cPhs_ERROR_e; return cPhs_ERROR_e;
} }
#if TARGET_PC
i_this->mTentacleInterpPrevValid = false;
i_this->mTentacleInterpCurrValid = false;
#endif
if (!hio_set) { if (!hio_set) {
i_this->mIsFirstSpawn = 1; i_this->mIsFirstSpawn = 1;
hio_set = true; hio_set = true;
+31
View File
@@ -12,6 +12,10 @@
#include "f_op/f_op_actor_enemy.h" #include "f_op/f_op_actor_enemy.h"
#include "f_op/f_op_kankyo_mng.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 { class daE_YH_HIO_c : public JORReflexible {
public: public:
daE_YH_HIO_c(); 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; 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) { static int daE_YH_Draw(e_yh_class* i_this) {
fopAc_ac_c* a_this = (fopAc_ac_c*)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); i_this->mLine.update(12, l_color, &a_this->tevStr);
dComIfGd_set3DlineMat(&i_this->mLine); 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++) { for (int i = 1; i < 11; i++) {
if (i_this->mModels[i] != NULL) { if (i_this->mModels[i] != NULL) {
+5 -3
View File
@@ -729,10 +729,12 @@ static void koro2_game(fshop_class* i_this) {
cLib_addCalcAngleS2(&i_this->field_0x4020.z, 0, 2, 0x200); cLib_addCalcAngleS2(&i_this->field_0x4020.z, 0, 2, 0x200);
case 2: case 2:
#if TARGET_PC #if TARGET_PC
if (dusk::getSettings().game.enableGyroRollgoal) { if (dusk::gyro::rollgoal_gyro_enabled()) {
if (!dusk::gyro::get_sensor_keep_alive()) { if (!dusk::gyro::get_sensor_keep_alive()) {
dusk::gyro::set_sensor_keep_alive(true); dusk::gyro::set_sensor_keep_alive(true);
} }
} else if (dusk::gyro::get_sensor_keep_alive()) {
dusk::gyro::set_sensor_keep_alive(false);
} }
#endif #endif
@@ -753,7 +755,7 @@ static void koro2_game(fshop_class* i_this) {
old_stick_x = mDoCPd_c::getSubStickX(PAD_1); old_stick_x = mDoCPd_c::getSubStickX(PAD_1);
cLib_addCalcAngleS2(&i_this->field_0x4060, i_this->field_0x4062, 4, 0x1000); cLib_addCalcAngleS2(&i_this->field_0x4060, i_this->field_0x4062, 4, 0x1000);
#if TARGET_PC #if TARGET_PC
if (dusk::getSettings().game.enableGyroRollgoal) { if (dusk::gyro::rollgoal_gyro_enabled()) {
dusk::gyro::rollgoalTick(true, i_this->field_0x4060); dusk::gyro::rollgoalTick(true, i_this->field_0x4060);
} }
#endif #endif
@@ -791,7 +793,7 @@ static void koro2_game(fshop_class* i_this) {
s16 gyro_ax = 0; s16 gyro_ax = 0;
s16 gyro_az = 0; s16 gyro_az = 0;
#if TARGET_PC #if TARGET_PC
if (dusk::getSettings().game.enableGyroRollgoal) { if (dusk::gyro::rollgoal_gyro_enabled()) {
dusk::gyro::rollgoalTableOffset(gyro_ax, gyro_az); dusk::gyro::rollgoalTableOffset(gyro_ax, gyro_az);
} }
#endif #endif
+36
View File
@@ -419,9 +419,30 @@ int daMidna_c::createHeap() {
return 0; 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")) { if (mpDemoFCTongueBmd != NULL && !daAlink_c::initDemoBck(&mpDemoFCTmpBck, "demo00_Midna_cut00_FC_tmp.bck")) {
return 0; return 0;
} }
#endif
modelData = modelData =
(J3DModelData*)dComIfG_getObjectRes(dStage_roomControl_c::getDemoArcName(), "demo00_Midna_cut00_BD_tmp.bmd"); (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(2)->setMaterialAnm(mpEyeMatAnm[0]);
modelData->getMaterialNodePointer(3)->setMaterialAnm(mpEyeMatAnm[1]); 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)) { if (!initDemoModel(&mpDemoBDMaskBmd, "demo00_Midna_cut00_BD_mask.bmd", 0)) {
+39
View File
@@ -10,6 +10,8 @@
#include "JSystem/J3DGraphBase/J3DDrawBuffer.h" #include "JSystem/J3DGraphBase/J3DDrawBuffer.h"
#include "SSystem/SComponent/c_math.h" #include "SSystem/SComponent/c_math.h"
#include "d/d_com_inf_game.h" #include "d/d_com_inf_game.h"
#include "dusk/frame_interpolation.h"
#include "dusk/settings.h"
#include <cstring> #include <cstring>
static char const l_arcName[] = "Fchain"; static char const l_arcName[] = "Fchain";
@@ -65,6 +67,10 @@ int daObjFchain_c::create() {
local_48++; local_48++;
} }
rv = cPhs_COMPLEATE_e; rv = cPhs_COMPLEATE_e;
#if TARGET_PC
mChainInterpPrevValid = false;
mChainInterpCurrValid = false;
#endif
break; break;
} }
return rv; 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() { int daObjFchain_c::draw() {
if (field_0x584 != 0) { if (field_0x584 != 0) {
g_env_light.settingTevStruct(0, &current.pos, &tevStr); g_env_light.settingTevStruct(0, &current.pos, &tevStr);
@@ -297,6 +323,19 @@ int daObjFchain_c::draw() {
return 1; return 1;
} }
dComIfGd_getOpaListDark()->entryImm(&mShape, 0); 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; return 1;
} }
+9
View File
@@ -12,6 +12,10 @@
#include "SSystem/SComponent/c_counter.h" #include "SSystem/SComponent/c_counter.h"
#include <cstring> #include <cstring>
#if TARGET_PC
#include "dusk/settings.h"
#endif
#define DRAW_TYPE_YELLOW 0 #define DRAW_TYPE_YELLOW 0
#define DRAW_TYPE_RED 1 #define DRAW_TYPE_RED 1
@@ -1422,6 +1426,11 @@ int dAttention_c::Run() {
} }
void dAttention_c::Draw() { void dAttention_c::Draw() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
if (mAttParam.CheckFlag(dAttParam_c::EFlag_ARROW_OFF)) { if (mAttParam.CheckFlag(dAttParam_c::EFlag_ARROW_OFF)) {
draw[0].field_0x173 = 3; draw[0].field_0x173 = 3;
draw[1].field_0x173 = 3; draw[1].field_0x173 = 3;
+114 -23
View File
@@ -31,6 +31,7 @@
#if TARGET_PC #if TARGET_PC
#include "dusk/frame_interpolation.h" #include "dusk/frame_interpolation.h"
#include "dusk/logging.h" #include "dusk/logging.h"
#include "imgui.h"
#endif #endif
namespace { namespace {
@@ -7483,6 +7484,8 @@ static constexpr f32 FLYCAM_SPEED = 0.5f;
static constexpr f32 FLYCAM_FAST_SPEED = 4.0f; static constexpr f32 FLYCAM_FAST_SPEED = 4.0f;
static constexpr f32 FLYCAM_ROTATION_SPEED = 0.002f; static constexpr f32 FLYCAM_ROTATION_SPEED = 0.002f;
static constexpr f32 FLYCAM_TRIGGER_DEADZONE = 20.0f; static constexpr f32 FLYCAM_TRIGGER_DEADZONE = 20.0f;
static constexpr s16 FLYCAM_ROLL_SPEED = 256;
static ImVec2 sFlyCamLastMousePos = {-1.f, -1.f};
#if TARGET_PC #if TARGET_PC
bool dCamera_c::executeDebugFlyCam() { bool dCamera_c::executeDebugFlyCam() {
@@ -7490,6 +7493,7 @@ bool dCamera_c::executeDebugFlyCam() {
if (mDebugFlyCam.initialized) { if (mDebugFlyCam.initialized) {
deactivateDebugFlyCam(); deactivateDebugFlyCam();
} }
sFlyCamLastMousePos = {-1.f, -1.f};
return false; return false;
} }
@@ -7519,16 +7523,63 @@ bool dCamera_c::executeDebugFlyCam() {
mDebugFlyCam.initialized = true; mDebugFlyCam.initialized = true;
} }
event->mEventStatus = 1; if (dusk::getSettings().game.debugFlyCamLockEvents) {
dComIfGp_getEventManager().setCameraPlay(1); event->mEventStatus = 1;
dComIfGp_getEventManager().setCameraPlay(1);
} else {
if (event->mEventStatus != 0) {
event->mEventStatus = 0;
}
dComIfGp_getEventManager().setCameraPlay(0);
}
interface_of_controller_pad& pad = mDoCPd_c::getCpadInfo(0); f32 stickY = 0.f;
f32 stickY = pad.mMainStickPosY * 72.0f; f32 stickX = 0.f;
f32 stickX = pad.mMainStickPosX * 72.0f; f32 cStickY = 0.f;
f32 cStickY = pad.mCStickPosY * 59.0f; f32 cStickX = 0.f;
f32 cStickX = pad.mCStickPosX * 59.0f; f32 trigL = 0.f;
f32 trigL = pad.mTriggerLeft * 150.0f; f32 trigR = 0.f;
f32 trigR = pad.mTriggerRight * 150.0f; f32 rollInput = 0.f;
bool fast = false;
if (dusk::getSettings().game.debugFlyCamLockEvents) {
interface_of_controller_pad& pad = mDoCPd_c::getCpadInfo(0);
stickY = pad.mMainStickPosY * 72.0f;
stickX = pad.mMainStickPosX * 72.0f;
cStickY = pad.mCStickPosY * 59.0f;
cStickX = pad.mCStickPosX * 59.0f;
trigL = pad.mTriggerLeft * 150.0f;
trigR = pad.mTriggerRight * 150.0f;
fast = mDoCPd_c::getHoldZ(PAD_1);
if (mDoCPd_c::getHoldY(PAD_1)) rollInput -= 1.f;
if (mDoCPd_c::getHoldX(PAD_1)) rollInput += 1.f;
}
{
ImGuiIO& io = ImGui::GetIO();
if (!io.WantCaptureKeyboard) {
f32 kbX = 0.0f, kbY = 0.0f;
if (ImGui::IsKeyDown(ImGuiKey_W) || ImGui::IsKeyDown(ImGuiKey_UpArrow)) kbY += 1.f;
if (ImGui::IsKeyDown(ImGuiKey_S) || ImGui::IsKeyDown(ImGuiKey_DownArrow)) kbY -= 1.f;
if (ImGui::IsKeyDown(ImGuiKey_D) || ImGui::IsKeyDown(ImGuiKey_RightArrow)) kbX += 1.f;
if (ImGui::IsKeyDown(ImGuiKey_A) || ImGui::IsKeyDown(ImGuiKey_LeftArrow)) kbX -= 1.f;
f32 len = sqrtf(kbX * kbX + kbY * kbY);
if (len > 1.f) { kbX /= len; kbY /= len; }
stickX += kbX * 72.0f;
stickY += kbY * 72.0f;
if (ImGui::IsKeyDown(ImGuiKey_Space)) trigR += 150.0f;
if (ImGui::IsKeyDown(ImGuiKey_LeftCtrl)) trigL += 150.0f;
if (ImGui::IsKeyDown(ImGuiKey_LeftShift)) fast = true;
if (ImGui::IsKeyDown(ImGuiKey_Q)) rollInput -= 1.0f;
if (ImGui::IsKeyDown(ImGuiKey_E)) rollInput += 1.0f;
}
bool mouseValid = !io.WantCaptureMouse && io.MousePos.x >= 0.0f && io.MousePos.y >= 0.0f;
if (mouseValid && sFlyCamLastMousePos.x >= 0.0f) {
cStickX -= (io.MousePos.x - sFlyCamLastMousePos.x) * 2.0f;
cStickY -= (io.MousePos.y - sFlyCamLastMousePos.y) * 2.0f;
}
sFlyCamLastMousePos = mouseValid ? io.MousePos : ImVec2{-1.0f, -1.0f};
}
f32 verticalDisp = 0.0f; f32 verticalDisp = 0.0f;
if (trigR >= FLYCAM_TRIGGER_DEADZONE) { if (trigR >= FLYCAM_TRIGGER_DEADZONE) {
@@ -7542,7 +7593,7 @@ bool dCamera_c::executeDebugFlyCam() {
f32 moveDx = stickY * cosf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) - stickX * sinf(mDebugFlyCam.yaw); f32 moveDx = stickY * cosf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) - stickX * sinf(mDebugFlyCam.yaw);
f32 moveDz = stickY * sinf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) + stickX * cosf(mDebugFlyCam.yaw); f32 moveDz = stickY * sinf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) + stickX * cosf(mDebugFlyCam.yaw);
f32 speed = mDoCPd_c::getHoldZ(PAD_1) ? FLYCAM_FAST_SPEED : FLYCAM_SPEED; f32 speed = fast ? FLYCAM_FAST_SPEED : FLYCAM_SPEED;
mEye.x += speed * moveDx; mEye.x += speed * moveDx;
mEye.y += speed * moveDy; mEye.y += speed * moveDy;
@@ -7553,6 +7604,7 @@ bool dCamera_c::executeDebugFlyCam() {
mCenter.z = mEye.z + sinf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST; mCenter.z = mEye.z + sinf(mDebugFlyCam.yaw) * cosf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST;
mCenter.y = mEye.y + sinf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST; mCenter.y = mEye.y + sinf(mDebugFlyCam.pitch) * FLYCAM_TARGET_DIST;
mBank = mBank + static_cast<s16>(rollInput * FLYCAM_ROLL_SPEED * (fast ? FLYCAM_FAST_SPEED / FLYCAM_SPEED : 1.f));
Reset(mCenter, mEye); Reset(mCenter, mEye);
f32 yawInput = dusk::getSettings().game.invertCameraXAxis ? cStickX : -cStickX; f32 yawInput = dusk::getSettings().game.invertCameraXAxis ? cStickX : -cStickX;
@@ -7570,7 +7622,7 @@ void dCamera_c::deactivateDebugFlyCam() {
Reset(mDebugFlyCam.savedCenter, mDebugFlyCam.savedEye, mDebugFlyCam.savedFovy, mDebugFlyCam.savedBank.Val()); Reset(mDebugFlyCam.savedCenter, mDebugFlyCam.savedEye, mDebugFlyCam.savedFovy, mDebugFlyCam.savedBank.Val());
dEvt_control_c* event = dComIfGp_getEvent(); dEvt_control_c* event = dComIfGp_getEvent();
if (event != nullptr) { if (event != nullptr && event->mEventStatus != 0) {
event->mEventStatus = 0; event->mEventStatus = 0;
} }
dComIfGp_getEventManager().setCameraPlay(0); dComIfGp_getEventManager().setCameraPlay(0);
@@ -11210,6 +11262,26 @@ static int camera_execute(camera_process_class* i_this) {
return 1; 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) { static int camera_draw(camera_process_class* i_this) {
camera_class* a_this = (camera_class*)i_this; camera_class* a_this = (camera_class*)i_this;
dCamera_c* body = &i_this->mCamera; dCamera_c* body = &i_this->mCamera;
@@ -11263,21 +11335,40 @@ static int camera_draw(camera_process_class* i_this) {
#endif #endif
#if TARGET_PC #if TARGET_PC
auto trim_height = body->TrimHeight(); set_ar_corrected_trim(window, body->TrimHeight());
if (mDoGph_gInf_c::isWideZoom()) { if (dusk::getSettings().game.enableFrameInterpolation) {
const auto target_ar = FB_WIDTH / (FB_HEIGHT - trim_height * 2.0f); dusk::frame_interp::add_interpolation_callback([](bool _, void* pUserWork) {
const auto current_ar = mDoGph_gInf_c::m_safeWidthF / mDoGph_gInf_c::m_safeHeightF; const auto i_this = static_cast<camera_process_class*>(pUserWork);
const auto camera = &i_this->mCamera;
if (current_ar < target_ar) { const auto trim_size = camera->mTrimSize;
trim_height = FB_HEIGHT / 2.0f * (1.0f - current_ar / target_ar);
} else { if (camera->mCurState != 2 && trim_size >= 0 && trim_size <= 3) {
trim_height = 0.0f; // 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);
} }
trim_height *= viewport->height / FB_HEIGHT;
window->setScissor(0.0f, trim_height, viewport->width, viewport->height - trim_height * 2.0f);
#else #else
int trim_height = body->TrimHeight(); int trim_height = body->TrimHeight();
+8
View File
@@ -13,6 +13,9 @@
#include "d/d_s_play.h" #include "d/d_s_play.h"
#include "d/d_com_inf_game.h" #include "d/d_com_inf_game.h"
#include "f_op/f_op_actor_mng.h" #include "f_op/f_op_actor_mng.h"
#if TARGET_PC
#include "dusk/achievements.h"
#endif
static int plCutLRC[58] = { static int plCutLRC[58] = {
0, // 0, //
@@ -434,6 +437,11 @@ fopAc_ac_c* cc_at_check(fopAc_ac_c* i_enemy, dCcU_AtInfo* i_AtInfo) {
if (i_AtInfo->mAttackPower != 0 && i_enemy->health <= 0) { if (i_AtInfo->mAttackPower != 0 && i_enemy->health <= 0) {
i_AtInfo->mHitStatus = 2; i_AtInfo->mHitStatus = 2;
i_enemy->health = 0; i_enemy->health = 0;
#if TARGET_PC
if (fopAcM_GetGroup(i_enemy) == fopAc_ENEMY_e) {
dusk::AchievementSystem::get().signal("enemy_killed");
}
#endif
} }
int uvar8; int uvar8;
+2 -1
View File
@@ -965,7 +965,8 @@ void dMenu_Letter_c::screenSetBase() {
} }
if (field_0x374 > 1) { if (field_0x374 > 1) {
J2DPane* pJVar6 = mpBaseScreen->search('pi_n'); J2DPane* pJVar6 = mpBaseScreen->search('pi_n');
f32 dVar18 = field_0x1f0[1]->getBounds().i.x - field_0x1f0[0]->getBounds().i.x; f32 x1 = field_0x1f0[1]->getBounds().i.x;
f32 dVar18 = x1 - field_0x1f0[0]->getBounds().i.x;
f32 dVar17 = dVar18 * (field_0x374 - 1); f32 dVar17 = dVar18 * (field_0x374 - 1);
f32 dVar16 = (pJVar6->getWidth() / 2) - (dVar17 / 2); f32 dVar16 = (pJVar6->getWidth() / 2) - (dVar17 / 2);
for (int i = 0; i < 9; i++) { for (int i = 0; i < 9; i++) {
+6 -3
View File
@@ -24,9 +24,10 @@
#include "d/actor/d_a_horse.h" #include "d/actor/d_a_horse.h"
#include <cstring> #include <cstring>
#if TARGET_PC
#include "dusk/memory.h" #include "dusk/memory.h"
#include "dusk/settings.h"
#include "dusk/memory.h" #endif
int dMeter2_c::_create() { int dMeter2_c::_create() {
stage_stag_info_class* stag_info = dComIfGp_getStageStagInfo(); stage_stag_info_class* stag_info = dComIfGp_getStageStagInfo();
@@ -317,7 +318,9 @@ int dMeter2_c::_execute() {
int dMeter2_c::_draw() { int dMeter2_c::_draw() {
#if TARGET_PC #if TARGET_PC
if (dusk::getSettings().game.disableMainHUD) { if (dusk::getSettings().game.recordingMode || dusk::getSettings().game.minimalHUD ||
dusk::getSettings().game.debugFlyCam)
{
return 1; return 1;
} }
#endif #endif
+3 -2
View File
@@ -427,14 +427,15 @@ static void dummyStrings() {
dMsgObject_HIO_c g_MsgObject_HIO_c; dMsgObject_HIO_c g_MsgObject_HIO_c;
int dMsgObject_c::_execute() { 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) { if (dusk::getSettings().game.enableMirrorMode) {
// enable wii message index override // enable wii message index override
g_MsgObject_HIO_c.mMessageDisplay = 1; g_MsgObject_HIO_c.mMessageDisplay = 1;
} else if (!dusk::getSettings().game.enableMirrorMode && g_MsgObject_HIO_c.mMessageDisplay == 1) { } else if (!dusk::getSettings().game.enableMirrorMode && g_MsgObject_HIO_c.mMessageDisplay == 1) {
g_MsgObject_HIO_c.mMessageDisplay = 0; g_MsgObject_HIO_c.mMessageDisplay = 0;
} }
#endif #endif*/
field_0x4c7 = 0; field_0x4c7 = 0;
+9
View File
@@ -6,6 +6,10 @@
#include "d/d_com_inf_game.h" #include "d/d_com_inf_game.h"
#include "d/d_pane_class.h" #include "d/d_pane_class.h"
#if TARGET_PC
#include "dusk/settings.h"
#endif
dMsgScrnArrow_c::dMsgScrnArrow_c() { dMsgScrnArrow_c::dMsgScrnArrow_c() {
mpScreen = JKR_NEW J2DScreen(); mpScreen = JKR_NEW J2DScreen();
JUT_ASSERT(0, mpScreen != NULL); JUT_ASSERT(0, mpScreen != NULL);
@@ -65,6 +69,11 @@ dMsgScrnArrow_c::~dMsgScrnArrow_c() {
} }
void dMsgScrnArrow_c::draw() { void dMsgScrnArrow_c::draw() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
J2DGrafContext* graf_ctx = dComIfGp_getCurrentGrafPort(); J2DGrafContext* graf_ctx = dComIfGp_getCurrentGrafPort();
mpScreen->draw(0.0f, 0.0f, graf_ctx); mpScreen->draw(0.0f, 0.0f, graf_ctx);
} }
+24
View File
@@ -8,6 +8,10 @@
#include "d/d_pane_class.h" #include "d/d_pane_class.h"
#include <cstring> #include <cstring>
#if TARGET_PC
#include "dusk/settings.h"
#endif
dMsgScrnBase_c::dMsgScrnBase_c() { dMsgScrnBase_c::dMsgScrnBase_c() {
init(); init();
} }
@@ -57,12 +61,22 @@ void dMsgScrnBase_c::init() {
} }
void dMsgScrnBase_c::multiDraw() { void dMsgScrnBase_c::multiDraw() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
if (field_0x48 != NULL) { if (field_0x48 != NULL) {
dComIfGd_set2DOpa(field_0x48); dComIfGd_set2DOpa(field_0x48);
} }
} }
void dMsgScrnBase_c::draw() { void dMsgScrnBase_c::draw() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
J2DGrafContext* ctx = dComIfGp_getCurrentGrafPort(); J2DGrafContext* ctx = dComIfGp_getCurrentGrafPort();
ctx->setup2D(); ctx->setup2D();
@@ -72,10 +86,20 @@ void dMsgScrnBase_c::draw() {
} }
void dMsgScrnBase_c::drawSelf() { void dMsgScrnBase_c::drawSelf() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
drawOutFont(0.0f, 0.0f, 1.0f); drawOutFont(0.0f, 0.0f, 1.0f);
} }
void dMsgScrnBase_c::drawOutFont(f32 param_0, f32 param_1, f32 param_2) { void dMsgScrnBase_c::drawOutFont(f32 param_0, f32 param_1, f32 param_2) {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
mpOutFont->draw(NULL, param_0, param_1, param_2); mpOutFont->draw(NULL, param_0, param_1, param_2);
} }
+9
View File
@@ -6,6 +6,10 @@
#include "d/d_msg_object.h" #include "d/d_msg_object.h"
#include "d/d_pane_class.h" #include "d/d_pane_class.h"
#if TARGET_PC
#include "dusk/settings.h"
#endif
dMsgScrnBoss_c::dMsgScrnBoss_c() { dMsgScrnBoss_c::dMsgScrnBoss_c() {
static u64 t_tag[7] = { static u64 t_tag[7] = {
MULTI_CHAR('sfontb0'), MULTI_CHAR('sfontb1'), MULTI_CHAR('sfontb2'), MULTI_CHAR('sfontl0'), MULTI_CHAR('sfontl1'), MULTI_CHAR('sfontl2'), MULTI_CHAR('sfont00'), MULTI_CHAR('sfontb0'), MULTI_CHAR('sfontb1'), MULTI_CHAR('sfontb2'), MULTI_CHAR('sfontl0'), MULTI_CHAR('sfontl1'), MULTI_CHAR('sfontl2'), MULTI_CHAR('sfont00'),
@@ -91,6 +95,11 @@ void dMsgScrnBoss_c::exec() {
} }
void dMsgScrnBoss_c::drawSelf() { void dMsgScrnBoss_c::drawSelf() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
J2DGrafContext* ctx = dComIfGp_getCurrentGrafPort(); J2DGrafContext* ctx = dComIfGp_getCurrentGrafPort();
ctx->setup2D(); ctx->setup2D();
drawOutFont(0.0f, 0.0f, 1.0f); drawOutFont(0.0f, 0.0f, 1.0f);
+9
View File
@@ -13,6 +13,10 @@
#include "d/d_msg_out_font.h" #include "d/d_msg_out_font.h"
#include "d/d_pane_class.h" #include "d/d_pane_class.h"
#if TARGET_PC
#include "dusk/settings.h"
#endif
dMsgScrnKanban_c::dMsgScrnKanban_c(JKRExpHeap* param_0) { dMsgScrnKanban_c::dMsgScrnKanban_c(JKRExpHeap* param_0) {
if (param_0 != NULL) { if (param_0 != NULL) {
field_0xd4 = param_0; field_0xd4 = param_0;
@@ -176,6 +180,11 @@ void dMsgScrnKanban_c::exec() {
} }
void dMsgScrnKanban_c::draw() { void dMsgScrnKanban_c::draw() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
J2DGrafContext* grafContext = dComIfGp_getCurrentGrafPort(); J2DGrafContext* grafContext = dComIfGp_getCurrentGrafPort();
grafContext->setup2D(); grafContext->setup2D();
mpScreen->draw(0.0f, 0.0f, grafContext); mpScreen->draw(0.0f, 0.0f, grafContext);
+9
View File
@@ -12,6 +12,10 @@
#include "d/d_msg_object.h" #include "d/d_msg_object.h"
#include "d/d_pane_class.h" #include "d/d_pane_class.h"
#if TARGET_PC
#include "dusk/settings.h"
#endif
dMsgScrnPlace_c::dMsgScrnPlace_c() { dMsgScrnPlace_c::dMsgScrnPlace_c() {
static u64 t_tag[7] = { static u64 t_tag[7] = {
MULTI_CHAR('sfontb0'), MULTI_CHAR('sfontb1'), MULTI_CHAR('sfontb2'), MULTI_CHAR('sfontl0'), MULTI_CHAR('sfontl1'), MULTI_CHAR('sfontl2'), MULTI_CHAR('sfont00'), MULTI_CHAR('sfontb0'), MULTI_CHAR('sfontb1'), MULTI_CHAR('sfontb2'), MULTI_CHAR('sfontl0'), MULTI_CHAR('sfontl1'), MULTI_CHAR('sfontl2'), MULTI_CHAR('sfont00'),
@@ -127,6 +131,11 @@ void dMsgScrnPlace_c::exec() {
} }
void dMsgScrnPlace_c::drawSelf() { void dMsgScrnPlace_c::drawSelf() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
J2DGrafContext* grafContext = dComIfGp_getCurrentGrafPort(); J2DGrafContext* grafContext = dComIfGp_getCurrentGrafPort();
grafContext->setup2D(); grafContext->setup2D();
drawOutFont(0.0f, 0.0f, 1.0f); drawOutFont(0.0f, 0.0f, 1.0f);
+9
View File
@@ -20,6 +20,10 @@
#include "JSystem/J2DGraph/J2DScreen.h" #include "JSystem/J2DGraph/J2DScreen.h"
#include <cstring> #include <cstring>
#if TARGET_PC
#include "dusk/settings.h"
#endif
dMsgScrnTalk_c::dMsgScrnTalk_c(u8 param_1, u8 param_2, JKRExpHeap* param_3) { dMsgScrnTalk_c::dMsgScrnTalk_c(u8 param_1, u8 param_2, JKRExpHeap* param_3) {
if (param_3 != NULL) { if (param_3 != NULL) {
field_0xe4 = param_3; field_0xe4 = param_3;
@@ -303,6 +307,11 @@ void dMsgScrnTalk_c::exec() {
} }
void dMsgScrnTalk_c::drawSelf() { void dMsgScrnTalk_c::drawSelf() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
J2DGrafContext* grafContext[1]; J2DGrafContext* grafContext[1];
grafContext[0] = dComIfGp_getCurrentGrafPort(); grafContext[0] = dComIfGp_getCurrentGrafPort();
grafContext[0]->setup2D(); grafContext[0]->setup2D();
+9
View File
@@ -9,6 +9,10 @@
#include "d/d_msg_out_font.h" #include "d/d_msg_out_font.h"
#include "d/d_pane_class.h" #include "d/d_pane_class.h"
#if TARGET_PC
#include "dusk/settings.h"
#endif
dMsgScrnTree_c::dMsgScrnTree_c(JUTFont* param_0, JKRExpHeap* param_1) { dMsgScrnTree_c::dMsgScrnTree_c(JUTFont* param_0, JKRExpHeap* param_1) {
if (param_1 != NULL) { if (param_1 != NULL) {
field_0xd8 = param_1; field_0xd8 = param_1;
@@ -187,6 +191,11 @@ void dMsgScrnTree_c::exec() {
} }
void dMsgScrnTree_c::draw() { void dMsgScrnTree_c::draw() {
#if TARGET_PC
if (dusk::getSettings().game.recordingMode) {
return;
}
#endif
J2DGrafContext* grafContext = dComIfGp_getCurrentGrafPort(); J2DGrafContext* grafContext = dComIfGp_getCurrentGrafPort();
grafContext->setup2D(); grafContext->setup2D();
mpScreen->draw(0.0f, 0.0f, grafContext); mpScreen->draw(0.0f, 0.0f, grafContext);
+31 -8
View File
@@ -21,16 +21,39 @@ static bool checkEnabled() {
return !__OSReport_disable || dusk::OSReportReallyForceEnable; return !__OSReport_disable || dusk::OSReportReallyForceEnable;
} }
#ifndef va_copy
#define va_copy(d, s) ((d) = (s))
#endif
static std::string FormatToString(const char* msg, va_list list) { static std::string FormatToString(const char* msg, va_list list) {
int ret = vsnprintf(nullptr, 0, msg, list); size_t size = (strlen(msg) * 2) + 50;
if (ret <= 0) { std::string str;
return {}; va_list ap;
int attempts = 0;
while (true) {
str.resize(size);
va_copy(ap, list);
int n = vsnprintf(str.data(), size, msg, ap);
va_end(ap);
if (n > -1 && n < size) {
str.resize(n);
break;
}
++attempts;
if (attempts >= 3) {
if (n == -1) {
str.clear();
}
break;
}
if (n > -1) {
size = n + 1;
} else {
size *= 2;
}
} }
++ret; return str;
std::unique_ptr<char[]> buf(new char[ret]);
vsnprintf(buf.get(), ret, msg, list);
buf[ret - 1] = '\0';
return {buf.get()};
} }
void OSReport(const char* fmt, ...) { void OSReport(const char* fmt, ...) {
+697 -138
View File
@@ -1,13 +1,21 @@
#include "dusk/achievements.h" #include "dusk/achievements.h"
#include "d/actor/d_a_alink.h"
#include "d/actor/d_a_npc4.h"
#include "d/actor/d_a_player.h"
#include "d/d_com_inf_game.h"
#include "d/d_demo.h"
#include "d/d_meter2_info.h"
#include "dusk/io.hpp" #include "dusk/io.hpp"
#include "dusk/main.h" #include "dusk/main.h"
#include "d/d_com_inf_game.h"
#include "d/d_item_data.h"
#include "d/d_map_path_fmap.h"
#include "d/d_stage.h"
#include "d/d_menu_fmap.h"
#include "JSystem/JKernel/JKRArchive.h"
#include "d/d_meter2_info.h"
#include "d/actor/d_a_alink.h"
#include "d/actor/d_a_ni.h"
#include "d/actor/d_a_npc4.h"
#include "d/actor/d_a_b_ob.h"
#include "d/actor/d_a_player.h"
#include "d/d_demo.h"
#include "dusk/ui/ui.hpp" #include "dusk/ui/ui.hpp"
#include "f_pc/f_pc_name.h"
#include "f_op/f_op_actor_mng.h" #include "f_op/f_op_actor_mng.h"
#include "f_pc/f_pc_name.h" #include "f_pc/f_pc_name.h"
@@ -18,6 +26,14 @@ namespace dusk {
using json = nlohmann::json; using json = nlohmann::json;
static void* s_cucco_play_search(void* i_actor, void*) {
if (!fopAcM_IsActor(i_actor) || fopAcM_GetName((fopAc_ac_c*)i_actor) != fpcNm_NI_e) {
return nullptr;
}
auto* ni = static_cast<ni_class*>(i_actor);
return ni->mAction == ACTION_PLAY_e ? i_actor : nullptr;
}
static void checkGoatHerding(Achievement& a, int32_t threshMs) { static void checkGoatHerding(Achievement& a, int32_t threshMs) {
if (dMeter2Info_getMaxCount() != 20 || dMeter2Info_getNowCount() != 20) { if (dMeter2Info_getMaxCount() != 20 || dMeter2Info_getNowCount() != 20) {
return; return;
@@ -32,12 +48,13 @@ static constexpr auto ACHIEVEMENTS_FILENAME = "achievements.json";
std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() { std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
return { return {
// Challenge
{ {
{ {
"hero_of_twilight", "hero_of_twilight",
"Hero of Twilight", "Hero of Twilight",
"Deliver the finishing blow to Ganondorf.", "Deliver the finishing blow to Ganondorf.",
AchievementCategory::Story, AchievementCategory::Challenge,
false, 0, 0, false false, 0, 0, false
}, },
[](Achievement& a, json&) { [](Achievement& a, json&) {
@@ -48,6 +65,445 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
}, },
{} {}
}, },
{
{
"completionist",
"Completionist",
"Complete the game after collecting all equipment, heart containers, portals, bugs, poes, and hidden skills.",
AchievementCategory::Challenge,
false, 0, 0, false
},
[](Achievement& a, json&) {
const auto* link = static_cast<const daAlink_c*>(daPy_getPlayerActorClass());
if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) {
return;
}
if (dComIfGs_getMaxLife() < 100) {
return;
}
for (int i = 0; i < 4; ++i) {
if (!dComIfGs_isCollectMirror(i)) {
return;
}
}
for (int i = 0; i < 3; ++i) {
if (!dComIfGs_isCollectCrystal(i)) {
return;
}
}
static const u16 skillBits[] = {
dSv_event_flag_c::F_0338, dSv_event_flag_c::F_0339,
dSv_event_flag_c::F_0340, dSv_event_flag_c::F_0341,
dSv_event_flag_c::F_0342, dSv_event_flag_c::F_0343,
dSv_event_flag_c::F_0344
};
for (u16 bit : skillBits) {
if (!dComIfGs_isEventBit(bit)) {
return;
}
}
if (dComIfGs_checkGetInsectNum() < 24) {
return;
}
if (dComIfGs_getPohSpiritNum() < 60) {
return;
}
if (dComIfGs_getWalletSize() < 2) {
return;
}
if (dComIfGs_getArrowMax() < 100) {
return;
}
if (!dComIfGs_isCollectSword(COLLECT_MASTER_SWORD)) {
return;
}
if (!dComIfGs_isCollectShield(COLLECT_HYLIAN_SHIELD)) {
return;
}
if (!dComIfGs_isCollectClothes(KOKIRI_CLOTHES_FLAG)) {
return;
}
if (!dComIfGs_isItemFirstBit(dItemNo_WEAR_ZORA_e)) {
return;
}
if (!dComIfGs_isItemFirstBit(dItemNo_ARMOR_e)) {
return;
}
static const struct { int stage; int sw; } warpPortals[] = {
{ dStage_SaveTbl_ORDON, 52 }, // Ordon Spring
{ dStage_SaveTbl_FARON, 71 }, // South Faron Woods
{ dStage_SaveTbl_FARON, 2 }, // North Faron Woods
{ dStage_SaveTbl_GROVE, 100 }, // Sacred Grove
{ dStage_SaveTbl_FIELD, 21 }, // Gorge
{ dStage_SaveTbl_ELDIN, 31 }, // Kakariko Village
{ dStage_SaveTbl_ELDIN, 21 }, // Death Mountain
{ dStage_SaveTbl_FIELD, 99 }, // Bridge of Eldin
{ dStage_SaveTbl_FIELD, 3 }, // Castle Town
{ dStage_SaveTbl_LANAYRU, 10 }, // Lake Hylia
{ dStage_SaveTbl_LANAYRU, 2 }, // Zora's Domain
{ dStage_SaveTbl_LANAYRU, 21 }, // Upper Zora's River
{ dStage_SaveTbl_SNOWPEAK, 21 }, // Snowpeak
{ dStage_SaveTbl_DESERT, 21 }, // Gerudo Mesa
{ dStage_SaveTbl_DESERT, 40 }, // Mirror Chamber
};
for (const auto& p : warpPortals) {
if (!g_dComIfG_gameInfo.info.getSavedata().getSave(p.stage).getBit().isSwitch(p.sw)) {
return;
}
}
if (dComIfGs_getCollectSmell() == dItemNo_NONE_e) {
return;
}
if (dMeter2Info_getRecieveLetterNum() == 0) {
return;
}
bool hasJournal = false;
for (int fi = 0; fi < 6; ++fi) {
if (dComIfGs_getFishNum(fi) != 0) {
hasJournal = true;
break;
}
}
if (!hasJournal) {
return;
}
int bottleCount = 0;
for (int i = 0; i < dSv_player_item_c::BOTTLE_MAX; ++i) {
if (dComIfGs_getItem(SLOT_11 + i, false) != dItemNo_NONE_e) {
bottleCount++;
}
}
if (bottleCount < 4) {
return;
}
int bombBagCount = 0;
for (int i = 0; i < dSv_player_item_c::BOMB_BAG_MAX; ++i) {
if (dComIfGs_getItem(SLOT_15 + i, false) != dItemNo_NONE_e) {
bombBagCount++;
}
}
if (bombBagCount < 3) {
return;
}
bool hasJewelRod = false;
for (int slot = 0; slot < 24 && !hasJewelRod; ++slot) {
const u8 item = dComIfGs_getItem(slot, false);
if (item == dItemNo_JEWEL_ROD_e || item == dItemNo_JEWEL_BEE_ROD_e || item == dItemNo_JEWEL_WORM_ROD_e) {
hasJewelRod = true;
}
}
if (!hasJewelRod) {
return;
}
static const u8 requiredWheelItems[] = {
dItemNo_BOOMERANG_e,
dItemNo_BOW_e,
dItemNo_W_HOOKSHOT_e,
dItemNo_SPINNER_e,
dItemNo_IRONBALL_e,
dItemNo_COPY_ROD_e,
dItemNo_HVY_BOOTS_e,
dItemNo_KANTERA_e,
dItemNo_PACHINKO_e,
dItemNo_HAWK_EYE_e,
dItemNo_ANCIENT_DOCUMENT_e,
dItemNo_HORSE_FLUTE_e,
};
for (u8 required : requiredWheelItems) {
bool found = false;
for (int slot = 0; slot < 24; ++slot) {
if (dComIfGs_getItem(slot, false) == required) {
found = true;
break;
}
}
if (!found) {
return;
}
}
a.progress = 1;
},
{}
},
// Collection
{
{
"princess_of_bugs",
"The Princess of Bugs",
"Deliver all 24 golden bugs to Agitha.",
AchievementCategory::Collection,
true, 24, 0, false
},
[](Achievement& a, json&) {
a.progress = dComIfGs_checkGetInsectNum();
},
{}
},
{
{
"all_poes",
"Poe Collector",
"Collect all 60 Poe Souls.",
AchievementCategory::Collection,
true, 60, 0, false
},
[](Achievement& a, json&) {
a.progress = dComIfGs_getPohSpiritNum();
},
{}
},
{
{
"hylian_loach",
"Legendary Catch",
"Catch a Hylian Loach.",
AchievementCategory::Collection,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (dComIfGs_getFishNum(1) > 0) {
a.progress = 1;
}
},
{}
},
{
{
"all_fish",
"Gone Fishin'",
"Catch all 6 species of fish.",
AchievementCategory::Collection,
true, 6, 0, false
},
[](Achievement& a, json&) {
int nUniqueFish = 0;
for (int i = 0; i < 6; ++i) {
if (dComIfGs_getFishNum(i) != 0) {
nUniqueFish++;
}
}
a.progress = nUniqueFish;
},
{}
},
{
{
"a_big_heart",
"A Big Heart",
"Reach maximum health with all 20 heart containers.",
AchievementCategory::Collection,
true, 20, 0, false
},
[](Achievement& a, json&) {
a.progress = dComIfGs_getMaxLife() / 5;
},
{}
},
{
{
"all_bottles",
"Glassware Guardian",
"Obtain all 4 bottles.",
AchievementCategory::Collection,
true, 4, 0, false
},
[](Achievement& a, json&) {
if (daPy_getPlayerActorClass() == nullptr) {
return;
}
int count = 0;
for (int i = 0; i < dSv_player_item_c::BOTTLE_MAX; ++i) {
if (dComIfGs_getItem(SLOT_11 + i, false) != dItemNo_NONE_e) {
count++;
}
}
a.progress = count;
},
{}
},
{
{
"all_hidden_skills",
"Master of Secrets",
"Learn all 7 Hidden Skills.",
AchievementCategory::Collection,
true, 7, 0, false
},
[](Achievement& a, json&) {
static const u16 skillBits[] = {
dSv_event_flag_c::F_0338, dSv_event_flag_c::F_0339,
dSv_event_flag_c::F_0340, dSv_event_flag_c::F_0341,
dSv_event_flag_c::F_0342, dSv_event_flag_c::F_0343,
dSv_event_flag_c::F_0344
};
int count = 0;
for (u16 bit : skillBits) {
if (dComIfGs_isEventBit(bit)) {
count++;
}
}
a.progress = count;
},
{}
},
{
{
"all_letters",
"We Deliver!",
"Collect all 16 postman letters.",
AchievementCategory::Collection,
true, 16, 0, false
},
[](Achievement& a, json&) {
a.progress = dMeter2Info_getRecieveLetterNum();
},
{}
},
{
{
"cave_of_ordeals",
"Conqueror of Ordeals",
"Clear all 50 floors of the Cave of Ordeals.",
AchievementCategory::Challenge,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (daNpcF_chkEvtBit(0x1F9)) {
a.progress = 1;
}
},
{}
},
{
{
"cave_of_ordeals_heartless",
"Indomitable",
"Clear all 50 floors of the Cave of Ordeals with only 3 heart containers.",
AchievementCategory::Challenge,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (daNpcF_chkEvtBit(0x1F9) && dComIfGs_getMaxLife() <= 15) {
a.progress = 1;
}
},
{}
},
{
{
"speedrun_12h",
"Been There Done That",
"Defeat Ganondorf with a total save file play time under 12 hours.",
AchievementCategory::Challenge,
false, 0, 0, false
},
[](Achievement& a, json&) {
const auto* link = static_cast<const daAlink_c*>(daPy_getPlayerActorClass());
if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) {
return;
}
const int64_t ticks = (static_cast<int64_t>(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime();
if (ticks / OS_TIMER_CLOCK < 12 * 3600) {
a.progress = 1;
}
},
{}
},
{
{
"speedrun_8h",
"Swift Blade",
"Defeat Ganondorf with a total save file play time under 6 hours.",
AchievementCategory::Challenge,
false, 0, 0, false
},
[](Achievement& a, json&) {
const auto* link = static_cast<const daAlink_c*>(daPy_getPlayerActorClass());
if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) {
return;
}
const int64_t ticks = (static_cast<int64_t>(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime();
if (ticks / OS_TIMER_CLOCK < 8 * 3600) {
a.progress = 1;
}
},
{}
},
{
{
"dark_hammer_one_hit",
"Mortal Edge",
"Defeat Dark Hammer in a single hit.",
AchievementCategory::Misc,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (AchievementSystem::get().hasSignal("dark_hammer_one_hit")) {
a.progress = 1;
}
},
{}
},
{
{
"no_deaths_clear",
"Deathless",
"Defeat Ganondorf with 0 deaths on your save file.",
AchievementCategory::Challenge,
false, 0, 0, false
},
[](Achievement& a, json&) {
const auto* link = static_cast<const daAlink_c*>(daPy_getPlayerActorClass());
if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) {
return;
}
if (dComIfGs_getDeathCount() == 0) {
a.progress = 1;
}
},
{}
},
{
{
"untouchable",
"Untouchable",
"Kill 25 enemies in a row without taking damage.",
AchievementCategory::Challenge,
true, 25, 0, false
},
[](Achievement& a, json&) {
auto& sys = AchievementSystem::get();
if (sys.hasSignal("player_damaged")) {
a.progress = 0;
}
if (sys.hasSignal("enemy_killed")) {
a.progress++;
}
},
{}
},
{
{
"bow_100m_hit",
"Long Shot",
"Hit an enemy from over 100 meters away with the bow.",
AchievementCategory::Misc,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (AchievementSystem::get().hasSignal("arrow_hit_100m")) {
a.progress = 1;
}
},
{}
},
// Minigame
{ {
{ {
"plumm_max", "plumm_max",
@@ -134,14 +590,16 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
}, },
{ {
{ {
"cave_of_ordeals", "snowboard_70s",
"Conqueror of Ordeals", "Downhill Dash",
"Clear all 50 floors of the Cave of Ordeals.", "Finish the snowboarding minigame in under 70 seconds.",
AchievementCategory::Challenge, AchievementCategory::Minigame,
false, 0, 0, false false, 0, 0, false
}, },
[](Achievement& a, json&) { [](Achievement& a, json&) {
if (daNpcF_chkEvtBit(0x1F9)) { const int32_t bestMs = dComIfGs_getRaceGameTime();
if (dComIfGs_isEventBit(dSv_event_flag_c::F_0481) &&
bestMs > 0 && bestMs <= 70000) {
a.progress = 1; a.progress = 1;
} }
}, },
@@ -149,54 +607,37 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
}, },
{ {
{ {
"cave_of_ordeals_heartless", "canoe_perfect",
"Indomitable", "River Raider",
"Clear all 50 floors of the Cave of Ordeals with only 3 heart containers.", "Achieve a perfect score in the canoe minigame.",
AchievementCategory::Challenge, AchievementCategory::Minigame,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (daNpcF_chkEvtBit(0x1F9) && dComIfGs_getMaxLife() <= 15) {
a.progress = 1;
}
},
{}
},
{
{
"speedrun_12h",
"Been There Done That",
"Defeat Ganondorf with a total save file play time under 12 hours.",
AchievementCategory::Challenge,
false, 0, 0, false false, 0, 0, false
}, },
[](Achievement& a, json&) { [](Achievement& a, json&) {
const auto* link = static_cast<const daAlink_c*>(daPy_getPlayerActorClass()); const auto* link = static_cast<const daAlink_c*>(daPy_getPlayerActorClass());
if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) { if (link == nullptr) {
return; return;
} }
const int64_t ticks = (static_cast<int64_t>(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime(); static bool wasInCanoe = false;
if (ticks / OS_TIMER_CLOCK < 12 * 3600) { bool inCanoe = link->mProcID >= daAlink_c::PROC_CANOE_RIDE &&
link->mProcID <= daAlink_c::PROC_CANOE_KANDELAAR_POUR;
if (wasInCanoe && !inCanoe && dMeter2Info_getNowCount() >= 30) {
a.progress = 1; a.progress = 1;
} }
wasInCanoe = inCanoe;
}, },
{} {}
}, },
{ {
{ {
"speedrun_8h", "star_2_under_40s",
"Swift Blade", "Rising Star",
"Defeat Ganondorf with a total save file play time under 6 hours.", "Complete the STAR Prize 2 minigame in under 40 seconds.",
AchievementCategory::Challenge, AchievementCategory::Minigame,
false, 0, 0, false false, 0, 0, false
}, },
[](Achievement& a, json&) { [](Achievement& a, json&) {
const auto* link = static_cast<const daAlink_c*>(daPy_getPlayerActorClass()); if(dComIfGs_getHookGameTime() > 0 && dComIfGs_getHookGameTime() <= 40000) {
if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) {
return;
}
const int64_t ticks = (static_cast<int64_t>(OSGetTime()) - dComIfGs_getSaveStartTime()) + dComIfGs_getSaveTotalTime();
if (ticks / OS_TIMER_CLOCK < 8 * 3600) {
a.progress = 1; a.progress = 1;
} }
}, },
@@ -204,77 +645,20 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
}, },
{ {
{ {
"princess_of_bugs", "star_2_under_30s",
"The Princess of Bugs", "Shooting Star",
"Deliver all 24 golden bugs to Agitha.", "Complete the STAR Prize 2 minigame in under 30 seconds.",
AchievementCategory::Collection, AchievementCategory::Minigame,
true, 24, 0, false
},
[](Achievement& a, json&) {
a.progress = dComIfGs_checkGetInsectNum();
},
{}
},
{
{
"all_poes",
"Poe Collector",
"Collect all 60 Poe Souls.",
AchievementCategory::Collection,
true, 60, 0, false
},
[](Achievement& a, json&) {
a.progress = dComIfGs_getPohSpiritNum();
},
{}
},
{
{
"hylian_loach",
"Legendary Catch",
"Catch a Hylian Loach.",
AchievementCategory::Collection,
false, 0, 0, false false, 0, 0, false
}, },
[](Achievement& a, json&) { [](Achievement& a, json&) {
if (dComIfGs_getFishNum(1) > 0) { if(dComIfGs_getHookGameTime() > 0 && dComIfGs_getHookGameTime() <= 30000) {
a.progress = 1; a.progress = 1;
} }
}, },
{} {}
}, },
{ // Misc
{
"all_fish",
"Gone Fishin'",
"Catch all 6 species of fish.",
AchievementCategory::Collection,
true, 6, 0, false
},
[](Achievement& a, json&) {
int nUniqueFish = 0;
for (int i = 0; i < 6; ++i) {
if (dComIfGs_getFishNum(i) != 0) {
nUniqueFish++;
}
}
a.progress = nUniqueFish;
},
{}
},
{
{
"a_big_heart",
"A Big Heart",
"Reach maximum health with all 20 heart containers.",
AchievementCategory::Collection,
true, 20, 0, false
},
[](Achievement& a, json&) {
a.progress = dComIfGs_getMaxLife() / 5;
},
{}
},
{ {
{ {
"friendly_fire", "friendly_fire",
@@ -327,6 +711,87 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
}, },
{} {}
}, },
{
{
"email_me",
"Email Me",
"Read a letter during the Dark Beast Ganon fight.",
AchievementCategory::Misc,
false, 0, 0, false
},
[](Achievement& a, json&) {
void* dbgExists = fopAcM_SearchByName(fpcNm_B_MGN_e);
if (dbgExists && AchievementSystem::get().hasSignal("open_letter")) {
a.progress = 1;
}
},
{}
},
{
{
"heavy_hitter",
"Heavy Hitter",
"Wear the Iron Boots during the end credits.",
AchievementCategory::Misc,
false, 0, 0, false
},
[](Achievement& a, json&) {
const auto* link = static_cast<const daAlink_c*>(daPy_getPlayerActorClass());
if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) {
return;
}
if (daPy_getPlayerActorClass()->checkEquipHeavyBoots()) {
a.progress = 1;
}
},
{}
},
{
{
"fishing_rod_ganondorf",
"Here Fishy Fishy",
"Confuse Ganondorf with the fishing rod.",
AchievementCategory::Misc,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (AchievementSystem::get().hasSignal("ganondorf_fishing_rod")) {
a.progress = 1;
}
},
{}
},
{
{
"steal_from_trill",
"Petty Theft",
"Steal from Trill.",
AchievementCategory::Misc,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (dComIfGs_isEventBit(dSv_event_flag_c::F_0758)) {
a.progress = 1;
}
},
{}
},
{
{
"cucco_control",
"Cucco Whisperer",
"Take control of a cucco.",
AchievementCategory::Misc,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (fopAcM_Search(s_cucco_play_search, nullptr) != nullptr) {
a.progress = 1;
}
},
{}
},
// Glitched
{ {
{ {
"back_in_time", "back_in_time",
@@ -377,19 +842,6 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
}, },
{} {}
}, },
{
{
"ultimate_delivery",
"The Ultimate Delivery",
"Have all 16 postman letters at the same time.",
AchievementCategory::Glitched,
true, 16, 0, false
},
[](Achievement& a, json&) {
a.progress = dMeter2Info_getRecieveLetterNum();
},
{}
},
{ {
{ {
"speedrun_4h", "speedrun_4h",
@@ -412,15 +864,85 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
}, },
{ {
{ {
"email_me", "no_fish_suit",
"Email Me", "No Fish Suit No Problem",
"Read a letter during the Dark Beast Ganon fight.", "Defeat Morpheel without equipping Zora Armor.",
AchievementCategory::Misc, AchievementCategory::Glitched,
false, 0, 0, false false, 0, 0, false
}, },
[](Achievement& a, json&) { [](Achievement& a, json&) {
void* dbgExists = fopAcM_SearchByName(fpcNm_B_MGN_e); static bool prevMorpheelAlive = false;
if (dbgExists && AchievementSystem::get().hasSignal("open_letter")) { static bool inArena = false;
static bool zoraWorn = false;
const auto* morpheel = static_cast<const b_ob_class*>(fopAcM_SearchByName(fpcNm_B_OB_e));
const bool morpheelAlive = morpheel != nullptr && morpheel->mAnmID != 0x14;
const bool morpheelDead = morpheel != nullptr && morpheel->mAnmID == 0x14;
const bool lakebedCleared = dComIfGs_isEventBit(dSv_event_flag_c::M_045);
if (inArena && morpheel == nullptr) {
zoraWorn = false;
}
if (morpheelAlive && !lakebedCleared) {
inArena = true;
if (daPy_py_c::checkZoraWearFlg()) {
zoraWorn = true;
}
}
if (prevMorpheelAlive && morpheelDead && inArena && !zoraWorn) {
a.progress = 1;
}
prevMorpheelAlive = morpheelAlive;
},
{}
},
{
{
"null_item",
"Null Item",
"Obtain the mysterious black rupee in the item wheel.",
AchievementCategory::Glitched,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (daPy_getPlayerActorClass() == nullptr) {
return;
}
for (int i = 0; i < 24; ++i) {
if (dComIfGs_getItem(i, false) == 0x00) {
a.progress = 1;
break;
}
}
},
{}
},
{
{
"stallord_skip",
"Stallord Skip",
"Leave Stallord's arena through the exit without defeating Stallord.",
AchievementCategory::Glitched,
false, 0, 0, false
},
[](Achievement& a, json&) {
static bool seenStallord = false;
if (strcmp(dComIfGp_getStartStageName(), "D_MN10A") != 0) {
seenStallord = false;
return;
}
if (dComIfGs_isEventBit(dSv_event_flag_c::F_0265)) {
seenStallord = false;
return;
}
if (fopAcM_SearchByName(fpcNm_B_DS_e) != nullptr) {
seenStallord = true;
}
if (seenStallord &&
dComIfGp_isEnableNextStage() &&
strcmp(dComIfGp_getNextStageName(), "F_SP125") == 0) {
a.progress = 1; a.progress = 1;
} }
}, },
@@ -428,22 +950,59 @@ std::vector<AchievementSystem::Entry> AchievementSystem::makeEntries() {
}, },
{ {
{ {
"heavy-hitter", "lakebed_before_lanayru",
"Heavy Hitter", "White Midna Glitch",
"Wear the Iron Boots during the end credits.", "Clear the Lakebed Temple before clearing Lanayru's Twilight.",
AchievementCategory::Misc, AchievementCategory::Glitched,
false, 0, 0, false false, 0, 0, false
}, },
[](Achievement& a, json&) { [](Achievement& a, json&) {
const auto* link = static_cast<const daAlink_c*>(daPy_getPlayerActorClass()); if (dComIfGs_isEventBit(dSv_event_flag_c::M_045) &&
if (link == nullptr || link->mProcID != daAlink_c::PROC_GANON_FINISH) { !dComIfGs_isDarkClearLV(2)) {
return;
}
if (daPy_getPlayerActorClass()->checkEquipHeavyBoots()) {
a.progress = 1; a.progress = 1;
} }
}, },
{} {}
},
{
{
"early_hidden_village",
"Quick Detour",
"Rescue the Hidden Village before clearing Goron Mines.",
AchievementCategory::Glitched,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (dComIfGs_isEventBit(dSv_event_flag_c::F_0278) &&
!dComIfGs_isEventBit(dSv_event_flag_c::M_031)) {
a.progress = 1;
}
},
{}
},
{
{
"forest_temple_no_boomerang",
"Must Have Been The Wind",
"Complete the Forest Temple without obtaining the Gale Boomerang.",
AchievementCategory::Glitched,
false, 0, 0, false
},
[](Achievement& a, json&) {
if (!dComIfGs_isEventBit(dSv_event_flag_c::M_022)) {
return;
}
if (daPy_getPlayerActorClass() == nullptr) {
return;
}
for (int i = 0; i < 24; ++i) {
if (dComIfGs_getItem(i, false) == dItemNo_BOOMERANG_e) {
return;
}
}
a.progress = 1;
},
{}
} }
}; };
} }
@@ -471,7 +1030,7 @@ void AchievementSystem::load() {
return; return;
} }
try { try {
auto data = io::FileStream::ReadAllBytes(filePath.string().c_str()); auto data = io::FileStream::ReadAllBytes(filePath);
auto j = json::parse(data); auto j = json::parse(data);
if (!j.is_object()) { if (!j.is_object()) {
return; return;
@@ -508,7 +1067,7 @@ void AchievementSystem::save() {
} }
try { try {
io::FileStream::WriteAllText( io::FileStream::WriteAllText(
(dusk::ConfigPath / ACHIEVEMENTS_FILENAME).string().c_str(), dusk::ConfigPath / ACHIEVEMENTS_FILENAME,
j.dump(2) j.dump(2)
); );
} catch (const std::exception&) {} } catch (const std::exception&) {}
@@ -554,7 +1113,7 @@ void AchievementSystem::processEntry(Entry& e) {
if (nowUnlocked) { if (nowUnlocked) {
e.achievement.progress = e.achievement.isCounter ? e.achievement.goal : 1; e.achievement.progress = e.achievement.isCounter ? e.achievement.goal : 1;
e.achievement.unlocked = true; e.achievement.unlocked = true;
if (getSettings().game.enableAchievementNotifications) { if (getSettings().game.enableAchievementToasts) {
ui::push_toast({ ui::push_toast({
.type = "achievement", .type = "achievement",
.title = "Achievement Unlocked!", .title = "Achievement Unlocked!",
+8 -5
View File
@@ -23,8 +23,8 @@ aurora::Module DuskConfigLog("dusk::config");
static absl::flat_hash_map<std::string_view, ConfigVarBase*> RegisteredConfigVars; static absl::flat_hash_map<std::string_view, ConfigVarBase*> RegisteredConfigVars;
static bool RegistrationDone = false; static bool RegistrationDone = false;
static std::string GetConfigJsonPath() { static std::u8string GetConfigJsonPath() {
return (dusk::ConfigPath / ConfigFileName).string(); return (dusk::ConfigPath / ConfigFileName).u8string();
} }
ConfigVarBase::ConfigVarBase(const char* name, const ConfigImplBase* impl) : name(name), registered(false), layer(ConfigVarLayer::Default), impl(impl) { ConfigVarBase::ConfigVarBase(const char* name, const ConfigImplBase* impl) : name(name), registered(false), layer(ConfigVarLayer::Default), impl(impl) {
@@ -156,6 +156,7 @@ namespace dusk::config {
template class ConfigImpl<dusk::BloomMode>; template class ConfigImpl<dusk::BloomMode>;
template class ConfigImpl<dusk::DiscVerificationState>; template class ConfigImpl<dusk::DiscVerificationState>;
template class ConfigImpl<dusk::GameLanguage>; template class ConfigImpl<dusk::GameLanguage>;
template class ConfigImpl<dusk::GyroMode>;
} }
void dusk::config::Register(ConfigVarBase& configVar) { void dusk::config::Register(ConfigVarBase& configVar) {
@@ -188,7 +189,7 @@ void dusk::config::LoadFromUserPreferences() {
if (configJsonPath.empty()) { if (configJsonPath.empty()) {
return; return;
} }
LoadFromFileName(configJsonPath.c_str()); LoadFromFileName(reinterpret_cast<const char*>(configJsonPath.c_str()));
} }
static void LoadFromPath(const char* path) { static void LoadFromPath(const char* path) {
@@ -240,7 +241,9 @@ void dusk::config::Save() {
return; return;
} }
DuskConfigLog.info("Saving config to '{}'", configJsonPath); DuskConfigLog.info(
"Saving config to '{}'",
reinterpret_cast<const char*>(configJsonPath.c_str()));
json j; json j;
@@ -250,7 +253,7 @@ 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));
} }
ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) { ConfigVarBase* dusk::config::GetConfigVar(std::string_view name) {
+105
View File
@@ -1,9 +1,16 @@
#include "file_select.hpp" #include "file_select.hpp"
#include <memory> #include <memory>
#include <string_view>
#include <SDL3/SDL_dialog.h> #include <SDL3/SDL_dialog.h>
#include <SDL3/SDL_error.h> #include <SDL3/SDL_error.h>
#include <SDL3/SDL_stdinc.h>
#if defined(__ANDROID__) || defined(ANDROID)
#include <SDL3/SDL_system.h>
#include <jni.h>
#endif
#if defined(__APPLE__) #if defined(__APPLE__)
#include <TargetConditionals.h> #include <TargetConditionals.h>
@@ -19,6 +26,92 @@
namespace dusk { namespace dusk {
namespace { namespace {
std::string fallback_display_name(std::string_view path) {
if (path.empty()) {
return {};
}
std::string pathString(path);
const std::size_t slash = pathString.find_last_of("/\\");
if (slash == std::string::npos || slash + 1 >= pathString.size()) {
return pathString;
}
return pathString.substr(slash + 1);
}
#if defined(__ANDROID__) || defined(ANDROID)
bool clear_pending_exception(JNIEnv* env) {
if (env == nullptr || !env->ExceptionCheck()) {
return false;
}
env->ExceptionClear();
return true;
}
std::string to_string(JNIEnv* env, jstring value) {
if (env == nullptr || value == nullptr) {
return {};
}
const char* utf8 = env->GetStringUTFChars(value, nullptr);
if (utf8 == nullptr) {
clear_pending_exception(env);
return {};
}
std::string result(utf8);
env->ReleaseStringUTFChars(value, utf8);
return result;
}
std::string android_display_name(std::string_view path) {
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
if (env == nullptr) {
return {};
}
jobject activity = static_cast<jobject>(SDL_GetAndroidActivity());
if (activity == nullptr || clear_pending_exception(env)) {
if (activity != nullptr) {
env->DeleteLocalRef(activity);
}
return {};
}
jclass activityClass = env->GetObjectClass(activity);
if (activityClass == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(activity);
return {};
}
jmethodID getDisplayName = env->GetMethodID(
activityClass, "getDisplayNameForUri", "(Ljava/lang/String;)Ljava/lang/String;");
env->DeleteLocalRef(activityClass);
if (getDisplayName == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(activity);
return {};
}
jstring uri = env->NewStringUTF(std::string(path).c_str());
if (uri == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(activity);
return {};
}
auto* displayName =
static_cast<jstring>(env->CallObjectMethod(activity, getDisplayName, uri));
env->DeleteLocalRef(uri);
env->DeleteLocalRef(activity);
if (displayName == nullptr || clear_pending_exception(env)) {
return {};
}
std::string result = to_string(env, displayName);
env->DeleteLocalRef(displayName);
return result;
}
#endif
#if USE_IOS_DIALOG #if USE_IOS_DIALOG
struct IOSDialogCallbackState { struct IOSDialogCallbackState {
FileCallback callback; FileCallback callback;
@@ -88,4 +181,16 @@ void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window,
default_location, allow_many); default_location, allow_many);
#endif #endif
} }
std::string display_name_for_path(std::string_view path) {
#if defined(__ANDROID__) || defined(ANDROID)
if (path.starts_with("content:") || path.starts_with("file:")) {
std::string displayName = android_display_name(path);
if (!displayName.empty()) {
return displayName;
}
}
#endif
return fallback_display_name(path);
}
} // namespace dusk } // namespace dusk
+7 -2
View File
@@ -2,6 +2,9 @@
#include <SDL3/SDL_dialog.h> #include <SDL3/SDL_dialog.h>
#include <string>
#include <string_view>
struct SDL_Window; struct SDL_Window;
namespace dusk { namespace dusk {
@@ -9,7 +12,9 @@ namespace dusk {
using FileCallback = void (*)(void* userdata, const char* path, const char* error); using FileCallback = void (*)(void* userdata, const char* path, const char* error);
void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window, void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window,
const SDL_DialogFileFilter* filters, int nfilters, const char* default_location, const SDL_DialogFileFilter* filters, int nfilters, const char* default_location,
bool allow_many); bool allow_many);
std::string display_name_for_path(std::string_view path);
} // namespace dusk } // namespace dusk
+81 -20
View File
@@ -1,5 +1,9 @@
#include "dusk/gyro.h" #include "dusk/gyro.h"
#include "dusk/ui/ui.hpp"
#include "d/actor/d_a_alink.h" #include "d/actor/d_a_alink.h"
#include <aurora/lib/window.hpp>
#include <SDL3/SDL_mouse.h>
#include <cmath> #include <cmath>
namespace dusk::gyro { namespace dusk::gyro {
@@ -12,11 +16,14 @@ constexpr float kGravityEmaAlpha = 0.1f;
constexpr float kMinGravityProjection = 0.2f; constexpr float kMinGravityProjection = 0.2f;
// Let roll contribute more strongly as the pad approaches an upright posture. // Let roll contribute more strongly as the pad approaches an upright posture.
constexpr float kRollAimBoostMax = 2.0f; constexpr float kRollAimBoostMax = 2.0f;
constexpr float kMousePixelToRad = 0.0025f;
bool s_sensor_enabled = false; bool s_sensor_enabled = false;
bool s_accel_enabled = false; bool s_accel_enabled = false;
bool s_was_aiming = false; bool s_was_aiming = false;
bool s_have_gravity_baseline = false; bool s_have_gravity_baseline = false;
bool s_mouse_enabled = false;
bool s_mouse_relative = false;
float s_smooth_gx = 0.0f; float s_smooth_gx = 0.0f;
float s_smooth_gy = 0.0f; float s_smooth_gy = 0.0f;
float s_smooth_gz = 0.0f; float s_smooth_gz = 0.0f;
@@ -36,6 +43,7 @@ void reset_filter_state() {
s_baseline_gravity_y = s_baseline_gravity_z = 0.0f; s_baseline_gravity_y = s_baseline_gravity_z = 0.0f;
s_was_aiming = false; s_was_aiming = false;
s_have_gravity_baseline = false; s_have_gravity_baseline = false;
s_mouse_enabled = false;
s_yaw_rad = s_pitch_rad = s_roll_rad = 0.0f; s_yaw_rad = s_pitch_rad = s_roll_rad = 0.0f;
s_rollgoal_ax = s_rollgoal_az = 0; s_rollgoal_ax = s_rollgoal_az = 0;
} }
@@ -46,14 +54,29 @@ float apply_deadband(float v, float deadband_rad_s) {
} }
return v; return v;
} }
void disable_pad_sensors() {
if (s_sensor_enabled) {
PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_GYRO, FALSE);
s_sensor_enabled = false;
}
if (s_accel_enabled) {
PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_ACCEL, FALSE);
s_accel_enabled = false;
}
}
} // namespace } // namespace
bool s_sensor_keep_alive = false; bool s_sensor_keep_alive = false;
bool get_sensor_keep_alive() { return s_sensor_keep_alive; } bool get_sensor_keep_alive() { return s_sensor_keep_alive; }
void set_sensor_keep_alive(bool value) { s_sensor_keep_alive = value; } void set_sensor_keep_alive(bool value) { s_sensor_keep_alive = value; }
bool rollgoal_gyro_enabled() {
return getSettings().game.enableGyroRollgoal && getSettings().game.gyroMode.getValue() != GyroMode::Mouse;
}
bool queryGyroAimContext() { bool queryGyroAimContext() {
if (!static_cast<bool>(dusk::getSettings().game.enableGyroAim)) { if (!static_cast<bool>(getSettings().game.enableGyroAim)) {
return false; return false;
} }
@@ -71,15 +94,28 @@ void read(float dt) {
const bool aim_just_ended = !aim_active && s_was_aiming; const bool aim_just_ended = !aim_active && s_was_aiming;
s_was_aiming = aim_active; s_was_aiming = aim_active;
const bool mouse_mode = getSettings().game.gyroMode.getValue() == GyroMode::Mouse;
const bool mouse_gyro_active = !ui::any_document_visible() && mouse_mode && (aim_active || s_sensor_keep_alive);
SDL_Window* window = aurora::window::get_sdl_window();
if (window != nullptr && mouse_gyro_active != s_mouse_relative &&
SDL_SetWindowRelativeMouseMode(window, mouse_gyro_active))
{
s_mouse_relative = mouse_gyro_active;
}
if (mouse_gyro_active && !s_mouse_enabled && window != nullptr) {
const AuroraWindowSize sz = aurora::window::get_window_size();
const float cx = static_cast<float>(sz.width) * 0.5f;
const float cy = static_cast<float>(sz.height) * 0.5f;
SDL_WarpMouseInWindow(window, cx, cy);
float discard_x = 0.0f;
float discard_y = 0.0f;
SDL_GetRelativeMouseState(&discard_x, &discard_y);
}
s_mouse_enabled = mouse_gyro_active;
if (!s_sensor_keep_alive && !aim_active) { if (!s_sensor_keep_alive && !aim_active) {
if (s_sensor_enabled) { disable_pad_sensors();
PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_GYRO, FALSE);
s_sensor_enabled = false;
}
if (s_accel_enabled) {
PADSetSensorEnabled(PAD_CHAN0, PAD_SENSOR_ACCEL, FALSE);
s_accel_enabled = false;
}
reset_filter_state(); reset_filter_state();
return; return;
} }
@@ -90,6 +126,31 @@ void read(float dt) {
s_have_gravity_baseline = false; s_have_gravity_baseline = false;
} }
if (mouse_mode && !mouse_gyro_active) {
s_pitch_rad = 0.0f;
s_yaw_rad = 0.0f;
s_roll_rad = 0.0f;
return;
}
if (mouse_mode) {
disable_pad_sensors();
float mx_rel = 0.0f;
float my_rel = 0.0f;
SDL_GetRelativeMouseState(&mx_rel, &my_rel);
// Convert pixels to radians
s_pitch_rad = my_rel * kMousePixelToRad * getSettings().game.gyroSensitivityY;
s_yaw_rad = -mx_rel * kMousePixelToRad * getSettings().game.gyroSensitivityX;
s_roll_rad = 0.0f;
s_pitch_rad = getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad;
s_yaw_rad = getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad;
s_yaw_rad = getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad;
return;
}
if (!s_sensor_enabled) { if (!s_sensor_enabled) {
if (!PADHasSensor(PAD_CHAN0, PAD_SENSOR_GYRO)) { if (!PADHasSensor(PAD_CHAN0, PAD_SENSOR_GYRO)) {
return; return;
@@ -112,8 +173,8 @@ void read(float dt) {
return; return;
} }
const float smooth_alpha = kGyroEmaAlphaMax + dusk::getSettings().game.gyroSmoothing * (kGyroEmaAlphaMin - kGyroEmaAlphaMax); const float smooth_alpha = kGyroEmaAlphaMax + getSettings().game.gyroSmoothing * (kGyroEmaAlphaMin - kGyroEmaAlphaMax);
const float deadband = dusk::getSettings().game.gyroDeadband; const float deadband = getSettings().game.gyroDeadband;
s_smooth_gx += smooth_alpha * (gyro[0] - s_smooth_gx); s_smooth_gx += smooth_alpha * (gyro[0] - s_smooth_gx);
s_smooth_gy += smooth_alpha * (gyro[1] - s_smooth_gy); s_smooth_gy += smooth_alpha * (gyro[1] - s_smooth_gy);
@@ -123,8 +184,8 @@ void read(float dt) {
const float yaw_rate = apply_deadband(s_smooth_gy, deadband); const float yaw_rate = apply_deadband(s_smooth_gy, deadband);
const float roll_rate = apply_deadband(s_smooth_gz, deadband); const float roll_rate = apply_deadband(s_smooth_gz, deadband);
s_pitch_rad = -pitch_rate * dt * dusk::getSettings().game.gyroSensitivityX; s_pitch_rad = -pitch_rate * dt * getSettings().game.gyroSensitivityY;
s_roll_rad = roll_rate * dt * dusk::getSettings().game.gyroSensitivityX; // GYRO NOTE: Exposing Z sensitivity seems unusual, so I'm just using X 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; float horizontal_rate = yaw_rate;
if (aim_active && s_accel_enabled) { if (aim_active && s_accel_enabled) {
@@ -162,11 +223,11 @@ void read(float dt) {
} }
} }
s_yaw_rad = horizontal_rate * dt * dusk::getSettings().game.gyroSensitivityY; s_yaw_rad = horizontal_rate * dt * getSettings().game.gyroSensitivityX;
s_pitch_rad = dusk::getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad; s_pitch_rad = getSettings().game.gyroInvertPitch ? -s_pitch_rad : s_pitch_rad;
s_yaw_rad = dusk::getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad; s_yaw_rad = getSettings().game.gyroInvertYaw ? -s_yaw_rad : s_yaw_rad;
s_yaw_rad = dusk::getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad; s_yaw_rad = getSettings().game.enableMirrorMode ? -s_yaw_rad : s_yaw_rad;
} }
void getAimDeltas(float& out_yaw, float& out_pitch) { void getAimDeltas(float& out_yaw, float& out_pitch) {
@@ -180,9 +241,9 @@ void rollgoalTick(bool play_active, s16 camera_yaw) {
return; return;
} }
float pitch_rad = -s_pitch_rad * dusk::getSettings().game.gyroSensitivityRollgoal; float pitch_rad = -s_pitch_rad * getSettings().game.gyroSensitivityRollgoal;
float roll_rad = s_roll_rad * dusk::getSettings().game.gyroSensitivityRollgoal; float roll_rad = s_roll_rad * getSettings().game.gyroSensitivityRollgoal;
roll_rad = dusk::getSettings().game.enableMirrorMode ? -roll_rad : roll_rad; roll_rad = getSettings().game.enableMirrorMode ? -roll_rad : roll_rad;
s_rollgoal_az += cM_rad2s(roll_rad); s_rollgoal_az += cM_rad2s(roll_rad);
cXyz in(roll_rad, 0.0f, pitch_rad); cXyz in(roll_rad, 0.0f, pitch_rad);
+402
View File
@@ -0,0 +1,402 @@
#include "http.hpp"
#include <SDL3/SDL_system.h>
#include <jni.h>
#include <algorithm>
#include <limits>
#include <string_view>
#include <utility>
namespace dusk::http {
namespace {
constexpr int JavaErrorNone = 0;
constexpr int JavaErrorInvalidUrl = 1;
constexpr int JavaErrorUnsupportedScheme = 2;
constexpr int JavaErrorTimeout = 3;
constexpr int JavaErrorTooLarge = 4;
int timeout_ms(std::chrono::milliseconds timeout) {
const auto count = std::max<std::chrono::milliseconds::rep>(1, timeout.count());
return static_cast<int>(
std::min<std::chrono::milliseconds::rep>(count, std::numeric_limits<int>::max()));
}
jlong max_body_bytes(size_t maxBodyBytes) {
return static_cast<jlong>(std::min<size_t>(
maxBodyBytes, static_cast<size_t>(std::numeric_limits<jlong>::max())));
}
bool clear_pending_exception(JNIEnv* env) {
if (env == nullptr || !env->ExceptionCheck()) {
return false;
}
env->ExceptionClear();
return true;
}
std::string to_string(JNIEnv* env, jstring value) {
if (env == nullptr || value == nullptr) {
return {};
}
const char* utf8 = env->GetStringUTFChars(value, nullptr);
if (utf8 == nullptr) {
clear_pending_exception(env);
return {};
}
std::string result(utf8);
env->ReleaseStringUTFChars(value, utf8);
return result;
}
jstring to_jstring(JNIEnv* env, std::string_view value) {
if (env == nullptr) {
return nullptr;
}
return env->NewStringUTF(std::string(value).c_str());
}
Error map_java_error(int error) {
switch (error) {
case JavaErrorNone:
return Error::None;
case JavaErrorInvalidUrl:
return Error::InvalidUrl;
case JavaErrorUnsupportedScheme:
return Error::UnsupportedScheme;
case JavaErrorTimeout:
return Error::Timeout;
case JavaErrorTooLarge:
return Error::TooLarge;
default:
return Error::Network;
}
}
jclass load_dusk_class(JNIEnv* env, jobject activity, const char* className) {
jclass activityClass = env->GetObjectClass(activity);
if (activityClass == nullptr || clear_pending_exception(env)) {
return nullptr;
}
jmethodID getClassLoader =
env->GetMethodID(activityClass, "getClassLoader", "()Ljava/lang/ClassLoader;");
env->DeleteLocalRef(activityClass);
if (getClassLoader == nullptr || clear_pending_exception(env)) {
return nullptr;
}
jobject classLoader = env->CallObjectMethod(activity, getClassLoader);
if (classLoader == nullptr || clear_pending_exception(env)) {
return nullptr;
}
jclass classLoaderClass = env->FindClass("java/lang/ClassLoader");
if (classLoaderClass == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(classLoader);
return nullptr;
}
jmethodID loadClass = env->GetMethodID(
classLoaderClass, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;");
env->DeleteLocalRef(classLoaderClass);
if (loadClass == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(classLoader);
return nullptr;
}
jstring javaClassName = env->NewStringUTF(className);
if (javaClassName == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(classLoader);
return nullptr;
}
auto* loadedClass =
static_cast<jclass>(env->CallObjectMethod(classLoader, loadClass, javaClassName));
env->DeleteLocalRef(javaClassName);
env->DeleteLocalRef(classLoader);
if (loadedClass == nullptr || clear_pending_exception(env)) {
return nullptr;
}
return loadedClass;
}
jobjectArray make_string_array(JNIEnv* env, const std::vector<Header>& headers, bool names) {
jclass stringClass = env->FindClass("java/lang/String");
if (stringClass == nullptr || clear_pending_exception(env)) {
return nullptr;
}
jobjectArray array =
env->NewObjectArray(static_cast<jsize>(headers.size()), stringClass, nullptr);
env->DeleteLocalRef(stringClass);
if (array == nullptr || clear_pending_exception(env)) {
return nullptr;
}
for (jsize i = 0; i < static_cast<jsize>(headers.size()); ++i) {
const std::string& value = names ? headers[static_cast<size_t>(i)].name :
headers[static_cast<size_t>(i)].value;
jstring javaValue = to_jstring(env, value);
if (javaValue == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(array);
return nullptr;
}
env->SetObjectArrayElement(array, i, javaValue);
env->DeleteLocalRef(javaValue);
if (clear_pending_exception(env)) {
env->DeleteLocalRef(array);
return nullptr;
}
}
return array;
}
std::vector<Header> read_headers(JNIEnv* env, jobjectArray names, jobjectArray values) {
std::vector<Header> headers;
if (names == nullptr || values == nullptr) {
return headers;
}
const jsize count = std::min(env->GetArrayLength(names), env->GetArrayLength(values));
headers.reserve(static_cast<size_t>(count));
for (jsize i = 0; i < count; ++i) {
auto* name = static_cast<jstring>(env->GetObjectArrayElement(names, i));
auto* value = static_cast<jstring>(env->GetObjectArrayElement(values, i));
if (clear_pending_exception(env)) {
if (name != nullptr) {
env->DeleteLocalRef(name);
}
if (value != nullptr) {
env->DeleteLocalRef(value);
}
headers.clear();
return headers;
}
if (name != nullptr) {
headers.push_back({
.name = to_string(env, name),
.value = to_string(env, value),
});
}
if (name != nullptr) {
env->DeleteLocalRef(name);
}
if (value != nullptr) {
env->DeleteLocalRef(value);
}
}
return headers;
}
std::string read_body(JNIEnv* env, jbyteArray body) {
if (body == nullptr) {
return {};
}
const jsize bodySize = env->GetArrayLength(body);
std::string result(static_cast<size_t>(bodySize), '\0');
if (bodySize > 0) {
env->GetByteArrayRegion(body, 0, bodySize, reinterpret_cast<jbyte*>(result.data()));
if (clear_pending_exception(env)) {
return {};
}
}
return result;
}
Result result_from_response(JNIEnv* env, jobject response) {
if (response == nullptr) {
return {
.error = Error::Network,
.message = "Android HTTP request did not return a response",
};
}
jclass responseClass = env->GetObjectClass(response);
if (responseClass == nullptr || clear_pending_exception(env)) {
return {
.error = Error::Network,
.message = "Failed to inspect Android HTTP response",
};
}
jfieldID errorField = env->GetFieldID(responseClass, "error", "I");
jfieldID messageField = env->GetFieldID(responseClass, "message", "Ljava/lang/String;");
jfieldID statusField = env->GetFieldID(responseClass, "statusCode", "I");
jfieldID headerNamesField =
env->GetFieldID(responseClass, "headerNames", "[Ljava/lang/String;");
jfieldID headerValuesField =
env->GetFieldID(responseClass, "headerValues", "[Ljava/lang/String;");
jfieldID bodyField = env->GetFieldID(responseClass, "body", "[B");
env->DeleteLocalRef(responseClass);
if (errorField == nullptr || messageField == nullptr || statusField == nullptr ||
headerNamesField == nullptr || headerValuesField == nullptr || bodyField == nullptr ||
clear_pending_exception(env))
{
return {
.error = Error::Network,
.message = "Android HTTP response shape was not recognized",
};
}
const int javaError = env->GetIntField(response, errorField);
auto* message = static_cast<jstring>(env->GetObjectField(response, messageField));
auto* headerNames = static_cast<jobjectArray>(env->GetObjectField(response, headerNamesField));
auto* headerValues =
static_cast<jobjectArray>(env->GetObjectField(response, headerValuesField));
auto* body = static_cast<jbyteArray>(env->GetObjectField(response, bodyField));
if (clear_pending_exception(env)) {
return {
.error = Error::Network,
.message = "Failed to read Android HTTP response",
};
}
Response httpResponse{
.statusCode = static_cast<int>(env->GetIntField(response, statusField)),
.headers = read_headers(env, headerNames, headerValues),
.body = read_body(env, body),
};
std::string messageString = to_string(env, message);
if (message != nullptr) {
env->DeleteLocalRef(message);
}
if (headerNames != nullptr) {
env->DeleteLocalRef(headerNames);
}
if (headerValues != nullptr) {
env->DeleteLocalRef(headerValues);
}
if (body != nullptr) {
env->DeleteLocalRef(body);
}
return {
.error = map_java_error(javaError),
.message = std::move(messageString),
.response = std::move(httpResponse),
};
}
} // namespace
bool available() noexcept {
return true;
}
Backend backend() noexcept {
return Backend::Android;
}
const char* backend_name() noexcept {
return "Android";
}
Result get(const Request& request) {
if (request.url.empty()) {
return {
.error = Error::InvalidUrl,
.message = "URL is empty",
};
}
if (!request.url.starts_with("https://")) {
return {
.error = Error::UnsupportedScheme,
.message = "Only https:// URLs are supported",
};
}
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
if (env == nullptr) {
return {
.error = Error::Network,
.message = "Failed to access Android JNI environment",
};
}
jobject activity = static_cast<jobject>(SDL_GetAndroidActivity());
if (activity == nullptr || clear_pending_exception(env)) {
if (activity != nullptr) {
env->DeleteLocalRef(activity);
}
return {
.error = Error::Network,
.message = "Failed to access Android activity",
};
}
jclass clientClass =
load_dusk_class(env, activity, "dev.twilitrealm.dusk.DuskHttpClient");
env->DeleteLocalRef(activity);
if (clientClass == nullptr) {
return {
.error = Error::Network,
.message = "Failed to load Android HTTP helper",
};
}
jmethodID getMethod = env->GetStaticMethodID(clientClass, "get",
"(Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/String;IJ)"
"Ldev/twilitrealm/dusk/DuskHttpClient$Response;");
if (getMethod == nullptr || clear_pending_exception(env)) {
env->DeleteLocalRef(clientClass);
return {
.error = Error::Network,
.message = "Failed to find Android HTTP helper method",
};
}
jstring url = to_jstring(env, request.url);
jobjectArray headerNames = make_string_array(env, request.headers, true);
jobjectArray headerValues = make_string_array(env, request.headers, false);
if (url == nullptr || headerNames == nullptr || headerValues == nullptr ||
clear_pending_exception(env))
{
if (url != nullptr) {
env->DeleteLocalRef(url);
}
if (headerNames != nullptr) {
env->DeleteLocalRef(headerNames);
}
if (headerValues != nullptr) {
env->DeleteLocalRef(headerValues);
}
env->DeleteLocalRef(clientClass);
return {
.error = Error::Network,
.message = "Failed to prepare Android HTTP request",
};
}
jobject response = env->CallStaticObjectMethod(clientClass, getMethod, url, headerNames,
headerValues, timeout_ms(request.timeout), max_body_bytes(request.maxBodyBytes));
env->DeleteLocalRef(url);
env->DeleteLocalRef(headerNames);
env->DeleteLocalRef(headerValues);
env->DeleteLocalRef(clientClass);
if (clear_pending_exception(env)) {
return {
.error = Error::Network,
.message = "Android HTTP request failed with a Java exception",
};
}
Result result = result_from_response(env, response);
if (response != nullptr) {
env->DeleteLocalRef(response);
}
return result;
}
} // namespace dusk::http
+206
View File
@@ -0,0 +1,206 @@
#include "http.hpp"
#include <curl/curl.h>
#include <algorithm>
#include <mutex>
#include <string_view>
#include <utility>
namespace dusk::http {
namespace {
struct CurlHeaders {
curl_slist* list = nullptr;
~CurlHeaders() {
if (list != nullptr) {
curl_slist_free_all(list);
}
}
bool append(const std::string& header) {
curl_slist* next = curl_slist_append(list, header.c_str());
if (next == nullptr) {
return false;
}
list = next;
return true;
}
};
struct CurlContext {
Response response;
size_t maxBodyBytes = 0;
bool tooLarge = false;
};
void initialize_curl() {
curl_global_init(CURL_GLOBAL_DEFAULT);
}
std::string trim_header_value(std::string_view value) {
while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) {
value.remove_prefix(1);
}
while (!value.empty() &&
(value.back() == '\r' || value.back() == '\n' || value.back() == ' ' ||
value.back() == '\t')) {
value.remove_suffix(1);
}
return std::string(value);
}
size_t write_body(char* ptr, size_t size, size_t nmemb, void* userdata) {
auto* context = static_cast<CurlContext*>(userdata);
const size_t bytes = size * nmemb;
if (bytes > context->maxBodyBytes ||
context->response.body.size() > context->maxBodyBytes - bytes) {
context->tooLarge = true;
return 0;
}
context->response.body.append(ptr, bytes);
return bytes;
}
size_t write_header(char* ptr, size_t size, size_t nmemb, void* userdata) {
auto* context = static_cast<CurlContext*>(userdata);
const std::string_view line(ptr, size * nmemb);
if (line.starts_with("HTTP/")) {
context->response.headers.clear();
return size * nmemb;
}
const size_t colon = line.find(':');
if (colon == std::string_view::npos) {
return size * nmemb;
}
context->response.headers.push_back({
.name = std::string(line.substr(0, colon)),
.value = trim_header_value(line.substr(colon + 1)),
});
return size * nmemb;
}
Error map_curl_error(CURLcode code, bool tooLarge) {
if (tooLarge) {
return Error::TooLarge;
}
switch (code) {
case CURLE_OK:
return Error::None;
case CURLE_URL_MALFORMAT:
return Error::InvalidUrl;
case CURLE_UNSUPPORTED_PROTOCOL:
return Error::UnsupportedScheme;
case CURLE_OPERATION_TIMEDOUT:
return Error::Timeout;
default:
return Error::Network;
}
}
long timeout_ms(std::chrono::milliseconds timeout) {
return std::max<std::chrono::milliseconds::rep>(1, timeout.count());
}
} // namespace
bool available() noexcept {
return true;
}
Backend backend() noexcept {
return Backend::LibCurl;
}
const char* backend_name() noexcept {
return "libcurl";
}
Result get(const Request& request) {
if (request.url.empty()) {
return {
.error = Error::InvalidUrl,
.message = "URL is empty",
};
}
if (!request.url.starts_with("https://")) {
return {
.error = Error::UnsupportedScheme,
.message = "Only https:// URLs are supported",
};
}
static std::once_flag initFlag;
std::call_once(initFlag, initialize_curl);
CURL* curl = curl_easy_init();
if (curl == nullptr) {
return {
.error = Error::Network,
.message = "Failed to create libcurl request",
};
}
CurlHeaders headers;
for (const Header& header : request.headers) {
if (!headers.append(header.name + ": " + header.value)) {
curl_easy_cleanup(curl);
return {
.error = Error::Network,
.message = "Failed to allocate libcurl headers",
};
}
}
CurlContext context{
.maxBodyBytes = request.maxBodyBytes,
};
curl_easy_setopt(curl, CURLOPT_URL, request.url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers.list);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
curl_easy_setopt(curl, CURLOPT_MAXREDIRS, 5L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT_MS, timeout_ms(request.timeout));
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT_MS, timeout_ms(request.timeout));
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_body);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &context);
curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, write_header);
curl_easy_setopt(curl, CURLOPT_HEADERDATA, &context);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1L);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2L);
#if CURL_AT_LEAST_VERSION(7, 85, 0)
curl_easy_setopt(curl, CURLOPT_PROTOCOLS_STR, "https");
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS_STR, "https");
#else
curl_easy_setopt(curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTPS);
curl_easy_setopt(curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTPS);
#endif
const CURLcode code = curl_easy_perform(curl);
long statusCode = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &statusCode);
curl_easy_cleanup(curl);
context.response.statusCode = static_cast<int>(statusCode);
if (code == CURLE_OK) {
return {
.response = std::move(context.response),
};
}
const Error error = map_curl_error(code, context.tooLarge);
return {
.error = error,
.message = error == Error::TooLarge ? "Response body exceeded the configured limit"
: curl_easy_strerror(code),
.response = std::move(context.response),
};
}
} // namespace dusk::http
+60
View File
@@ -0,0 +1,60 @@
#ifndef DUSK_HTTP_HTTP_HPP
#define DUSK_HTTP_HTTP_HPP
#include <chrono>
#include <cstddef>
#include <string>
#include <vector>
namespace dusk::http {
enum class Backend {
None,
WinHttp,
UrlSession,
LibCurl,
Android,
};
enum class Error {
None,
NoBackend,
InvalidUrl,
UnsupportedScheme,
Timeout,
TooLarge,
Network,
};
struct Header {
std::string name;
std::string value;
};
struct Request {
std::string url;
std::vector<Header> headers;
std::chrono::milliseconds timeout{10000};
size_t maxBodyBytes = 1024 * 1024;
};
struct Response {
int statusCode = 0;
std::vector<Header> headers;
std::string body;
};
struct Result {
Error error = Error::None;
std::string message;
Response response;
};
bool available() noexcept;
Backend backend() noexcept;
const char* backend_name() noexcept;
Result get(const Request& request);
} // namespace dusk::http
#endif // DUSK_HTTP_HTTP_HPP
+24
View File
@@ -0,0 +1,24 @@
#include "http.hpp"
namespace dusk::http {
bool available() noexcept {
return false;
}
Backend backend() noexcept {
return Backend::None;
}
const char* backend_name() noexcept {
return "none";
}
Result get(const Request&) {
return {
.error = Error::NoBackend,
.message = "No HTTP backend is available",
};
}
} // namespace dusk::http
+238
View File
@@ -0,0 +1,238 @@
#include "http.hpp"
#import <Foundation/Foundation.h>
#include <algorithm>
#include <string_view>
#include <utility>
@interface DuskHttpRequestDelegate : NSObject <NSURLSessionDataDelegate, NSURLSessionTaskDelegate>
@property(nonatomic) dispatch_semaphore_t semaphore;
@property(nonatomic) size_t maxBodyBytes;
@property(nonatomic, strong) NSMutableData* data;
@property(nonatomic, strong) NSURLResponse* response;
@property(nonatomic, strong) NSError* error;
@property(nonatomic) BOOL tooLarge;
- (instancetype)initWithMaxBodyBytes:(size_t)maxBodyBytes;
@end
@implementation DuskHttpRequestDelegate
- (instancetype)initWithMaxBodyBytes:(size_t)maxBodyBytes {
self = [super init];
if (self != nil) {
_semaphore = dispatch_semaphore_create(0);
_maxBodyBytes = maxBodyBytes;
_data = [NSMutableData data];
}
return self;
}
- (void)URLSession:(NSURLSession*)session
task:(NSURLSessionTask*)task
willPerformHTTPRedirection:(NSHTTPURLResponse*)response
newRequest:(NSURLRequest*)request
completionHandler:(void (^)(NSURLRequest*))completionHandler {
if ([[request.URL.scheme lowercaseString] isEqualToString:@"https"]) {
completionHandler(request);
} else {
completionHandler(nil);
}
}
- (void)URLSession:(NSURLSession*)session
dataTask:(NSURLSessionDataTask*)dataTask
didReceiveResponse:(NSURLResponse*)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
self.response = response;
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession*)session
dataTask:(NSURLSessionDataTask*)dataTask
didReceiveData:(NSData*)data {
if (data.length > self.maxBodyBytes ||
self.data.length > self.maxBodyBytes - data.length) {
self.tooLarge = YES;
[dataTask cancel];
return;
}
[self.data appendData:data];
}
- (void)URLSession:(NSURLSession*)session
task:(NSURLSessionTask*)task
didCompleteWithError:(NSError*)error {
if (error != nil && !self.tooLarge) {
self.error = error;
}
dispatch_semaphore_signal(self.semaphore);
}
@end
namespace dusk::http {
namespace {
NSString* to_nsstring(std::string_view value) {
return [[NSString alloc] initWithBytes:value.data()
length:value.size()
encoding:NSUTF8StringEncoding];
}
std::string to_string(NSString* value) {
if (value == nil) {
return {};
}
const char* utf8 = [value UTF8String];
return utf8 == nullptr ? std::string() : std::string(utf8);
}
Error map_nsurl_error(NSError* error) {
if (error == nil || ![error.domain isEqualToString:NSURLErrorDomain]) {
return Error::Network;
}
switch (error.code) {
case NSURLErrorTimedOut:
return Error::Timeout;
case NSURLErrorBadURL:
case NSURLErrorUnsupportedURL:
return Error::InvalidUrl;
default:
return Error::Network;
}
}
dispatch_time_t timeout_deadline(std::chrono::milliseconds timeout) {
const auto milliseconds = std::max<std::chrono::milliseconds::rep>(1, timeout.count());
return dispatch_time(DISPATCH_TIME_NOW,
static_cast<int64_t>(milliseconds) * static_cast<int64_t>(NSEC_PER_MSEC));
}
} // namespace
bool available() noexcept {
return true;
}
Backend backend() noexcept {
return Backend::UrlSession;
}
const char* backend_name() noexcept {
return "NSURLSession";
}
Result get(const Request& request) {
@autoreleasepool {
if (request.url.empty()) {
return {
.error = Error::InvalidUrl,
.message = "URL is empty",
};
}
if (!request.url.starts_with("https://")) {
return {
.error = Error::UnsupportedScheme,
.message = "Only https:// URLs are supported",
};
}
NSString* urlString = to_nsstring(request.url);
if (urlString == nil) {
return {
.error = Error::InvalidUrl,
.message = "URL is not valid UTF-8",
};
}
NSURL* url = [NSURL URLWithString:urlString];
if (url == nil || ![[url.scheme lowercaseString] isEqualToString:@"https"]) {
return {
.error = Error::InvalidUrl,
.message = "Failed to parse URL",
};
}
NSMutableURLRequest* urlRequest = [NSMutableURLRequest requestWithURL:url];
urlRequest.HTTPMethod = @"GET";
urlRequest.timeoutInterval = request.timeout.count() / 1000.0;
urlRequest.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
for (const Header& header : request.headers) {
NSString* name = to_nsstring(header.name);
NSString* value = to_nsstring(header.value);
if (name == nil || value == nil) {
return {
.error = Error::InvalidUrl,
.message = "Request header is not valid UTF-8",
};
}
[urlRequest setValue:value forHTTPHeaderField:name];
}
NSURLSessionConfiguration* configuration =
[NSURLSessionConfiguration ephemeralSessionConfiguration];
configuration.timeoutIntervalForRequest = request.timeout.count() / 1000.0;
configuration.timeoutIntervalForResource = request.timeout.count() / 1000.0;
DuskHttpRequestDelegate* delegate =
[[DuskHttpRequestDelegate alloc] initWithMaxBodyBytes:request.maxBodyBytes];
NSURLSession* session = [NSURLSession sessionWithConfiguration:configuration
delegate:delegate
delegateQueue:nil];
NSURLSessionDataTask* task = [session dataTaskWithRequest:urlRequest];
[task resume];
if (dispatch_semaphore_wait(delegate.semaphore, timeout_deadline(request.timeout)) != 0) {
[task cancel];
[session invalidateAndCancel];
return {
.error = Error::Timeout,
.message = "Request timed out",
};
}
[session finishTasksAndInvalidate];
Response response;
if ([delegate.response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)delegate.response;
response.statusCode = static_cast<int>(httpResponse.statusCode);
NSDictionary* headers = httpResponse.allHeaderFields;
for (id key in headers) {
id value = headers[key];
response.headers.push_back({
.name = to_string([key description]),
.value = to_string([value description]),
});
}
}
if (delegate.data != nil && delegate.data.length > 0) {
response.body.assign(static_cast<const char*>(delegate.data.bytes),
static_cast<size_t>(delegate.data.length));
}
if (delegate.tooLarge) {
return {
.error = Error::TooLarge,
.message = "Response body exceeded the configured limit",
.response = std::move(response),
};
}
if (delegate.error != nil) {
return {
.error = map_nsurl_error(delegate.error),
.message = to_string(delegate.error.localizedDescription),
.response = std::move(response),
};
}
return {
.response = std::move(response),
};
}
}
} // namespace dusk::http
+320
View File
@@ -0,0 +1,320 @@
#include "http.hpp"
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <winhttp.h>
#include <algorithm>
#include <limits>
#include <string_view>
#include <utility>
#include <vector>
namespace dusk::http {
namespace {
struct WinHttpHandle {
HINTERNET handle = nullptr;
WinHttpHandle() = default;
explicit WinHttpHandle(HINTERNET handle) : handle(handle) {}
WinHttpHandle(const WinHttpHandle&) = delete;
WinHttpHandle& operator=(const WinHttpHandle&) = delete;
~WinHttpHandle() {
if (handle != nullptr) {
WinHttpCloseHandle(handle);
}
}
operator HINTERNET() const { return handle; }
};
std::wstring utf8_to_wide(std::string_view value) {
if (value.empty()) {
return {};
}
const int required = MultiByteToWideChar(
CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast<int>(value.size()), nullptr, 0);
if (required <= 0) {
return {};
}
std::wstring result(static_cast<size_t>(required), L'\0');
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.data(), static_cast<int>(value.size()),
result.data(), required);
return result;
}
std::string wide_to_utf8(std::wstring_view value) {
if (value.empty()) {
return {};
}
const int required = WideCharToMultiByte(
CP_UTF8, 0, value.data(), static_cast<int>(value.size()), nullptr, 0, nullptr, nullptr);
if (required <= 0) {
return {};
}
std::string result(static_cast<size_t>(required), '\0');
WideCharToMultiByte(CP_UTF8, 0, value.data(), static_cast<int>(value.size()), result.data(),
required, nullptr, nullptr);
return result;
}
DWORD timeout_ms(std::chrono::milliseconds timeout) {
const auto count = std::max<std::chrono::milliseconds::rep>(1, timeout.count());
return static_cast<DWORD>(
std::min<std::chrono::milliseconds::rep>(count, std::numeric_limits<int>::max()));
}
Error map_winhttp_error(DWORD error) {
switch (error) {
case ERROR_WINHTTP_TIMEOUT:
return Error::Timeout;
case ERROR_WINHTTP_INVALID_URL:
case ERROR_WINHTTP_UNRECOGNIZED_SCHEME:
return Error::InvalidUrl;
case ERROR_WINHTTP_SECURE_FAILURE:
case ERROR_WINHTTP_CANNOT_CONNECT:
case ERROR_WINHTTP_CONNECTION_ERROR:
default:
return Error::Network;
}
}
Result fail_from_last_error(const char* message) {
const DWORD error = GetLastError();
return {
.error = map_winhttp_error(error),
.message = std::string(message) + " (" + std::to_string(error) + ")",
};
}
std::string trim_header_value(std::string_view value) {
while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) {
value.remove_prefix(1);
}
while (!value.empty() && (value.back() == '\r' || value.back() == '\n' || value.back() == ' ' ||
value.back() == '\t'))
{
value.remove_suffix(1);
}
return std::string(value);
}
void parse_headers(std::wstring_view rawHeaders, Response& response) {
size_t start = 0;
bool firstLine = true;
while (start < rawHeaders.size()) {
size_t end = rawHeaders.find(L"\r\n", start);
if (end == std::wstring_view::npos) {
end = rawHeaders.size();
}
const std::wstring_view line = rawHeaders.substr(start, end - start);
if (!line.empty() && !firstLine) {
const size_t colon = line.find(L':');
if (colon != std::wstring_view::npos) {
response.headers.push_back({
.name = wide_to_utf8(line.substr(0, colon)),
.value = trim_header_value(wide_to_utf8(line.substr(colon + 1))),
});
}
}
firstLine = false;
if (end == rawHeaders.size()) {
break;
}
start = end + 2;
}
}
bool read_status(HINTERNET request, Response& response) {
DWORD statusCode = 0;
DWORD statusCodeSize = sizeof(statusCode);
if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &statusCode, &statusCodeSize, WINHTTP_NO_HEADER_INDEX))
{
return false;
}
response.statusCode = static_cast<int>(statusCode);
return true;
}
bool read_headers(HINTERNET request, Response& response) {
DWORD headerBytes = 0;
WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX,
WINHTTP_NO_OUTPUT_BUFFER, &headerBytes, WINHTTP_NO_HEADER_INDEX);
if (GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
return false;
}
std::wstring rawHeaders(headerBytes / sizeof(wchar_t), L'\0');
if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_RAW_HEADERS_CRLF, WINHTTP_HEADER_NAME_BY_INDEX,
rawHeaders.data(), &headerBytes, WINHTTP_NO_HEADER_INDEX))
{
return false;
}
if (!rawHeaders.empty() && rawHeaders.back() == L'\0') {
rawHeaders.pop_back();
}
parse_headers(rawHeaders, response);
return true;
}
} // namespace
bool available() noexcept {
return true;
}
Backend backend() noexcept {
return Backend::WinHttp;
}
const char* backend_name() noexcept {
return "WinHTTP";
}
Result get(const Request& request) {
if (request.url.empty()) {
return {
.error = Error::InvalidUrl,
.message = "URL is empty",
};
}
std::wstring wideUrl = utf8_to_wide(request.url);
if (wideUrl.empty()) {
return {
.error = Error::InvalidUrl,
.message = "URL is not valid UTF-8",
};
}
URL_COMPONENTS components{};
components.dwStructSize = sizeof(components);
components.dwSchemeLength = static_cast<DWORD>(-1);
components.dwHostNameLength = static_cast<DWORD>(-1);
components.dwUrlPathLength = static_cast<DWORD>(-1);
components.dwExtraInfoLength = static_cast<DWORD>(-1);
if (!WinHttpCrackUrl(wideUrl.c_str(), static_cast<DWORD>(wideUrl.size()), 0, &components)) {
return fail_from_last_error("Failed to parse URL");
}
if (components.nScheme != INTERNET_SCHEME_HTTPS) {
return {
.error = Error::UnsupportedScheme,
.message = "Only https:// URLs are supported",
};
}
const std::wstring host(components.lpszHostName, components.dwHostNameLength);
std::wstring path;
if (components.lpszUrlPath != nullptr && components.dwUrlPathLength > 0) {
path.assign(components.lpszUrlPath, components.dwUrlPathLength);
}
if (components.lpszExtraInfo != nullptr && components.dwExtraInfoLength > 0) {
path.append(components.lpszExtraInfo, components.dwExtraInfoLength);
}
if (path.empty()) {
path = L"/";
}
WinHttpHandle session(WinHttpOpen(L"Dusk", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0));
if (session.handle == nullptr) {
return fail_from_last_error("Failed to create WinHTTP session");
}
const DWORD timeout = timeout_ms(request.timeout);
WinHttpSetTimeouts(session, timeout, timeout, timeout, timeout);
WinHttpHandle connection(WinHttpConnect(session, host.c_str(), components.nPort, 0));
if (connection.handle == nullptr) {
return fail_from_last_error("Failed to connect");
}
WinHttpHandle httpRequest(WinHttpOpenRequest(connection, L"GET", path.c_str(), nullptr,
WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE));
if (httpRequest.handle == nullptr) {
return fail_from_last_error("Failed to create request");
}
DWORD redirectPolicy = WINHTTP_OPTION_REDIRECT_POLICY_DISALLOW_HTTPS_TO_HTTP;
WinHttpSetOption(
httpRequest, WINHTTP_OPTION_REDIRECT_POLICY, &redirectPolicy, sizeof(redirectPolicy));
DWORD maxRedirects = 5;
WinHttpSetOption(httpRequest, WINHTTP_OPTION_MAX_HTTP_AUTOMATIC_REDIRECTS, &maxRedirects,
sizeof(maxRedirects));
for (const Header& header : request.headers) {
const std::wstring wideHeader = utf8_to_wide(header.name + ": " + header.value);
if (wideHeader.empty()) {
return {
.error = Error::InvalidUrl,
.message = "Request header is not valid UTF-8",
};
}
if (!WinHttpAddRequestHeaders(httpRequest, wideHeader.c_str(),
static_cast<DWORD>(wideHeader.size()), WINHTTP_ADDREQ_FLAG_ADD))
{
return fail_from_last_error("Failed to add request header");
}
}
if (!WinHttpSendRequest(
httpRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0))
{
return fail_from_last_error("Failed to send request");
}
if (!WinHttpReceiveResponse(httpRequest, nullptr)) {
return fail_from_last_error("Failed to receive response");
}
Response response;
if (!read_status(httpRequest, response)) {
return fail_from_last_error("Failed to read response status");
}
read_headers(httpRequest, response);
for (;;) {
DWORD availableBytes = 0;
if (!WinHttpQueryDataAvailable(httpRequest, &availableBytes)) {
return fail_from_last_error("Failed to query response body");
}
if (availableBytes == 0) {
break;
}
if (availableBytes > request.maxBodyBytes ||
response.body.size() > request.maxBodyBytes - availableBytes)
{
return {
.error = Error::TooLarge,
.message = "Response body exceeded the configured limit",
.response = std::move(response),
};
}
std::vector<char> buffer(availableBytes);
DWORD bytesRead = 0;
if (!WinHttpReadData(httpRequest, buffer.data(), availableBytes, &bytesRead)) {
return fail_from_last_error("Failed to read response body");
}
response.body.append(buffer.data(), bytesRead);
}
return {
.response = std::move(response),
};
}
} // namespace dusk::http
+6 -2
View File
@@ -282,7 +282,9 @@ static void ShowAllJAISeqs() {
} }
void dusk::ImGuiMenuTools::ShowAudioDebug() { void dusk::ImGuiMenuTools::ShowAudioDebug() {
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F10, m_showAudioDebug)) { if (!getSettings().backend.enableAdvancedSettings ||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F10, m_showAudioDebug))
{
return; return;
} }
@@ -328,7 +330,9 @@ void dusk::ImGuiMenuTools::ShowAudioDebug() {
} }
void dusk::ImGuiMenuTools::ShowSaveEditor() { void dusk::ImGuiMenuTools::ShowSaveEditor() {
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F6, m_showSaveEditor)) { if (!getSettings().backend.enableAdvancedSettings ||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F6, m_showSaveEditor))
{
return; return;
} }
m_saveEditor.draw(m_showSaveEditor); m_saveEditor.draw(m_showSaveEditor);
+22 -4
View File
@@ -10,7 +10,9 @@
namespace dusk { namespace dusk {
void ImGuiMenuTools::ShowCameraOverlay() { void ImGuiMenuTools::ShowCameraOverlay() {
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F9, m_showCameraOverlay)) { if (!getSettings().backend.enableAdvancedSettings ||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F9, m_showCameraOverlay))
{
return; return;
} }
@@ -61,16 +63,32 @@ namespace dusk {
ImGui::SetTooltip("Cannot enable while paused or during an active event."); ImGui::SetTooltip("Cannot enable while paused or during an active event.");
} else { } else {
ImGui::SetTooltip("Detach camera and fly freely.\n" ImGui::SetTooltip("Detach camera and fly freely.\n"
"Left stick: move, C-stick: look\n" "WASD/Arrows/Left stick: move, Mouse/C-stick: look\n"
"L/R triggers: up/down, Z: fast"); "Ctrl/L: down, Space/R: up, Shift/Z: fast\n"
"Q Key/Y: roll left, R Key/X: roll right");
} }
} }
if (eventRunning) { if (eventRunning) {
ImGui::EndDisabled(); ImGui::EndDisabled();
} }
if (!getSettings().game.debugFlyCam) {
ImGui::BeginDisabled();
}
config::ImGuiCheckbox("Lock Events", getSettings().game.debugFlyCamLockEvents);
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
if (!getSettings().game.debugFlyCam) {
ImGui::SetTooltip("Enable Fly Mode first.");
} else {
ImGui::SetTooltip("Freeze game events while flying.");
}
}
if (!getSettings().game.debugFlyCam) {
ImGui::EndDisabled();
}
ShowCornerContextMenu(m_cameraOverlayCorner, 0); ShowCornerContextMenu(m_cameraOverlayCorner, 0);
ImGui::End(); ImGui::End();
} }
} }
+22 -33
View File
@@ -253,20 +253,20 @@ namespace dusk {
UpdateSettings(); UpdateSettings();
if (!fpcM_SearchByName(fpcNm_LOGO_SCENE_e) &&
(ImGui::IsKeyDown(ImGuiKey_LeftCtrl) || ImGui::IsKeyDown(ImGuiKey_RightCtrl)) &&
ImGui::IsKeyPressed(ImGuiKey_R))
{
JUTGamePad::C3ButtonReset::sResetSwitchPushing = true;
}
if (ImGui::IsKeyPressed(ImGuiKey_F11)) { if (ImGui::IsKeyPressed(ImGuiKey_F11)) {
ImGuiMenuGame::ToggleFullscreen(); getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
VISetWindowFullscreen(getSettings().video.enableFullscreen);
config::Save();
} }
if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_F1)) { if (ImGui::GetIO().KeyShift && ImGui::IsKeyPressed(ImGuiKey_F1)) {
m_isHidden = !m_isHidden; if (getSettings().backend.enableAdvancedSettings) {
m_isHidden = !m_isHidden;
} else {
m_isHidden = true;
}
} }
bool showMenu = !m_isHidden; bool showMenu = !m_isHidden;
// The menu bar renders with ImGuiCol_WindowBg behind it. We just want ImGuiCol_MenuBarBg, // The menu bar renders with ImGuiCol_WindowBg behind it. We just want ImGuiCol_MenuBarBg,
@@ -276,16 +276,6 @@ namespace dusk {
m_menuGame.draw(); m_menuGame.draw();
m_menuTools.draw(); m_menuTools.draw();
const auto fpsLabel =
fmt::format(FMT_STRING("FPS: {:.2f}\n"), ImGui::GetIO().Framerate);
const auto fpsSize =
ImGui::CalcTextSize(fpsLabel.data(), fpsLabel.data() + fpsLabel.size());
ImGui::SetCursorPosX(
ImMax(ImGui::GetCursorPosX(), ImGui::GetWindowWidth() -
ImGui::GetStyle().DisplaySafeAreaPadding.x -
fpsSize.x - ImGui::GetStyle().ItemSpacing.x));
ImGuiStringViewText(fpsLabel);
ImGui::EndMainMenuBar(); ImGui::EndMainMenuBar();
} }
ImGui::PopStyleColor(); ImGui::PopStyleColor();
@@ -360,8 +350,7 @@ namespace dusk {
ImGui::End(); ImGui::End();
} }
m_menuGame.windowControllerConfig(); m_menuTools.ShowInputViewer();
m_menuGame.windowInputViewer();
m_menuGame.drawSpeedrunTimerOverlay(); m_menuGame.drawSpeedrunTimerOverlay();
if (getSettings().game.liveSplitEnabled) { if (getSettings().game.liveSplitEnabled) {
@@ -387,18 +376,18 @@ namespace dusk {
} }
// Hide mouse cursor if the F1 menu is not open and the cursor is idle for 3 seconds. // Hide mouse cursor if the F1 menu is not open and the cursor is idle for 3 seconds.
ImGuiIO& io = ImGui::GetIO(); if (dusk::getSettings().game.gyroMode.getValue() != GyroMode::Mouse)
if (showMenu) { {
mouseHideTimer = 0.0f; ImGuiIO& io = ImGui::GetIO();
ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; // Imgui will re-show cursor. if (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f) {
} else if (io.MouseDelta.x != 0.0f || io.MouseDelta.y != 0.0f) { mouseHideTimer = 0.0f;
mouseHideTimer = 0.0f; ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; // Imgui will re-show cursor.
ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange; // Imgui will re-show cursor. } else if (mouseHideTimer <= 3.0f) {
} else if (mouseHideTimer <= 3.0f) { mouseHideTimer += ImGui::GetIO().DeltaTime;
mouseHideTimer += ImGui::GetIO().DeltaTime; } else {
} else { ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; SDL_HideCursor();
SDL_HideCursor(); }
} }
ShowToasts(); ShowToasts();
-22
View File
@@ -2,11 +2,9 @@
#define DUSK_IMGUI_HPP #define DUSK_IMGUI_HPP
#include <deque> #include <deque>
#include <filesystem>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <SDL3/SDL_misc.h>
#include <aurora/aurora.h> #include <aurora/aurora.h>
#include "ImGuiMenuGame.hpp" #include "ImGuiMenuGame.hpp"
@@ -73,24 +71,4 @@ bool ImGuiButtonCenter(std::string_view text);
float ImGuiScale(); float ImGuiScale();
} // namespace dusk } // namespace dusk
#if defined(_WIN32) || \
(defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_TV && !TARGET_OS_MACCATALYST) || \
(defined(__linux__) && !defined(__ANDROID__))
#define DUSK_CAN_OPEN_DATA_FOLDER 1
namespace fs = std::filesystem;
static void OpenDataFolder() {
const std::string path = fs::absolute(dusk::ConfigPath).generic_string();
#if defined(_WIN32)
const std::string url = std::string("file:///") + path;
#else
const std::string url = std::string("file://") + path;
#endif
(void)SDL_OpenURL(url.c_str());
}
#else
#define DUSK_CAN_OPEN_DATA_FOLDER 0
#endif
#endif // DUSK_IMGUI_HPP #endif // DUSK_IMGUI_HPP
+1 -2
View File
@@ -3,12 +3,11 @@
#include "imgui.h" #include "imgui.h"
#include <imgui_internal.h> #include <imgui_internal.h>
#include "ImGuiConsole.hpp" #include "ImGuiConsole.hpp"
#include "ImGuiMenuGame.hpp"
#include <dolphin/pad.h> #include <dolphin/pad.h>
namespace dusk { namespace dusk {
void ImGuiMenuGame::windowInputViewer() { void ImGuiMenuTools::ShowInputViewer() {
if (!m_showInputViewer) { if (!m_showInputViewer) {
return; return;
} }
+3 -1
View File
@@ -22,7 +22,9 @@ namespace dusk {
void ShowHeapDetailed(JKRHeap* heap, OpenHeapData& data, bool& open); void ShowHeapDetailed(JKRHeap* heap, OpenHeapData& data, bool& open);
void ImGuiMenuTools::ShowHeapOverlay() { void ImGuiMenuTools::ShowHeapOverlay() {
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F4, m_showHeapOverlay)) { if (!getSettings().backend.enableAdvancedSettings ||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F4, m_showHeapOverlay))
{
return; return;
} }
+3 -1
View File
@@ -9,7 +9,9 @@
namespace dusk { namespace dusk {
void ImGuiMenuTools::ShowMapLoader() { void ImGuiMenuTools::ShowMapLoader() {
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F7, m_showMapLoader)) { if (!getSettings().backend.enableAdvancedSettings ||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F7, m_showMapLoader))
{
return; return;
} }
+4 -560
View File
@@ -1,573 +1,16 @@
#include "fmt/format.h" #include "fmt/format.h"
#include "imgui.h" #include "imgui.h"
#include "ImGuiEngine.hpp"
#include "ImGuiConsole.hpp" #include "ImGuiConsole.hpp"
#include "ImGuiConfig.hpp" #include "ImGuiConfig.hpp"
#include "dusk/main.h" #include "dusk/main.h"
#include "dusk/hotkeys.h"
#include "m_Do/m_Do_main.h" #include "m_Do/m_Do_main.h"
#include <SDL3/SDL_gamepad.h>
namespace dusk { namespace dusk {
void ImGuiMenuGame::ToggleFullscreen() {
getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
VISetWindowFullscreen(getSettings().video.enableFullscreen);
config::Save();
}
ImGuiMenuGame::ImGuiMenuGame() {} ImGuiMenuGame::ImGuiMenuGame() {}
void ImGuiMenuGame::draw() { void ImGuiMenuGame::draw() {}
if (ImGui::BeginMenu("Settings")) {
// TODO: Remove this once Controller Config exists in RmlUi
if (ImGui::Button("Configure Controller")){
m_showControllerConfig = !m_showControllerConfig;
}
ImGui::Checkbox("Show Input Viewer", &m_showInputViewer);
ImGui::EndMenu();
}
}
static void drawVirtualStick(const char* id, const ImVec2& stick) {
float scale = ImGuiScale();
ImGui::SetCursorPos(ImVec2(ImGui::GetCursorPos().x + 45 * scale, ImGui::GetCursorPos().y + 10));
ImGui::BeginChild(id, ImVec2(80 * scale, 80 * scale), 0, ImGuiWindowFlags_NoBackground);
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 p = ImGui::GetCursorScreenPos();
float radius = 30.0f * scale;
ImVec2 pos = ImVec2(p.x + radius, p.y + radius);
constexpr ImU32 stickGray = IM_COL32(150, 150, 150, 255);
constexpr ImU32 white = IM_COL32(255, 255, 255, 255);
constexpr ImU32 red = IM_COL32(230, 0, 0, 255);
dl->AddCircleFilled(pos, radius, stickGray, 8);
dl->AddCircleFilled(ImVec2(pos.x + stick.x * (radius), pos.y + -stick.y * (radius)), 3 * scale, red);
ImGui::EndChild();
}
struct SpecificButtonName {
SDL_GamepadType Type;
const char* Name;
};
struct ButtonNames {
SDL_GamepadButton Button;
std::vector<SpecificButtonName> Names;
};
// clang-format off
static const std::vector<ButtonNames> GamepadButtonNames = {
{ SDL_GAMEPAD_BUTTON_LEFT_STICK, {
{SDL_GAMEPAD_TYPE_PS3, "L3"},
{SDL_GAMEPAD_TYPE_PS4, "L3"},
{SDL_GAMEPAD_TYPE_PS5, "L3"},
{SDL_GAMEPAD_TYPE_XBOX360, "Left Stick"},
{SDL_GAMEPAD_TYPE_XBOXONE, "Left Stick"},
{SDL_GAMEPAD_TYPE_GAMECUBE, "Control Stick"},
}},
{ SDL_GAMEPAD_BUTTON_RIGHT_STICK, {
{SDL_GAMEPAD_TYPE_PS3, "R3"},
{SDL_GAMEPAD_TYPE_PS4, "R3"},
{SDL_GAMEPAD_TYPE_PS5, "R3"},
{SDL_GAMEPAD_TYPE_XBOX360, "Right Stick"},
{SDL_GAMEPAD_TYPE_XBOXONE, "Right Stick"},
{SDL_GAMEPAD_TYPE_GAMECUBE, "C Stick"},
}},
{ SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, {
{SDL_GAMEPAD_TYPE_PS3, "L1"},
{SDL_GAMEPAD_TYPE_PS4, "L1"},
{SDL_GAMEPAD_TYPE_PS5, "L1"},
{SDL_GAMEPAD_TYPE_XBOX360, "LB"},
{SDL_GAMEPAD_TYPE_XBOXONE, "LB"},
}},
{ SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, {
{SDL_GAMEPAD_TYPE_PS3, "R1"},
{SDL_GAMEPAD_TYPE_PS4, "R1"},
{SDL_GAMEPAD_TYPE_PS5, "R1"},
{SDL_GAMEPAD_TYPE_XBOX360, "RB"},
{SDL_GAMEPAD_TYPE_XBOXONE, "RB"},
{SDL_GAMEPAD_TYPE_GAMECUBE, "Z"},
}},
{ SDL_GAMEPAD_BUTTON_BACK, {
{SDL_GAMEPAD_TYPE_PS3, "Select"},
{SDL_GAMEPAD_TYPE_PS4, "Share"},
{SDL_GAMEPAD_TYPE_PS5, "Create"},
{SDL_GAMEPAD_TYPE_XBOX360, "Back"},
{SDL_GAMEPAD_TYPE_XBOXONE, "View"},
}},
{ SDL_GAMEPAD_BUTTON_START, {
{SDL_GAMEPAD_TYPE_PS3, "Start"},
{SDL_GAMEPAD_TYPE_PS4, "Options"},
{SDL_GAMEPAD_TYPE_PS5, "Options"},
{SDL_GAMEPAD_TYPE_XBOX360, "Start"},
{SDL_GAMEPAD_TYPE_XBOXONE, "Menu"},
{SDL_GAMEPAD_TYPE_GAMECUBE, "Start/Pause"},
}},
};
// clang-format on
static const char* GetNameForGamepadButton(SDL_Gamepad* gamepad, u32 buttonUntyped) {
if (buttonUntyped == PAD_NATIVE_BUTTON_INVALID) {
return "Not bound";
}
auto button = static_cast<SDL_GamepadButton>(buttonUntyped);
auto label = SDL_GetGamepadButtonLabel(gamepad, button);
switch (label) {
case SDL_GAMEPAD_BUTTON_LABEL_A:
return "A";
case SDL_GAMEPAD_BUTTON_LABEL_B:
return "B";
case SDL_GAMEPAD_BUTTON_LABEL_X:
return "X";
case SDL_GAMEPAD_BUTTON_LABEL_Y:
return "Y";
case SDL_GAMEPAD_BUTTON_LABEL_CROSS:
return "Cross";
case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE:
return "Circle";
case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE:
return "Triangle";
case SDL_GAMEPAD_BUTTON_LABEL_SQUARE:
return "Square";
default:; // Fall through
}
auto padType = SDL_GetGamepadType(gamepad);
for (const auto& buttonNames : GamepadButtonNames) {
if (buttonNames.Button != button) {
continue;
}
for (const auto& name : buttonNames.Names) {
if (name.Type == padType) {
return name.Name;
}
}
}
switch (button) {
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
return "D-pad left";
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
return "D-pad right";
case SDL_GAMEPAD_BUTTON_DPAD_UP:
return "D-pad up";
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
return "D-pad down";
default:
return PADGetNativeButtonName(buttonUntyped);
}
}
void ImGuiMenuGame::windowControllerConfig() {
if (!m_showControllerConfig) {
return;
}
// if pending for a button mapping, check to set new input
if (m_controllerConfig.m_pendingButtonMapping != nullptr) {
s32 nativeButton = PADGetNativeButtonPressed(m_controllerConfig.m_pendingPort);
if (nativeButton != -1) {
m_controllerConfig.m_pendingButtonMapping->nativeButton = nativeButton;
m_controllerConfig.m_pendingButtonMapping = nullptr;
m_controllerConfig.m_pendingPort = -1;
PADBlockInput(false);
PADSerializeMappings();
}
}
// if pending for an axis mapping, check to set new input
if (m_controllerConfig.m_pendingAxisMapping != nullptr) {
auto nativeAxis = PADGetNativeAxisPulled(m_controllerConfig.m_pendingPort);
if (nativeAxis.nativeAxis != -1) {
m_controllerConfig.m_pendingAxisMapping->nativeAxis = nativeAxis;
m_controllerConfig.m_pendingAxisMapping->nativeButton = -1;
m_controllerConfig.m_pendingAxisMapping = nullptr;
m_controllerConfig.m_pendingPort = -1;
PADBlockInput(false);
PADSerializeMappings();
} else {
auto nativeButton = PADGetNativeButtonPressed(m_controllerConfig.m_pendingPort);
if (nativeButton != -1) {
m_controllerConfig.m_pendingAxisMapping->nativeAxis = {-1, AXIS_SIGN_POSITIVE};
m_controllerConfig.m_pendingAxisMapping->nativeButton = nativeButton;
m_controllerConfig.m_pendingAxisMapping = nullptr;
m_controllerConfig.m_pendingPort = -1;
PADBlockInput(false);
PADSerializeMappings();
}
}
}
float scale = ImGuiScale();
ImGuiWindowFlags windowFlags =
ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_AlwaysAutoResize;
// ImGui::SetNextWindowBgAlpha(0.65f);
if (!ImGui::Begin("Controller Config", &m_showControllerConfig, windowFlags)) {
ImGui::End();
return;
}
// port tabs
ImGui::BeginTabBar("##ControllerTabs");
for (int i = PAD_1; i <= PAD_4; i++) {
if (ImGui::BeginTabItem(fmt::format("Port {}", i + 1).c_str())) {
m_controllerConfig.m_selectedPort = i;
ImGui::EndTabItem();
}
}
ImGui::EndTabBar();
// if tab is changed while waiting for input, cancel pending
if ((m_controllerConfig.m_pendingButtonMapping != nullptr ||
m_controllerConfig.m_pendingAxisMapping != nullptr) &&
m_controllerConfig.m_pendingPort != m_controllerConfig.m_selectedPort)
{
m_controllerConfig.m_pendingButtonMapping = nullptr;
m_controllerConfig.m_pendingAxisMapping = nullptr;
m_controllerConfig.m_pendingPort = -1;
PADBlockInput(false);
}
// get a list of all available controller's names
std::vector<std::string> controllerList;
controllerList.push_back("None");
for (int i = 0; i < PADCount(); i++) {
// attach index to name for unique name
controllerList.push_back(fmt::format("{}-{}", PADGetNameForControllerIndex(i), i));
}
// get current controller name
const char* tmpName = PADGetName(m_controllerConfig.m_selectedPort);
std::string currentName = "None";
if (tmpName != nullptr) {
currentName = fmt::format("{}-{}", tmpName, PADGetIndexForPort(m_controllerConfig.m_selectedPort));
}
// controller selection combo box
bool changedController = false;
int changedControllerIndex = 0;
ImGui::SetNextItemWidth(400.0f * scale);
if (ImGui::BeginCombo("##ControllerDeviceList", currentName.c_str())) {
for (int i = 0; const auto& name : controllerList) {
if (ImGui::Selectable(name.c_str(), currentName == name)) {
changedControllerIndex = i;
changedController = true;
}
i++;
}
ImGui::EndCombo();
}
// handle controller change
if (changedController) {
if (changedControllerIndex > 0) {
PADSetPortForIndex(changedControllerIndex - 1, m_controllerConfig.m_selectedPort);
}
else if (changedControllerIndex == 0) {
// if "None" selected
PADClearPort(m_controllerConfig.m_selectedPort);
}
PADSerializeMappings();
}
// restore defaults button
ImGui::SameLine();
if (ImGui::Button("Default")) {
PADRestoreDefaultMapping(m_controllerConfig.m_selectedPort);
PADSerializeMappings();
}
// buttons panel
const float uiButtonSize = 40 * scale;
ImVec2 btnSize(110.0f * scale, 30.0f * scale);
ImGuiBeginGroupPanel("Buttons", ImVec2(150 * scale, 20 * scale));
SDL_Gamepad* gamepad = PADGetSDLGamepadForIndex(PADGetIndexForPort(m_controllerConfig.m_selectedPort));
u32 buttonCount;
PADButtonMapping* btnMappingList = PADGetButtonMappings(m_controllerConfig.m_selectedPort, &buttonCount);
if (btnMappingList != nullptr) {
for (int i = 0; i < buttonCount; i++) {
const char* btnName = PADGetButtonName(btnMappingList[i].padButton);
ImVec2 len = ImGui::CalcTextSize(btnName);
ImVec2 pos = ImGui::GetCursorPos();
ImGui::SetCursorPosY(pos.y + len.y / 4);
ImGui::SetCursorPosX(pos.x + abs(len.x - uiButtonSize));
ImGui::Text("%s", btnName);
ImGui::SameLine();
ImGui::SetCursorPosY(pos.y);
std::string dispName;
if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingButtonMapping == &btnMappingList[i]) {
dispName = fmt::format("Press a Key...##{}", btnName);
} else {
const char* nativeName = GetNameForGamepadButton(gamepad, btnMappingList[i].nativeButton);
if (nativeName == nullptr) {
nativeName = "[unbound]";
}
dispName = fmt::format("{0}##-{1}", nativeName, i);
}
bool pressed = ImGui::Button(dispName.c_str(),
btnSize);
if (pressed) {
m_controllerConfig.m_isReading = true;
m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort;
m_controllerConfig.m_pendingButtonMapping = &btnMappingList[i];
PADBlockInput(true);
}
}
}
ImGuiEndGroupPanel();
ImGui::SameLine();
uint32_t axisCount;
PADAxisMapping* axisMappingList = PADGetAxisMappings(m_controllerConfig.m_selectedPort, &axisCount);
ImGuiBeginGroupPanel("Analog Triggers", ImVec2(150 * scale, 20 * scale));
PADAxis triggers[] = {PAD_AXIS_TRIGGER_L, PAD_AXIS_TRIGGER_R};
if (axisMappingList != nullptr) {
for (PADAxis trigger : triggers) {
const char* axisName = PADGetAxisName(axisMappingList[trigger].padAxis);
ImVec2 len = ImGui::CalcTextSize(axisName);
ImVec2 pos = ImGui::GetCursorPos();
ImGui::SetCursorPosY(pos.y + len.y / 4);
ImGui::SetCursorPosX(pos.x + abs(len.x - uiButtonSize));
ImGui::Text("%s", axisName);
ImGui::SameLine();
ImGui::SetCursorPosY(pos.y);
std::string dispName;
if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[trigger]) {
dispName = fmt::format("Press a Key...##{}", axisName);
} else {
dispName = fmt::format("{0}##-{1}", PADGetNativeAxisName(axisMappingList[trigger].nativeAxis), trigger);
}
bool pressed = ImGui::Button(dispName.c_str(),
btnSize);
if (pressed) {
m_controllerConfig.m_isReading = true;
m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort;
m_controllerConfig.m_pendingAxisMapping = &axisMappingList[trigger];
PADBlockInput(true);
}
}
}
int port = m_controllerConfig.m_selectedPort;
PADDeadZones* deadZones = PADGetDeadZones(port);
if (deadZones != nullptr) {
ImGui::Text("L Threshold");
ImGui::SameLine();
{
float tmp = static_cast<float>(deadZones->leftTriggerActivationZone * 100.f) / 32767.f;
if (ImGui::DragFloat("##LThreshold", &tmp, 0.5f, 0.f, 100.f, "%.3f%%")) {
deadZones->leftTriggerActivationZone = static_cast<u16>((tmp / 100.f) * 32767);
PADSerializeMappings();
}
}
}
if (deadZones != nullptr) {
ImGui::Text("R Threshold");
ImGui::SameLine();
{
float tmp = static_cast<float>(deadZones->rightTriggerActivationZone * 100.f) / 32767.f;
if (ImGui::DragFloat("##RThreshold", &tmp, 0.5f, 0.f, 100.f, "%.3f%%")) {
deadZones->rightTriggerActivationZone = static_cast<u16>((tmp / 100.f) * 32767);
PADSerializeMappings();
}
}
}
ImGuiEndGroupPanel();
ImGui::SameLine();
// main stick panel
ImGuiBeginGroupPanel("Control Stick", ImVec2(150 * scale, 20 * scale));
drawVirtualStick("##mainStick", ImVec2{ mDoCPd_c::getStickX(port), mDoCPd_c::getStickY(port) });
if (axisMappingList != nullptr) {
const PADAxis lStickAxes[] = {PAD_AXIS_LEFT_Y_POS, PAD_AXIS_LEFT_Y_NEG, PAD_AXIS_LEFT_X_NEG, PAD_AXIS_LEFT_X_POS};
for (auto axis : lStickAxes) {
const char* label = PADGetAxisDirectionLabel(axis);
ImVec2 len = ImGui::CalcTextSize(label);
ImVec2 pos = ImGui::GetCursorPos();
ImGui::SetCursorPosY(pos.y + len.y / 4);
ImGui::SetCursorPosX(pos.x + abs(len.x - uiButtonSize));
ImGui::Text("%s", label);
ImGui::SameLine();
ImGui::SetCursorPosY(pos.y);
std::string dispName;
if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[axis]) {
dispName = fmt::format("Press a Key...##{}", label);
} else {
if (axisMappingList[axis].nativeAxis.nativeAxis != -1) {
const char* signStr;
if (axis == PAD_AXIS_TRIGGER_L || axis == PAD_AXIS_TRIGGER_R) {
signStr = "";
} else if (axisMappingList[axis].nativeAxis.sign == AXIS_SIGN_POSITIVE) {
signStr = "+";
} else {
signStr = "-";
}
dispName = fmt::format("{0}{1}##-{2}", PADGetNativeAxisName(axisMappingList[axis].nativeAxis), signStr, axis);
} else {
assert(axisMappingList[axis].nativeButton != -1);
dispName = fmt::format("{0}##-{1}", PADGetNativeButtonName(axisMappingList[axis].nativeButton), axis);
}
}
bool pressed = ImGui::Button(dispName.c_str(), btnSize);
if (pressed) {
m_controllerConfig.m_isReading = true;
m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort;
m_controllerConfig.m_pendingAxisMapping = &axisMappingList[axis];
PADBlockInput(true);
}
}
}
if (deadZones != nullptr) {
ImGui::Text("Dead Zone");
{
float tmp = static_cast<float>(deadZones->stickDeadZone * 100.f) / 32767.f;
if (ImGui::DragFloat("##mainDeadZone", &tmp, 0.5f, 0.f, 100.f, "%.3f%%")) {
deadZones->stickDeadZone = static_cast<u16>((tmp / 100.f) * 32767);
PADSerializeMappings();
}
}
}
ImGuiEndGroupPanel();
ImGui::SameLine();
// sub stick panel
ImGuiBeginGroupPanel("C Stick", ImVec2(150 * scale, 20 * scale));
drawVirtualStick("##subStick", ImVec2{ mDoCPd_c::getSubStickX(port), mDoCPd_c::getSubStickY(port) });
if (axisMappingList != nullptr) {
const PADAxis rStickAxes[] = {PAD_AXIS_RIGHT_Y_POS, PAD_AXIS_RIGHT_Y_NEG, PAD_AXIS_RIGHT_X_NEG, PAD_AXIS_RIGHT_X_POS};
for (auto axis : rStickAxes) {
const char* label = PADGetAxisDirectionLabel(axisMappingList[axis].padAxis);
ImVec2 len = ImGui::CalcTextSize(label);
ImVec2 pos = ImGui::GetCursorPos();
ImGui::SetCursorPosY(pos.y + len.y / 4);
ImGui::SetCursorPosX(pos.x + abs(len.x - uiButtonSize));
ImGui::Text("%s", label);
ImGui::SameLine();
ImGui::SetCursorPosY(pos.y);
std::string dispName;
if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingAxisMapping == &axisMappingList[axis]) {
dispName = fmt::format("Press a Key...##sub{}", label);
} else {
if (axisMappingList[axis].nativeAxis.nativeAxis != -1) {
const char* signStr;
if (axis == PAD_AXIS_TRIGGER_L || axis == PAD_AXIS_TRIGGER_R) {
signStr = "";
} else if (axisMappingList[axis].nativeAxis.sign == AXIS_SIGN_POSITIVE) {
signStr = "+";
} else {
signStr = "-";
}
dispName = fmt::format("{0}{1}##-{2}", PADGetNativeAxisName(axisMappingList[axis].nativeAxis), signStr, axis);
} else {
assert(axisMappingList[axis].nativeButton != -1);
dispName = fmt::format("{0}##-{1}", PADGetNativeButtonName(axisMappingList[axis].nativeButton), axis);
}
}
bool pressed = ImGui::Button(fmt::format("{0}##sub{1}", dispName, label).c_str(), btnSize);
if (pressed) {
m_controllerConfig.m_isReading = true;
m_controllerConfig.m_pendingPort = m_controllerConfig.m_selectedPort;
m_controllerConfig.m_pendingAxisMapping = &axisMappingList[axis];
PADBlockInput(true);
}
}
}
if (deadZones != nullptr) {
ImGui::Text("Dead Zone");
{
float tmp = static_cast<float>(deadZones->substickDeadZone * 100.f) / 32767.f;
if (ImGui::DragFloat("##subDeadZone", &tmp, 0.5f, 0.f, 100.f, "%.3f%%")) {
deadZones->substickDeadZone = static_cast<u16>((tmp / 100.f) * 32767);
PADSerializeMappings();
}
}
}
ImGuiEndGroupPanel();
ImGui::SameLine();
// Options panel
ImGuiBeginGroupPanel("Options", ImVec2(150 * scale, -1));
if (deadZones != nullptr) {
if (ImGui::Checkbox("Enable Dead Zones", &deadZones->useDeadzones)) {
PADSerializeMappings();
}
if (ImGui::Checkbox("Emulate Triggers", &deadZones->emulateTriggers)) {
PADSerializeMappings();
}
}
if (PADSupportsRumbleIntensity(m_controllerConfig.m_selectedPort)) {
ImGuiBeginGroupPanel("Rumble Intensity", ImVec2(150 * scale, -1));
u16 low;
u16 high;
(void)PADGetRumbleIntensity(m_controllerConfig.m_selectedPort, &low, &high);
float fLow = (static_cast<float>(low) / 32767.f) * 100.f;
bool changed = ImGui::SliderFloat("Low", &fLow, 0.f, 100.f, "%.0f%%");
float fHigh = (static_cast<float>(high) / 32767.f) * 100.f;
changed |= ImGui::SliderFloat("High", &fHigh, 0.f, 100.f, "%.0f%%");
if (changed) {
PADSetRumbleIntensity(m_controllerConfig.m_selectedPort,
static_cast<u16>((fLow / 100) * 32767),
static_cast<u16>((fHigh / 100) * 32767));
PADSerializeMappings();
}
if (ImGui::Button(fmt::format("{0}...##rumbleTest", m_controllerConfig.m_isRumbling ? "Stop": "Test").c_str(), {-1, 0})) {
PADControlMotor(m_controllerConfig.m_selectedPort, !m_controllerConfig.m_isRumbling ? PAD_MOTOR_RUMBLE : PAD_MOTOR_STOP_HARD);
m_controllerConfig.m_isRumbling ^= 1;
}
ImGuiEndGroupPanel();
}
ImGuiEndGroupPanel();
ImGui::End();
}
static std::string GetFormattedTime(OSTime ticks) { static std::string GetFormattedTime(OSTime ticks) {
OSCalendarTime time; OSCalendarTime time;
@@ -602,6 +45,7 @@ namespace dusk {
getSettings().game.enableTurboKeybind.setValue(false); getSettings().game.enableTurboKeybind.setValue(false);
getSettings().game.debugFlyCam.setValue(false); getSettings().game.debugFlyCam.setValue(false);
getSettings().game.autoSave.setValue(false);
} }
SpeedrunInfo m_speedrunInfo; SpeedrunInfo m_speedrunInfo;
@@ -612,12 +56,12 @@ namespace dusk {
} }
// L+R+A+Start to reset timer // L+R+A+Start to reset timer
if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigStart(PAD_1)) { if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigZ(PAD_1)) {
m_speedrunInfo.reset(); m_speedrunInfo.reset();
} }
// L+R+A+Z to manually stop timer // L+R+A+Z to manually stop timer
if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigZ(PAD_1)) { if (mDoCPd_c::getHoldL(PAD_1) && mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getHoldA(PAD_1) && mDoCPd_c::getTrigY(PAD_1)) {
if (m_speedrunInfo.m_isRunStarted) { if (m_speedrunInfo.m_isRunStarted) {
m_speedrunInfo.m_endTimestamp = OSGetTime() - m_speedrunInfo.m_startTimestamp; m_speedrunInfo.m_endTimestamp = OSGetTime() - m_speedrunInfo.m_startTimestamp;
m_speedrunInfo.m_isRunStarted = false; m_speedrunInfo.m_isRunStarted = false;
-20
View File
@@ -46,31 +46,11 @@ namespace dusk {
ImGuiMenuGame(); ImGuiMenuGame();
void draw(); void draw();
void windowInputViewer();
void windowControllerConfig();
void drawSpeedrunTimerOverlay(); void drawSpeedrunTimerOverlay();
static void ToggleFullscreen();
static void resetForSpeedrunMode(); static void resetForSpeedrunMode();
private: private:
struct {
int m_selectedPort = 0;
bool m_isReading = false;
PADButtonMapping* m_pendingButtonMapping = nullptr;
PADAxisMapping* m_pendingAxisMapping = nullptr;
int m_pendingPort = -1;
bool m_isRumbling = false;
} m_controllerConfig;
bool m_showControllerConfig = false;
bool m_showInputViewer = false;
bool m_showInputViewerGyro = false;
int m_inputOverlayCorner = 3;
std::string m_controllerName;
bool m_showTimerWindow = false; bool m_showTimerWindow = false;
}; };
} }
+8 -2
View File
@@ -48,6 +48,8 @@ namespace dusk {
ImGui::EndDisabled(); ImGui::EndDisabled();
} }
ImGui::Separator();
ImGui::Checkbox("Show Input Viewer", &m_showInputViewer);
#if DUSK_CAN_OPEN_DATA_FOLDER #if DUSK_CAN_OPEN_DATA_FOLDER
ImGui::Separator(); ImGui::Separator();
@@ -119,7 +121,9 @@ namespace dusk {
} }
void ImGuiMenuTools::ShowDebugOverlay() { void ImGuiMenuTools::ShowDebugOverlay() {
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F3, m_showDebugOverlay)) { if (!getSettings().backend.enableAdvancedSettings ||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F3, m_showDebugOverlay))
{
return; return;
} }
@@ -183,7 +187,9 @@ namespace dusk {
} }
void ImGuiMenuTools::ShowPlayerInfo() { void ImGuiMenuTools::ShowPlayerInfo() {
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F5, m_showPlayerInfo)) { if (!getSettings().backend.enableAdvancedSettings ||
!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F5, m_showPlayerInfo))
{
return; return;
} }

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