Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 625f752fb9 | |||
| 04b5861f29 | |||
| 453e958068 | |||
| e7d2fbcc0b | |||
| 8f71c70d14 | |||
| df23edcb69 | |||
| daff157027 | |||
| 0c23bd4332 | |||
| 7562486449 | |||
| 5e08b810fc | |||
| c66cccf660 | |||
| 3b1118229b | |||
| 491da372a1 | |||
| a2f463d146 | |||
| 63b3ce4849 | |||
| 8280ac00a0 | |||
| 45ef0d72b1 | |||
| ad9c460ec9 | |||
| 23dc9bc39a | |||
| bf23d44389 | |||
| d0b9b6d10f | |||
| 2f83753260 | |||
| eeb0ad77a4 | |||
| 594cadcf7d | |||
| 4290726691 | |||
| 80dd5ff278 | |||
| 08efb9a3cf | |||
| d8a1dd1da4 | |||
| 13dd3c3932 | |||
| dd7885da9c | |||
| 5a05433a2b | |||
| c3ff3884d7 | |||
| e42c4d3174 | |||
| 06c77a6818 | |||
| 4d4a80891f | |||
| 71c892368d | |||
| d2a1dda523 | |||
| 78179eb93f | |||
| 34e10d3844 | |||
| 65e8577253 | |||
| a4fcc10f5f | |||
| a2c2988666 | |||
| abec043249 | |||
| 699d069b0a | |||
| 4d67033ff8 | |||
| 84ffd67622 | |||
| 1c85ee63eb | |||
| 81c7213993 | |||
| be82e606b2 | |||
| 44da1a9f7d | |||
| d0f8ea56f9 | |||
| e472b36cef | |||
| 3934e09c8f | |||
| 3136816ce9 | |||
| 6fd3762ffc | |||
| 97a1190713 | |||
| 73a3bd9ae8 | |||
| fc533dbdc7 | |||
| 6217e071d2 | |||
| 673ca7f686 | |||
| 29a1cff7ea | |||
| 3c5152a67b | |||
| 44f3828f68 | |||
| 3f560b060c |
@@ -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}}
|
||||||
|
|||||||
@@ -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,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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 928 KiB |
|
Before Width: | Height: | Size: 306 B After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 79 B After Width: | Height: | Size: 1014 B |
|
Before Width: | Height: | Size: 761 B After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 99 B After Width: | Height: | Size: 2.7 KiB |
|
Before Width: | Height: | Size: 123 B After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 179 B After Width: | Height: | Size: 8.7 KiB |
@@ -6,4 +6,4 @@ Exec=dusk
|
|||||||
Icon=dusk
|
Icon=dusk
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Graphics;3DGraphics;Game
|
Categories=Game;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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("" center center);
|
decorator: text("" 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("" center center);
|
decorator: text("" 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("" center center);
|
decorator: text("" 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("" center center);
|
decorator: text("" 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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("" 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("" center center);
|
decorator: text("" 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%;
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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, ¤t.pos, &tevStr);
|
g_env_light.settingTevStruct(0, ¤t.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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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++) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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!",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
<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();
|
||||||
}
|
}
|
||||||
|
|||||||