Compare commits

..

64 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
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
112 changed files with 5104 additions and 501 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}}
+56 -21
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
@@ -109,19 +111,26 @@ 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)
@@ -151,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
@@ -284,7 +294,7 @@ 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 MTX_USE_PS=1) set(GAME_COMPILE_DEFS TARGET_PC WIDESCREEN_SUPPORT=1 AVOID_UB=1 VERSION=0 MTX_USE_PS=1)
@@ -314,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)
@@ -332,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
+16 -2
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
@@ -102,8 +113,8 @@ struct UserSettings {
ConfigVar<bool> minimalHUD; 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;
@@ -120,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;
@@ -135,6 +147,7 @@ struct UserSettings {
ConfigVar<float> freeCameraSensitivity; ConfigVar<float> freeCameraSensitivity;
ConfigVar<bool> debugFlyCam; ConfigVar<bool> debugFlyCam;
ConfigVar<bool> debugFlyCamLockEvents; ConfigVar<bool> debugFlyCamLockEvents;
ConfigVar<bool> allowBackgroundInput;
// Cheats // Cheats
ConfigVar<bool> infiniteHearts; ConfigVar<bool> infiniteHearts;
@@ -172,6 +185,7 @@ 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; 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>
+27 -12
View File
@@ -163,30 +163,30 @@ 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);
} }
@@ -274,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;
}
}
+107 -15
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;
@@ -334,8 +374,8 @@ body.animate-in .intro-item {
} }
menu { menu {
left: 20dp; left: 32dp;
right: 20dp; right: 32dp;
width: auto; width: auto;
min-width: 0; min-width: 0;
max-width: none; max-width: none;
@@ -346,8 +386,8 @@ body.animate-in .intro-item {
} }
body.mirrored menu { body.mirrored menu {
left: 20dp; left: 32dp;
right: 20dp; right: 32dp;
flex-direction: row-reverse; flex-direction: row-reverse;
} }
@@ -355,7 +395,7 @@ body.animate-in .intro-item {
flex: 1 1 0; flex: 1 1 0;
min-width: 0; min-width: 0;
max-width: 48%; max-width: 48%;
margin-left: 32dp;
} }
body.mirrored hero { body.mirrored hero {
@@ -397,9 +437,61 @@ body.animate-in .intro-item {
decorator: horizontal-gradient(#FEE685FF #FEE68500); decorator: horizontal-gradient(#FEE685FF #FEE68500);
} }
.eyebrow, .eyebrow {
disc-info,
version-info {
display: none; 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;
}
} }
+14 -1
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;
@@ -291,6 +292,19 @@ icon.question-mark {
decorator: text("&#xeb8b;" center center); 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;
@@ -484,7 +498,6 @@ progress.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;
} }
+51 -12
View File
@@ -11262,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;
@@ -11315,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++) {
+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;
+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
+12 -19
View File
@@ -253,13 +253,6 @@ 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)) {
getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen); getSettings().video.enableFullscreen.setValue(!getSettings().video.enableFullscreen);
VISetWindowFullscreen(getSettings().video.enableFullscreen); VISetWindowFullscreen(getSettings().video.enableFullscreen);
@@ -383,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
+3 -2
View File
@@ -45,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;
@@ -55,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;
+27 -4
View File
@@ -1,7 +1,8 @@
#include "dusk/io.hpp"
#include <cstdio> #include <cstdio>
#include <filesystem> #include <filesystem>
#include "dusk/io.hpp"
using namespace dusk::io; using namespace dusk::io;
#if _WIN32 #if _WIN32
@@ -58,7 +59,7 @@ static FILE* OpenCore(const char* path, const MODE_TYPE* mode) {
FileStream::FileStream() noexcept : file(nullptr) { FileStream::FileStream() noexcept : file(nullptr) {
} }
FileStream::FileStream(void* file) : file(file) { FileStream::FileStream(FILE* file) : file(file) {
if (!file) { if (!file) {
CRASH("Invalid file handle"); CRASH("Invalid file handle");
} }
@@ -78,10 +79,18 @@ FileStream FileStream::OpenRead(const char* utf8Path) {
return FileStream(OpenCore(utf8Path, MODE("rb"))); return FileStream(OpenCore(utf8Path, MODE("rb")));
} }
FileStream FileStream::OpenRead(const std::filesystem::path& path) {
return FileStream(OpenCore(path, MODE("rb")));
}
FileStream FileStream::Create(const char* utf8Path) { FileStream FileStream::Create(const char* utf8Path) {
return FileStream(OpenCore(utf8Path, MODE("wb"))); return FileStream(OpenCore(utf8Path, MODE("wb")));
} }
FileStream FileStream::Create(const std::filesystem::path& path) {
return FileStream(OpenCore(path, MODE("wb")));
}
std::vector<u8> FileStream::ReadFull() { std::vector<u8> FileStream::ReadFull() {
const auto fileHandle = ThrowIfNotOpen(*this); const auto fileHandle = ThrowIfNotOpen(*this);
@@ -128,7 +137,11 @@ std::vector<u8> FileStream::ReadFull() {
} }
std::vector<u8> FileStream::ReadAllBytes(const char* utf8Path) { std::vector<u8> FileStream::ReadAllBytes(const char* utf8Path) {
auto handle = OpenRead(utf8Path); return ReadAllBytes(reinterpret_cast<const char8_t*>(utf8Path));
}
std::vector<u8> FileStream::ReadAllBytes(const std::filesystem::path& path) {
auto handle = OpenRead(path);
return std::move(handle.ReadFull()); return std::move(handle.ReadFull());
} }
@@ -142,6 +155,16 @@ void FileStream::Write(const char* data, size_t dataLen) {
} }
void FileStream::WriteAllText(const char* utf8Path, const std::string_view text) { void FileStream::WriteAllText(const char* utf8Path, const std::string_view text) {
auto handle = Create(utf8Path); WriteAllText(reinterpret_cast<const char8_t*>(utf8Path), text);
}
void FileStream::WriteAllText(const std::filesystem::path& path, const std::string_view text) {
auto handle = Create(path);
handle.Write(text.data(), text.size()); handle.Write(text.data(), text.size());
} }
FILE* FileStream::ToInner() {
auto handle = file;
file = nullptr;
return handle;
}
+20
View File
@@ -9,6 +9,8 @@
#include <stdexcept> #include <stdexcept>
#include <string_view> #include <string_view>
#include "dusk/logging.h"
namespace { namespace {
constexpr uint8_t hex_nibble_to_u8(char c) { constexpr uint8_t hex_nibble_to_u8(char c) {
@@ -42,6 +44,18 @@ constexpr XXH128_hash_t parse_xxh128(std::string_view hex) {
}; };
} }
const char* verification_state_name(dusk::DiscVerificationState state) noexcept {
switch (state) {
case dusk::DiscVerificationState::Success:
return "verified";
case dusk::DiscVerificationState::HashMismatch:
return "hash mismatch";
case dusk::DiscVerificationState::Unknown:
default:
return "unknown";
}
}
} // namespace } // namespace
namespace dusk::iso { namespace dusk::iso {
@@ -248,4 +262,10 @@ bool isPal(const char* path) {
DiscInfo info{}; DiscInfo info{};
return inspect(path, info) == ValidationError::Success && info.isPal; return inspect(path, info) == ValidationError::Success && info.isPal;
} }
void log_verification_state(std::string_view path, DiscVerificationState state) {
const std::string pathText = path.empty() ? "<none>" : std::string(path);
DuskLog.info(
"Disc verification status: {} (path: {})", verification_state_name(state), pathText);
}
} // namespace dusk::iso } // namespace dusk::iso
+1
View File
@@ -31,6 +31,7 @@ struct DiscInfo {
ValidationError inspect(const char* path, DiscInfo& info); ValidationError inspect(const char* path, DiscInfo& info);
ValidationError validate(const char* path, VerificationStatus& status, DiscInfo& info); ValidationError validate(const char* path, VerificationStatus& status, DiscInfo& info);
bool isPal(const char* path); bool isPal(const char* path);
void log_verification_state(std::string_view path, DiscVerificationState state);
} // namespace dusk::iso } // namespace dusk::iso
+6 -4
View File
@@ -8,6 +8,7 @@
#include <mutex> #include <mutex>
#include <string> #include <string>
#include "dusk/io.hpp"
#include "tracy/Tracy.hpp" #include "tracy/Tracy.hpp"
#if TARGET_ANDROID #if TARGET_ANDROID
@@ -40,7 +41,7 @@ std::atomic g_logStateAlive(true);
struct LogState { struct LogState {
std::mutex mutex; std::mutex mutex;
FILE* file = nullptr; FILE* file = nullptr;
std::string filePath; std::u8string filePath;
~LogState() { ~LogState() {
CloseFile(); CloseFile();
@@ -212,14 +213,14 @@ void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraL
} }
const std::filesystem::path logPath = logsDir / MakeTimestampedLogName(); const std::filesystem::path logPath = logsDir / MakeTimestampedLogName();
g_logState.file = std::fopen(logPath.string().c_str(), "wb"); g_logState.file = io::FileStream::Create(logPath).ToInner();
if (g_logState.file == nullptr) { if (g_logState.file == nullptr) {
std::fprintf(stderr, "[WARNING | dusk] Failed to open log file '%s'\n", std::fprintf(stderr, "[WARNING | dusk] Failed to open log file '%s'\n",
logPath.string().c_str()); logPath.string().c_str());
return; return;
} }
g_logState.filePath = logPath.string(); g_logState.filePath = logPath.u8string();
aurora::g_config.logCallback = &aurora_log_callback; aurora::g_config.logCallback = &aurora_log_callback;
aurora::g_config.logLevel = logLevel; aurora::g_config.logLevel = logLevel;
WriteLogLine(g_logState.file, "INFO", "dusk", "File logging initialized", 24); WriteLogLine(g_logState.file, "INFO", "dusk", "File logging initialized", 24);
@@ -237,5 +238,6 @@ const char* dusk::GetLogFilePath() {
return nullptr; return nullptr;
} }
std::lock_guard lock(g_logState.mutex); std::lock_guard lock(g_logState.mutex);
return g_logState.filePath.empty() ? nullptr : g_logState.filePath.c_str(); return reinterpret_cast<const char*>(
g_logState.filePath.empty() ? nullptr : g_logState.filePath.c_str());
} }
+10 -2
View File
@@ -50,7 +50,8 @@ UserSettings g_userSettings = {
.minimalHUD {"game.minimalHUD", false}, .minimalHUD {"game.minimalHUD", false},
.pauseOnFocusLost {"game.pauseOnFocusLost", false}, .pauseOnFocusLost {"game.pauseOnFocusLost", false},
.enableLinkDollRotation {"game.enableLinkDollRotation", false}, .enableLinkDollRotation {"game.enableLinkDollRotation", false},
.enableAchievementNotifications {"game.enableAchievementNotifications", true}, .enableAchievementToasts {"game.enableAchievementToasts", true},
.enableControllerToasts {"game.enableControllerToasts", true},
// Graphics // Graphics
.bloomMode {"game.bloomMode", BloomMode::Dusk}, .bloomMode {"game.bloomMode", BloomMode::Dusk},
@@ -67,6 +68,7 @@ UserSettings g_userSettings = {
.midnasLamentNonStop {"game.midnasLamentNonStop", false}, .midnasLamentNonStop {"game.midnasLamentNonStop", false},
// Input // Input
.gyroMode {"game.gyroMode", GyroMode::Sensor},
.enableGyroAim {"game.enableGyroAim", false}, .enableGyroAim {"game.enableGyroAim", false},
.enableGyroRollgoal {"game.enableGyroRollgoal", false}, .enableGyroRollgoal {"game.enableGyroRollgoal", false},
.gyroSensitivityX {"game.gyroSensitivityX", 1.0f}, .gyroSensitivityX {"game.gyroSensitivityX", 1.0f},
@@ -82,6 +84,7 @@ UserSettings g_userSettings = {
.freeCameraSensitivity {"game.freeCameraSensitivity", 1.0f}, .freeCameraSensitivity {"game.freeCameraSensitivity", 1.0f},
.debugFlyCam {"game.debugFlyCam", false}, .debugFlyCam {"game.debugFlyCam", false},
.debugFlyCamLockEvents {"game.debugFlyCamLockEvents", true}, .debugFlyCamLockEvents {"game.debugFlyCamLockEvents", true},
.allowBackgroundInput {"game.allowBackgroundInput", true},
// Cheats // Cheats
.infiniteHearts {"game.infiniteHearts", false}, .infiniteHearts {"game.infiniteHearts", false},
@@ -119,6 +122,7 @@ UserSettings g_userSettings = {
.showPipelineCompilation {"backend.showPipelineCompilation", false}, .showPipelineCompilation {"backend.showPipelineCompilation", false},
.wasPresetChosen {"backend.wasPresetChosen", false}, .wasPresetChosen {"backend.wasPresetChosen", false},
.enableCrashReporting {"backend.enableCrashReporting", true}, .enableCrashReporting {"backend.enableCrashReporting", true},
.checkForUpdates {"backend.checkForUpdates", true},
.cardFileType {"backend.cardFileType", static_cast<int>(CARD_GCIFOLDER)}, .cardFileType {"backend.cardFileType", static_cast<int>(CARD_GCIFOLDER)},
.enableAdvancedSettings {"backend.enableAdvancedSettings", false}, .enableAdvancedSettings {"backend.enableAdvancedSettings", false},
} }
@@ -182,7 +186,8 @@ void registerSettings() {
Register(g_userSettings.game.freeMagicArmor); Register(g_userSettings.game.freeMagicArmor);
Register(g_userSettings.game.restoreWiiGlitches); Register(g_userSettings.game.restoreWiiGlitches);
Register(g_userSettings.game.enableLinkDollRotation); Register(g_userSettings.game.enableLinkDollRotation);
Register(g_userSettings.game.enableAchievementNotifications); Register(g_userSettings.game.enableAchievementToasts);
Register(g_userSettings.game.enableControllerToasts);
Register(g_userSettings.game.noMissClimbing); Register(g_userSettings.game.noMissClimbing);
Register(g_userSettings.game.noLowHpSound); Register(g_userSettings.game.noLowHpSound);
Register(g_userSettings.game.midnasLamentNonStop); Register(g_userSettings.game.midnasLamentNonStop);
@@ -202,6 +207,7 @@ void registerSettings() {
Register(g_userSettings.game.superClawshot); Register(g_userSettings.game.superClawshot);
Register(g_userSettings.game.alwaysGreatspin); Register(g_userSettings.game.alwaysGreatspin);
Register(g_userSettings.game.enableFrameInterpolation); Register(g_userSettings.game.enableFrameInterpolation);
Register(g_userSettings.game.gyroMode);
Register(g_userSettings.game.enableGyroAim); Register(g_userSettings.game.enableGyroAim);
Register(g_userSettings.game.enableGyroRollgoal); Register(g_userSettings.game.enableGyroRollgoal);
Register(g_userSettings.game.gyroSensitivityX); Register(g_userSettings.game.gyroSensitivityX);
@@ -214,6 +220,7 @@ void registerSettings() {
Register(g_userSettings.game.freeCamera); Register(g_userSettings.game.freeCamera);
Register(g_userSettings.game.debugFlyCam); Register(g_userSettings.game.debugFlyCam);
Register(g_userSettings.game.debugFlyCamLockEvents); Register(g_userSettings.game.debugFlyCamLockEvents);
Register(g_userSettings.game.allowBackgroundInput);
Register(g_userSettings.backend.isoPath); Register(g_userSettings.backend.isoPath);
Register(g_userSettings.backend.isoVerification); Register(g_userSettings.backend.isoVerification);
@@ -222,6 +229,7 @@ void registerSettings() {
Register(g_userSettings.backend.showPipelineCompilation); Register(g_userSettings.backend.showPipelineCompilation);
Register(g_userSettings.backend.wasPresetChosen); Register(g_userSettings.backend.wasPresetChosen);
Register(g_userSettings.backend.enableCrashReporting); Register(g_userSettings.backend.enableCrashReporting);
Register(g_userSettings.backend.checkForUpdates);
Register(g_userSettings.backend.cardFileType); Register(g_userSettings.backend.cardFileType);
Register(g_userSettings.backend.enableAdvancedSettings); Register(g_userSettings.backend.enableAdvancedSettings);
} }
+25 -2
View File
@@ -16,9 +16,8 @@ struct CategoryInfo {
}; };
constexpr CategoryInfo kCategories[] = { constexpr CategoryInfo kCategories[] = {
{AchievementCategory::Story, "Story"},
{AchievementCategory::Collection, "Collection"},
{AchievementCategory::Challenge, "Challenge"}, {AchievementCategory::Challenge, "Challenge"},
{AchievementCategory::Collection, "Collection"},
{AchievementCategory::Minigame, "Minigame"}, {AchievementCategory::Minigame, "Minigame"},
{AchievementCategory::Misc, "Misc"}, {AchievementCategory::Misc, "Misc"},
{AchievementCategory::Glitched, "Glitched"}, {AchievementCategory::Glitched, "Glitched"},
@@ -114,6 +113,13 @@ private:
AchievementsWindow::AchievementsWindow() { AchievementsWindow::AchievementsWindow() {
const auto all = AchievementSystem::get().getAchievements(); const auto all = AchievementSystem::get().getAchievements();
{
auto elem = mDocument->CreateElement("div");
elem->SetClass("achievement-total", true);
mTotalEl = mRoot->AppendChild(std::move(elem));
updateTotal();
}
for (const auto& catInfo : kCategories) { for (const auto& catInfo : kCategories) {
int catTotal = 0; int catTotal = 0;
for (const auto& a : all) { for (const auto& a : all) {
@@ -201,8 +207,25 @@ void AchievementsWindow::update() {
if (dirty) { if (dirty) {
mSnapshot = current; mSnapshot = current;
refresh_active_tab(); refresh_active_tab();
updateTotal();
} }
Window::update(); Window::update();
} }
void AchievementsWindow::updateTotal() {
if (mTotalEl == nullptr) {
return;
}
const auto all = AchievementSystem::get().getAchievements();
int total = static_cast<int>(all.size());
int unlocked = 0;
for (const auto& a : all) {
if (a.unlocked) {
++unlocked;
}
}
const int pct = total > 0 ? (unlocked * 100 / total) : 0;
mTotalEl->SetInnerRML(fmt::format("{}%", pct));
}
} // namespace dusk::ui } // namespace dusk::ui
+2
View File
@@ -13,7 +13,9 @@ public:
void update() override; void update() override;
private: private:
void updateTotal();
std::vector<Achievement> mSnapshot; std::vector<Achievement> mSnapshot;
Rml::Element* mTotalEl = nullptr;
}; };
} // namespace dusk::ui } // namespace dusk::ui
+15
View File
@@ -5,6 +5,7 @@
#include "Z2AudioLib/Z2SeMgr.h" #include "Z2AudioLib/Z2SeMgr.h"
#include "m_Do/m_Do_audio.h" #include "m_Do/m_Do_audio.h"
#include <imgui.h>
namespace dusk::ui { namespace dusk::ui {
namespace { namespace {
@@ -106,6 +107,7 @@ bool Document::visible() const {
bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) { bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) {
if (cmd == NavCommand::Menu) { if (cmd == NavCommand::Menu) {
toggle_cursor_if_gyro(!visible());
mDoAud_seStartMenu(visible() ? kSoundMenuClose : kSoundMenuOpen); mDoAud_seStartMenu(visible() ? kSoundMenuClose : kSoundMenuOpen);
toggle(); toggle();
return true; return true;
@@ -113,4 +115,17 @@ bool Document::handle_nav_command(Rml::Event& event, NavCommand cmd) {
return false; return false;
} }
void Document::toggle_cursor_if_gyro(bool cursor_enabled) {
if (dusk::getSettings().game.gyroMode.getValue() == GyroMode::Mouse)
{
if (cursor_enabled) {
ImGui::GetIO().ConfigFlags &= ~ImGuiConfigFlags_NoMouseCursorChange;
SDL_ShowCursor();
} else {
ImGui::GetIO().ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange;
SDL_HideCursor();
}
}
}
} // namespace dusk::ui } // namespace dusk::ui
+2
View File
@@ -43,6 +43,8 @@ public:
bool pending_close() const { return mPendingClose; } bool pending_close() const { return mPendingClose; }
bool closed() const { return mClosed; } bool closed() const { return mClosed; }
void toggle_cursor_if_gyro(bool);
protected: protected:
virtual bool handle_nav_command(Rml::Event& event, NavCommand cmd); virtual bool handle_nav_command(Rml::Event& event, NavCommand cmd);
+3 -3
View File
@@ -1881,9 +1881,9 @@ EditorWindow::EditorWindow() {
} }
}); });
add_tab("Flags", [this](Rml::Element* content) { //add_tab("Flags", [this](Rml::Element* content) {
// TODO // // TODO
}); //});
add_tab("Minigame", [this](Rml::Element* content) { add_tab("Minigame", [this](Rml::Element* content) {
auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled); auto& leftPane = add_child<Pane>(content, Pane::Type::Controlled);
+9 -4
View File
@@ -193,9 +193,9 @@ Rml::String format_graphics_setting_value(GraphicsOption option, int value) {
return ""; return "";
} }
GraphicsTuner::GraphicsTuner(GraphicsTunerProps props) GraphicsTuner::GraphicsTuner(GraphicsTunerProps props, bool prelaunch)
: Document(kDocumentSource), mOption(props.option), mValueMin(props.valueMin), : Document(kDocumentSource), mOption(props.option), mValueMin(props.valueMin),
mValueMax(props.valueMax), mDefaultValue(props.defaultValue) { mValueMax(props.valueMax), mDefaultValue(props.defaultValue), mPrelaunch(prelaunch) {
if (mDocument == nullptr) { if (mDocument == nullptr) {
return; return;
} }
@@ -207,7 +207,7 @@ GraphicsTuner::GraphicsTuner(GraphicsTunerProps props)
description->SetInnerRML(escape(props.helpText)); description->SetInnerRML(escape(props.helpText));
} }
if (auto* carouselParent = mDocument->GetElementById("carousel-container")) { if (auto* carouselParent = mDocument->GetElementById("carousel-container")) {
add_component<SteppedCarousel>(carouselParent, mCarousel = &add_component<SteppedCarousel>(carouselParent,
SteppedCarousel::Props{ SteppedCarousel::Props{
.min = mValueMin, .min = mValueMin,
.max = mValueMax, .max = mValueMax,
@@ -281,7 +281,12 @@ bool GraphicsTuner::handle_nav_command(Rml::Event& event, NavCommand cmd) {
pop(); pop();
return true; return true;
} }
return Document::handle_nav_command(event, cmd);
if (mCarousel && mCarousel->handle_nav_command(cmd)) {
return true;
}
return mPrelaunch ? false : Document::handle_nav_command(event, cmd);
} }
void GraphicsTuner::reset_default() { void GraphicsTuner::reset_default() {
+4 -2
View File
@@ -28,9 +28,9 @@ public:
bool focus() override; bool focus() override;
void update() override; void update() override;
bool handle_nav_command(NavCommand cmd);
private: private:
bool handle_nav_command(NavCommand cmd);
void apply(int value); void apply(int value);
Props mProps; Props mProps;
@@ -59,7 +59,7 @@ struct GraphicsTunerProps {
class GraphicsTuner : public Document { class GraphicsTuner : public Document {
public: public:
explicit GraphicsTuner(GraphicsTunerProps props); explicit GraphicsTuner(GraphicsTunerProps props, bool prelaunch);
void show() override; void show() override;
void hide(bool close) override; void hide(bool close) override;
@@ -86,7 +86,9 @@ private:
int mValueMax = 0; int mValueMax = 0;
int mDefaultValue = 0; int mDefaultValue = 0;
std::vector<std::unique_ptr<Component> > mComponents; std::vector<std::unique_ptr<Component> > mComponents;
SteppedCarousel* mCarousel;
Rml::Element* mRoot; Rml::Element* mRoot;
bool mPrelaunch;
}; };
} // namespace dusk::ui } // namespace dusk::ui
+14 -2
View File
@@ -42,6 +42,7 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById(
mTabBar = std::make_unique<TabBar>(mRoot, TabBar::Props{ mTabBar = std::make_unique<TabBar>(mRoot, TabBar::Props{
.onClose = .onClose =
[this] { [this] {
toggle_cursor_if_gyro(false);
mDoAud_seStartMenu(kSoundMenuClose); mDoAud_seStartMenu(kSoundMenuClose);
hide(false); hide(false);
}, },
@@ -68,12 +69,17 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById(
{ {
ModalAction{ ModalAction{
.label = "Cancel", .label = "Cancel",
.onPressed = dismiss, .onPressed =
[this, dismiss](Modal& modal) {
mDoAud_seStartMenu(kSoundWindowClose);
dismiss(modal);
},
}, },
ModalAction{ ModalAction{
.label = "Reset", .label = "Reset",
.onPressed = .onPressed =
[this, dismiss](Modal& modal) { [this, dismiss](Modal& modal) {
mDoAud_seStartMenu(kSoundClick);
if (fpcM_SearchByName(fpcNm_LOGO_SCENE_e)) { if (fpcM_SearchByName(fpcNm_LOGO_SCENE_e)) {
dismiss(modal); dismiss(modal);
return; return;
@@ -98,12 +104,17 @@ MenuBar::MenuBar() : Document(kDocumentSource), mRoot(mDocument->GetElementById(
{ {
ModalAction{ ModalAction{
.label = "Cancel", .label = "Cancel",
.onPressed = dismiss, .onPressed =
[dismiss](Modal& modal) {
mDoAud_seStartMenu(kSoundWindowClose);
dismiss(modal);
},
}, },
ModalAction{ ModalAction{
.label = "Quit", .label = "Quit",
.onPressed = .onPressed =
[dismiss](Modal& modal) { [dismiss](Modal& modal) {
mDoAud_seStartMenu(kSoundClick);
dismiss(modal); dismiss(modal);
IsRunning = false; IsRunning = false;
}, },
@@ -193,6 +204,7 @@ bool MenuBar::handle_nav_command(Rml::Event& event, NavCommand cmd) {
return true; return true;
} }
if (cmd == NavCommand::Cancel && visible()) { if (cmd == NavCommand::Cancel && visible()) {
toggle_cursor_if_gyro(false);
mDoAud_seStartMenu(kSoundMenuClose); mDoAud_seStartMenu(kSoundMenuClose);
hide(false); hide(false);
return true; return true;
+2
View File
@@ -36,6 +36,8 @@ Modal::Modal(Props props) : WindowSmall("modal", "modal-dialog"), mProps(std::mo
}); });
mButtons.push_back(std::move(btn)); mButtons.push_back(std::move(btn));
} }
mDoAud_seStartMenu(kSoundWindowOpen);
} }
bool Modal::focus() { bool Modal::focus() {
+2 -2
View File
@@ -54,7 +54,7 @@ void NumberButton::set_value(Rml::String value) {
} }
bool NumberButton::handle_nav_command(NavCommand cmd) { bool NumberButton::handle_nav_command(NavCommand cmd) {
if (cmd == NavCommand::Left || cmd == NavCommand::Right) { if (!is_editing() && (cmd == NavCommand::Left || cmd == NavCommand::Right)) {
const int newValue = std::clamp( const int newValue = std::clamp(
mGetValue() + (cmd == NavCommand::Right ? mStep : -mStep), mMin, mMax); mGetValue() + (cmd == NavCommand::Right ? mStep : -mStep), mMin, mMax);
if (newValue != mGetValue()) { if (newValue != mGetValue()) {
@@ -66,4 +66,4 @@ bool NumberButton::handle_nav_command(NavCommand cmd) {
return BaseStringButton::handle_nav_command(cmd); return BaseStringButton::handle_nav_command(cmd);
} }
} // namespace dusk::ui } // namespace dusk::ui
+14 -3
View File
@@ -10,6 +10,10 @@
#include <algorithm> #include <algorithm>
#include <dolphin/pad.h> #include <dolphin/pad.h>
#if defined(__APPLE__)
#include <TargetConditionals.h>
#endif
namespace dusk::ui { namespace dusk::ui {
namespace { namespace {
aurora::Module Log{"dusk::ui::overlay"}; aurora::Module Log{"dusk::ui::overlay"};
@@ -33,7 +37,7 @@ constexpr std::array<std::pair<const char*, const char*>, 3> kAutoSaveLayers{{
constexpr auto kMenuNotificationDuration = std::chrono::milliseconds(2500); constexpr auto kMenuNotificationDuration = std::chrono::milliseconds(2500);
constexpr std::array<const char*, 4> kFpsCorners = { "tl", "tr", "bl", "br" }; constexpr std::array<const char*, 4> kFpsCorners = {"tl", "tr", "bl", "br"};
Rml::Element* create_toast(Rml::Element* parent, const Toast& toast) { Rml::Element* create_toast(Rml::Element* parent, const Toast& toast) {
if (toast.type == "autosave") { if (toast.type == "autosave") {
@@ -130,13 +134,19 @@ Rml::String back_button_name() {
return "Back"; return "Back";
} }
#if defined(TARGET_ANDROID) || (defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST)
constexpr auto kMenuNotificationPrefix = "3-finger tap or";
#else
constexpr auto kMenuNotificationPrefix = "Press F1 or";
#endif
Rml::Element* create_menu_notification(Rml::Element* parent) { Rml::Element* create_menu_notification(Rml::Element* parent) {
auto* elem = append(parent, "toast"); auto* elem = append(parent, "toast");
elem->SetClass("menu-notification", true); elem->SetClass("menu-notification", true);
auto* message = append(elem, "message"); auto* message = append(elem, "message");
auto* row = append(message, "row"); auto* row = append(message, "row");
append(row, "span")->SetInnerRML("Press F1 or"); append(row, "span")->SetInnerRML(kMenuNotificationPrefix);
auto* icon = append(row, "icon"); auto* icon = append(row, "icon");
icon->SetClass("controller", true); icon->SetClass("controller", true);
append(row, "span")->SetInnerRML(escape(back_button_name())); append(row, "span")->SetInnerRML(escape(back_button_name()));
@@ -242,7 +252,8 @@ void Overlay::update() {
const Uint64 now = SDL_GetPerformanceCounter(); const Uint64 now = SDL_GetPerformanceCounter();
// Limit updates to twice per second // Limit updates to twice per second
const bool refreshLabel = perfFreq == 0 || mFpsLastUpdate == 0 || const bool refreshLabel =
perfFreq == 0 || mFpsLastUpdate == 0 ||
static_cast<double>(now - mFpsLastUpdate) >= 0.5 * static_cast<double>(perfFreq); static_cast<double>(now - mFpsLastUpdate) >= 0.5 * static_cast<double>(perfFreq);
if (refreshLabel) { if (refreshLabel) {
mFpsLastUpdate = now; mFpsLastUpdate = now;
+152 -4
View File
@@ -5,12 +5,15 @@
#include "dusk/iso_validate.hpp" #include "dusk/iso_validate.hpp"
#include "dusk/main.h" #include "dusk/main.h"
#include "dusk/settings.h" #include "dusk/settings.h"
#include "dusk/update_check.hpp"
#include "modal.hpp" #include "modal.hpp"
#include "preset.hpp" #include "preset.hpp"
#include "settings.hpp" #include "settings.hpp"
#include "version.h" #include "version.h"
#include <SDL3/SDL_dialog.h> #include <SDL3/SDL_dialog.h>
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_misc.h>
#include <aurora/lib/logging.hpp> #include <aurora/lib/logging.hpp>
#include <aurora/lib/window.hpp> #include <aurora/lib/window.hpp>
#include <fmt/format.h> #include <fmt/format.h>
@@ -54,7 +57,13 @@ const Rml::String kDocumentSource = R"RML(
</disc-info> </disc-info>
<version-info class="intro-item delay-5"> <version-info class="intro-item delay-5">
<div class="version">Version <span id="version-text"></span></div> <div class="version">Version <span id="version-text"></span></div>
<div class="update"><span>Update available!</span> Download</div> <div id="update-status" class="update">
<span id="update-message"></span>
<button id="update-download">
<span id="update-download-label"></span>
&nbsp;<icon />
</button>
</div>
</version-info> </version-info>
</content> </content>
</body> </body>
@@ -114,6 +123,44 @@ struct DiscVerificationTask {
std::unique_ptr<DiscVerificationTask> sDiscVerificationTask; std::unique_ptr<DiscVerificationTask> sDiscVerificationTask;
bool sDiscVerificationModalPushed = false; bool sDiscVerificationModalPushed = false;
struct UpdateCheckTask {
UpdateCheckTask() {
worker = std::thread([this] {
try {
result = update_check::check_latest_github_release("TwilitRealm", "dusk");
} catch (const std::exception& e) {
result = {
.status = update_check::Status::Failed,
.message = fmt::format("Update check failed with exception: {}", e.what()),
};
} catch (...) {
result = {
.status = update_check::Status::Failed,
.message = "Update check failed with an unknown exception",
};
}
done.store(true, std::memory_order_release);
});
}
~UpdateCheckTask() { join(); }
void join() {
if (worker.joinable()) {
worker.join();
}
}
[[nodiscard]] bool finished() const { return done.load(std::memory_order_acquire); }
update_check::Result result;
std::atomic_bool done = false;
std::thread worker;
};
std::unique_ptr<UpdateCheckTask> sUpdateCheckTask;
std::optional<update_check::Result> sUpdateCheckResult;
bool verification_state_allows_launch(iso::ValidationError validation) noexcept { bool verification_state_allows_launch(iso::ValidationError validation) noexcept {
return validation == iso::ValidationError::Unknown || return validation == iso::ValidationError::Unknown ||
validation == iso::ValidationError::Success || validation == iso::ValidationError::Success ||
@@ -185,6 +232,52 @@ std::optional<DiscVerificationResult> take_finished_disc_verification() {
return result; return result;
} }
void begin_update_check() {
if (!getSettings().backend.checkForUpdates.getValue()) {
return;
}
if (sUpdateCheckTask != nullptr || sUpdateCheckResult.has_value()) {
return;
}
sUpdateCheckTask = std::make_unique<UpdateCheckTask>();
}
std::optional<update_check::Result> take_finished_update_check() {
if (sUpdateCheckTask == nullptr || !sUpdateCheckTask->finished()) {
return std::nullopt;
}
sUpdateCheckTask->join();
auto result = std::move(sUpdateCheckTask->result);
sUpdateCheckTask.reset();
return result;
}
std::string update_release_label(const update_check::Release& release) {
std::string_view tagName = release.tagName;
if (!tagName.empty() && tagName.front() == 'v') {
tagName.remove_prefix(1);
}
return std::string(tagName);
}
void open_update_release() {
if (!sUpdateCheckResult.has_value() ||
sUpdateCheckResult->status != update_check::Status::UpdateAvailable)
{
return;
}
const std::string url = sUpdateCheckResult->latest.htmlUrl;
if (url.empty()) {
PrelaunchLog.warn("Update is available, but the release did not include a download URL");
return;
}
if (!SDL_OpenURL(url.c_str())) {
PrelaunchLog.warn("Failed to open update URL '{}': {}", url, SDL_GetError());
}
}
std::string get_error_msg(iso::ValidationError error) { std::string get_error_msg(iso::ValidationError error) {
switch (error) { switch (error) {
default: default:
@@ -209,9 +302,17 @@ std::string get_error_msg(iso::ValidationError error) {
} }
void persist_disc_choice(const std::string& path, iso::ValidationError validation) { void persist_disc_choice(const std::string& path, iso::ValidationError validation) {
const auto previousPath = getSettings().backend.isoPath.getValue();
const auto previousVerification = getSettings().backend.isoVerification.getValue();
const auto verification = verification_to_config(validation);
getSettings().backend.isoPath.setValue(path); getSettings().backend.isoPath.setValue(path);
getSettings().backend.isoVerification.setValue(verification_to_config(validation)); getSettings().backend.isoVerification.setValue(verification);
config::Save(); config::Save();
if (previousPath != path || previousVerification != verification) {
iso::log_verification_state(path, verification);
}
} }
void apply_valid_disc_result( void apply_valid_disc_result(
@@ -352,8 +453,7 @@ private:
} }
if (mFileName != nullptr) { if (mFileName != nullptr) {
std::string fileName = std::string fileName = display_name_for_path(sDiscVerificationTask->path);
std::filesystem::path(sDiscVerificationTask->path).filename().string();
if (fileName.empty()) { if (fileName.empty()) {
fileName = sDiscVerificationTask->path; fileName = sDiscVerificationTask->path;
} }
@@ -582,6 +682,7 @@ void try_apply_mirrored_layout(Rml::Element* body) {
Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementById("root")) { Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementById("root")) {
ensure_initialized(); ensure_initialized();
begin_update_check();
if (auto* menuList = mDocument->GetElementById("menu-list")) { if (auto* menuList = mDocument->GetElementById("menu-list")) {
auto& state = prelaunch_state(); auto& state = prelaunch_state();
@@ -594,6 +695,8 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
return; return;
} }
toggle_cursor_if_gyro(false);
mDoAud_seStartMenu(kSoundPlay); mDoAud_seStartMenu(kSoundPlay);
show_menu_notification(); show_menu_notification();
@@ -629,6 +732,23 @@ Prelaunch::Prelaunch() : Document(kDocumentSource), mRoot(mDocument->GetElementB
mDiscStatus = mDocument->GetElementById("disc-status"); mDiscStatus = mDocument->GetElementById("disc-status");
mDiscDetail = mDocument->GetElementById("disc-version"); mDiscDetail = mDocument->GetElementById("disc-version");
mVersion = mDocument->GetElementById("version-text"); mVersion = mDocument->GetElementById("version-text");
mUpdateStatus = mDocument->GetElementById("update-status");
mUpdateMessage = mDocument->GetElementById("update-message");
mUpdateDownload = mDocument->GetElementById("update-download");
mUpdateDownloadLabel = mDocument->GetElementById("update-download-label");
if (mUpdateDownload != nullptr) {
listen(mUpdateDownload, Rml::EventId::Click, [](Rml::Event& event) {
open_update_release();
event.StopPropagation();
});
listen(mUpdateDownload, Rml::EventId::Keydown, [](Rml::Event& event) {
if (map_nav_event(event) == NavCommand::Confirm) {
open_update_release();
event.StopPropagation();
}
});
}
try_apply_mirrored_layout(mDocument); try_apply_mirrored_layout(mDocument);
@@ -767,6 +887,34 @@ void Prelaunch::update() {
} }
mVersion->SetInnerRML(escape(versionStr)); mVersion->SetInnerRML(escape(versionStr));
} }
if (mUpdateStatus != nullptr && mUpdateMessage != nullptr) {
if (auto result = take_finished_update_check()) {
if (result->status == update_check::Status::Failed) {
PrelaunchLog.error("Failed to check for updates: {}", result->message);
}
sUpdateCheckResult = std::move(*result);
}
if (sUpdateCheckTask != nullptr) {
mUpdateStatus->SetAttribute("state", "checking");
mUpdateMessage->SetInnerRML("Checking for updates...");
} else if (!sUpdateCheckResult.has_value() ||
sUpdateCheckResult->status == update_check::Status::UpToDate)
{
mUpdateStatus->RemoveAttribute("state");
mUpdateMessage->SetInnerRML("");
} else if (sUpdateCheckResult->status == update_check::Status::UpdateAvailable) {
mUpdateStatus->SetAttribute("state", "available");
mUpdateMessage->SetInnerRML("Update available!");
if (mUpdateDownloadLabel != nullptr) {
mUpdateDownloadLabel->SetInnerRML(escape(
fmt::format("Download {}", update_release_label(sUpdateCheckResult->latest))));
}
} else {
mUpdateStatus->SetAttribute("state", "failed");
mUpdateMessage->SetInnerRML("Failed to check for updates");
}
}
Document::update(); Document::update();
} }

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