mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-06 17:32:23 -04:00
Merge branch 'main' of https://www.github.com/TwilitRealm/dusklight into rando-mod
This commit is contained in:
@@ -76,7 +76,7 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dusklight-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}}
|
||||
name: dusklight-${{env.APP_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}}
|
||||
path: |
|
||||
build/install/Dusklight-*.AppImage
|
||||
build/install/debug.tar.*
|
||||
@@ -145,7 +145,7 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dusklight-${{env.DUSK_VERSION}}-${{matrix.artifact_name}}
|
||||
name: dusklight-${{env.APP_VERSION}}-${{matrix.artifact_name}}
|
||||
path: |
|
||||
build/install/Dusklight.app
|
||||
build/install/debug.tar.*
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dusklight-${{env.DUSK_VERSION}}-android-${{matrix.artifact_arch}}
|
||||
name: dusklight-${{env.APP_VERSION}}-android-${{matrix.artifact_arch}}
|
||||
path: upload/
|
||||
|
||||
build-windows:
|
||||
@@ -283,7 +283,7 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dusklight-${{env.DUSK_VERSION}}-win32-msvc-${{matrix.artifact_arch}}
|
||||
name: dusklight-${{env.APP_VERSION}}-win32-msvc-${{matrix.artifact_arch}}
|
||||
path: |
|
||||
build/install/*.exe
|
||||
build/install/*.dll
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
[submodule "extern/aurora"]
|
||||
path = extern/aurora
|
||||
url = https://github.com/encounter/aurora.git
|
||||
[submodule "extern/borealis"]
|
||||
path = extern/borealis
|
||||
url = https://github.com/encounter/borealis.git
|
||||
|
||||
+12
-127
@@ -5,10 +5,11 @@ if (NOT CMAKE_BUILD_TYPE)
|
||||
"Build type options: Debug Release RelWithDebInfo MinSizeRel" FORCE)
|
||||
endif ()
|
||||
|
||||
include(cmake/DetectVersion.cmake)
|
||||
detect_version()
|
||||
include(extern/borealis/cmake/DetectVersion.cmake)
|
||||
borealis_detect_version()
|
||||
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
|
||||
project(dusklight LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING})
|
||||
project(dusklight LANGUAGES C CXX VERSION ${BOREALIS_APP_VERSION})
|
||||
|
||||
if (APPLE)
|
||||
enable_language(OBJC OBJCXX)
|
||||
endif ()
|
||||
@@ -74,6 +75,8 @@ set(AURORA_ENABLE_RMLUI ON CACHE BOOL "Enable RmlUi UI support" FORCE)
|
||||
add_subdirectory(extern/aurora EXCLUDE_FROM_ALL)
|
||||
target_compile_definitions(aurora_mtx PRIVATE MTX_USE_PS=1)
|
||||
|
||||
add_subdirectory(extern/borealis EXCLUDE_FROM_ALL)
|
||||
|
||||
add_subdirectory(libs/freeverb)
|
||||
|
||||
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
@@ -85,8 +88,6 @@ endif ()
|
||||
option(DUSK_BUILD_WARNINGS "Enable compiler warnings (off by default)")
|
||||
option(DUSK_SELECTED_OPT "If on, selected parts of the project will be compiled with optimizations on Debug, intending to make the game run at 30 FPS. Note for MSVC: you will need to remove '/RTC1' from your debug flags in CMake.")
|
||||
option(DUSK_MOVIE_SUPPORT "If on, compile against libjpeg-turbo to enable THP file decoding" ON)
|
||||
option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking support" ON)
|
||||
option(DUSK_ENABLE_SENTRY_NATIVE "Enable sentry-native crash reporting support" OFF)
|
||||
option(DUSK_PACKAGE_INSTALL "Install Dusklight with a Linux-native file structure" OFF)
|
||||
option(DUSK_GFX_DEBUG_GROUPS "Report debug groups to the native graphics API" ${DUSK_GFX_DEBUG_GROUPS_DEFAULT})
|
||||
option(DUSK_ENABLE_CODE_MODS "Enable code mods" OFF)
|
||||
@@ -235,18 +236,6 @@ endif ()
|
||||
include(FetchContent)
|
||||
|
||||
# Declare all dependencies first so CMake can download them in parallel
|
||||
message(STATUS "dusklight: Fetching cxxopts")
|
||||
FetchContent_Declare(cxxopts
|
||||
URL https://github.com/jarro2783/cxxopts/archive/refs/tags/v3.3.1.tar.gz
|
||||
URL_HASH SHA256=3bfc70542c521d4b55a46429d808178916a579b28d048bd8c727ee76c39e2072
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
)
|
||||
message(STATUS "dusklight: Fetching nlohmann/json")
|
||||
FetchContent_Declare(json
|
||||
URL https://github.com/nlohmann/json/releases/download/v3.12.0/json.tar.xz
|
||||
URL_HASH SHA256=42f6e95cad6ec532fd372391373363b62a14af6d771056dbfc86160e6dfff7aa
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
)
|
||||
message(STATUS "dusklight: Fetching miniz")
|
||||
FetchContent_Declare(miniz
|
||||
URL https://github.com/richgel999/miniz/releases/download/3.0.2/miniz-3.0.2.zip
|
||||
@@ -254,7 +243,7 @@ FetchContent_Declare(miniz
|
||||
EXCLUDE_FROM_ALL
|
||||
)
|
||||
|
||||
set(_fetch_content_deps cxxopts json miniz)
|
||||
set(_fetch_content_deps miniz)
|
||||
if (DUSK_HAS_FUNCHOOK)
|
||||
message(STATUS "dusklight: Fetching funchook")
|
||||
# cmake/PatchFunchook.cmake patches funchook's cmake/capstone.cmake.in to inject a
|
||||
@@ -289,40 +278,12 @@ if (DUSK_HAS_FUNCHOOK)
|
||||
endif ()
|
||||
FetchContent_MakeAvailable(${_fetch_content_deps})
|
||||
|
||||
if (DUSK_ENABLE_SENTRY_NATIVE)
|
||||
message(STATUS "dusklight: Fetching sentry-native")
|
||||
set(SENTRY_BUILD_SHARED_LIBS OFF CACHE BOOL "" FORCE)
|
||||
set(SENTRY_BACKEND crashpad CACHE STRING "" FORCE)
|
||||
if (WIN32)
|
||||
set(SENTRY_TRANSPORT winhttp CACHE STRING "" FORCE)
|
||||
endif ()
|
||||
set(SENTRY_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
||||
set(SENTRY_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||
set(SENTRY_BUILD_BENCHMARKS OFF CACHE BOOL "" FORCE)
|
||||
FetchContent_Declare(sentry_native
|
||||
GIT_REPOSITORY https://github.com/getsentry/sentry-native.git
|
||||
GIT_TAG 0.13.6
|
||||
GIT_SHALLOW TRUE
|
||||
GIT_PROGRESS TRUE
|
||||
GIT_SUBMODULES_RECURSE TRUE
|
||||
)
|
||||
if (NOT sentry_native_POPULATED)
|
||||
FetchContent_Populate(sentry_native)
|
||||
set(_skip_install_rules ${CMAKE_SKIP_INSTALL_RULES})
|
||||
set(CMAKE_SKIP_INSTALL_RULES ON)
|
||||
add_subdirectory(${sentry_native_SOURCE_DIR} ${sentry_native_BINARY_DIR} EXCLUDE_FROM_ALL)
|
||||
set(CMAKE_SKIP_INSTALL_RULES ${_skip_install_rules})
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
# Use signed char on ARM to match the original game (and x86)
|
||||
string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" _arch)
|
||||
if(_arch MATCHES "^(arm|aarch64)" AND CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "GNU")
|
||||
add_compile_options(-fsigned-char)
|
||||
endif()
|
||||
|
||||
configure_version_header()
|
||||
|
||||
include(files.cmake)
|
||||
|
||||
# TODO: version handling for res includes
|
||||
@@ -335,67 +296,23 @@ set(DUSK_PRODUCT_NAME "Dusklight")
|
||||
set(DUSK_COPYRIGHT "Copyright (C) Twilit Realm contributors")
|
||||
|
||||
source_group("dolzel" FILES ${DOLZEL_FILES} ${Z2AUDIOLIB_FILES} ${REL_FILES})
|
||||
source_group("dusklight" FILES ${DUSK_FILES} ${DUSK_HTTP_BACKEND_FILES})
|
||||
source_group("dusklight" FILES ${DUSK_FILES})
|
||||
|
||||
include(cmake/GameABIConfig.cmake)
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
set(GAME_COMPILE_DEFS DUSK_BUILDING_GAME=1)
|
||||
set(GAME_LIBS aurora::core aurora::gx aurora::gd aurora::si aurora::vi aurora::pad aurora::mtx aurora::os aurora::dvd
|
||||
aurora::card freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt
|
||||
aurora::card borealis::cli borealis::crash borealis::data borealis::disc borealis::discord borealis::file_select borealis::log borealis::presentation borealis::sentry borealis::update freeverb cxxopts::cxxopts absl::flat_hash_map nlohmann_json::nlohmann_json TracyClient fmt::fmt
|
||||
Threads::Threads zstd::libzstd dusklight_game_headers)
|
||||
if (DUSK_HAS_FUNCHOOK)
|
||||
list(APPEND GAME_LIBS funchook-static)
|
||||
endif ()
|
||||
|
||||
if (DUSK_ENABLE_SENTRY_NATIVE)
|
||||
list(APPEND GAME_LIBS sentry)
|
||||
list(APPEND GAME_COMPILE_DEFS DUSK_ENABLE_SENTRY_NATIVE=1 SENTRY_BUILD_STATIC=1)
|
||||
endif ()
|
||||
|
||||
if (WIN32)
|
||||
list(APPEND GAME_LIBS Ws2_32)
|
||||
if (CMAKE_BUILD_TYPE STREQUAL Debug)
|
||||
list(APPEND GAME_LIBS dbghelp)
|
||||
list(APPEND GAME_COMPILE_DEFS DUSK_CRASH_DBGHELP=1)
|
||||
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 "dusklight: Enabled update checker (WinHTTP)")
|
||||
elseif (ANDROID)
|
||||
set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/android.cpp)
|
||||
list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_ANDROID=1)
|
||||
message(STATUS "dusklight: Enabled update checker (Android)")
|
||||
elseif (APPLE)
|
||||
find_library(FOUNDATION_FRAMEWORK Foundation REQUIRED)
|
||||
set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/url_session.mm)
|
||||
set_source_files_properties(src/dusk/http/url_session.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
|
||||
list(APPEND GAME_LIBS ${FOUNDATION_FRAMEWORK})
|
||||
list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_URLSESSION=1)
|
||||
message(STATUS "dusklight: Enabled update checker (NSURLSession)")
|
||||
elseif (CMAKE_SYSTEM_NAME STREQUAL Linux)
|
||||
find_package(CURL QUIET OPTIONAL_COMPONENTS HTTPS SSL)
|
||||
if (CURL_FOUND AND CURL_HTTPS_FOUND AND CURL_SSL_FOUND)
|
||||
set(DUSK_HTTP_BACKEND_SOURCE src/dusk/http/curl.cpp)
|
||||
list(APPEND GAME_LIBS CURL::libcurl)
|
||||
list(APPEND GAME_COMPILE_DEFS DUSK_HTTP_BACKEND_LIBCURL=1)
|
||||
message(STATUS "dusklight: Enabled update checker (libcurl)")
|
||||
else ()
|
||||
message(STATUS "dusklight: Disabled update checker (libcurl + HTTPS/SSL not found)")
|
||||
endif ()
|
||||
else ()
|
||||
message(STATUS "dusklight: Disabled update checker (unsupported platform)")
|
||||
endif ()
|
||||
endif ()
|
||||
list(APPEND DUSK_FILES ${DUSK_HTTP_BACKEND_SOURCE})
|
||||
|
||||
if (DUSK_MOVIE_SUPPORT)
|
||||
if (TARGET libjpeg-turbo::turbojpeg-static)
|
||||
list(APPEND GAME_LIBS libjpeg-turbo::turbojpeg-static)
|
||||
@@ -405,15 +322,6 @@ if (DUSK_MOVIE_SUPPORT)
|
||||
list(APPEND GAME_COMPILE_DEFS MOVIE_SUPPORT=1)
|
||||
endif ()
|
||||
|
||||
set(DUSK_ENABLE_DISCORD_DEFAULT ON)
|
||||
if (DEFINED DUSK_ENABLE_DISCORD_RPC AND NOT DEFINED DUSK_ENABLE_DISCORD)
|
||||
set(DUSK_ENABLE_DISCORD_DEFAULT ${DUSK_ENABLE_DISCORD_RPC})
|
||||
endif ()
|
||||
option(DUSK_ENABLE_DISCORD "Enable Discord Rich Presence support" ${DUSK_ENABLE_DISCORD_DEFAULT})
|
||||
if (DUSK_ENABLE_DISCORD AND NOT ANDROID AND NOT IOS AND NOT TVOS)
|
||||
list(APPEND GAME_COMPILE_DEFS DUSK_DISCORD=1)
|
||||
endif ()
|
||||
|
||||
if (DUSK_ENABLE_CODE_MODS)
|
||||
list(APPEND GAME_COMPILE_DEFS DUSK_CODE_MODS=1)
|
||||
endif ()
|
||||
@@ -478,10 +386,10 @@ endif ()
|
||||
set(DUSK_FILES src/dusk/main.cpp ${GAME_BASE_FILES} ${GAME_DEBUG_FILES} ${miniz_SOURCE_DIR}/miniz.c)
|
||||
if(ANDROID)
|
||||
add_library(dusklight SHARED ${DUSK_FILES})
|
||||
set_target_properties(dusklight PROPERTIES OUTPUT_NAME main)
|
||||
else ()
|
||||
add_executable(dusklight ${DUSK_FILES})
|
||||
endif ()
|
||||
borealis_configure_android_application(dusklight)
|
||||
if (ENABLE_ASAN)
|
||||
target_sources(dusklight PRIVATE src/dusk/asan_options.c)
|
||||
endif ()
|
||||
@@ -559,17 +467,9 @@ if (TARGET crashpad_handler)
|
||||
)
|
||||
endif ()
|
||||
|
||||
if (ANDROID)
|
||||
# SDLActivity loads SDL_main via dlsym on Android. Since aurora::main is a static
|
||||
# archive, force an undefined reference so the linker keeps the SDL_main object.
|
||||
target_link_options(dusklight PRIVATE "-Wl,-u,SDL_main")
|
||||
endif ()
|
||||
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
|
||||
target_link_options(dusklight PRIVATE "-Wl,--build-id=sha1")
|
||||
target_link_libraries(dusklight PRIVATE dl)
|
||||
elseif (ANDROID)
|
||||
target_link_options(dusklight PRIVATE "-Wl,--build-id=sha1")
|
||||
endif ()
|
||||
|
||||
if (NOT APPLE)
|
||||
@@ -652,8 +552,8 @@ if (APPLE)
|
||||
MACOSX_BUNDLE TRUE
|
||||
MACOSX_BUNDLE_BUNDLE_NAME ${DUSK_BUNDLE_NAME}
|
||||
MACOSX_BUNDLE_GUI_IDENTIFIER ${DUSK_BUNDLE_IDENTIFIER}
|
||||
MACOSX_BUNDLE_BUNDLE_VERSION ${DUSK_VERSION_STRING}
|
||||
MACOSX_BUNDLE_SHORT_VERSION_STRING ${DUSK_SHORT_VERSION_STRING}
|
||||
MACOSX_BUNDLE_BUNDLE_VERSION ${BOREALIS_APP_VERSION}
|
||||
MACOSX_BUNDLE_SHORT_VERSION_STRING ${BOREALIS_APP_SHORT_VERSION}
|
||||
MACOSX_BUNDLE_INFO_PLIST ${DUSK_INFO_PLIST}
|
||||
OUTPUT_NAME Dusklight
|
||||
XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED "YES"
|
||||
@@ -686,21 +586,6 @@ if (APPLE)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
find_library(APPKIT_FRAMEWORK AppKit REQUIRED)
|
||||
target_sources(dusklight PRIVATE src/dusk/file_select_macos.mm)
|
||||
set_source_files_properties(src/dusk/file_select_macos.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
|
||||
target_link_libraries(dusklight PRIVATE ${APPKIT_FRAMEWORK})
|
||||
endif ()
|
||||
|
||||
if (IOS)
|
||||
find_library(UIKIT_FRAMEWORK UIKit REQUIRED)
|
||||
find_library(UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK UniformTypeIdentifiers REQUIRED)
|
||||
target_sources(dusklight PRIVATE src/dusk/ios/FileSelectDialog.m)
|
||||
set_source_files_properties(src/dusk/ios/FileSelectDialog.m PROPERTIES COMPILE_FLAGS -fobjc-arc)
|
||||
target_link_libraries(dusklight PRIVATE ${UIKIT_FRAMEWORK} ${UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK})
|
||||
endif ()
|
||||
|
||||
include(extern/aurora/cmake/AuroraCopyRuntimeDLLs.cmake)
|
||||
aurora_copy_runtime_dlls(dusklight)
|
||||
|
||||
|
||||
+4
-4
@@ -28,12 +28,12 @@
|
||||
"cacheVariables": {
|
||||
"CMAKE_C_COMPILER_LAUNCHER": "sccache",
|
||||
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache",
|
||||
"DUSK_ENABLE_SENTRY_NATIVE": {
|
||||
"BOREALIS_ENABLE_SENTRY": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
},
|
||||
"DUSK_SENTRY_DSN": "$env{SENTRY_DSN}",
|
||||
"DUSK_SENTRY_ENVIRONMENT": "production",
|
||||
"BOREALIS_SENTRY_DSN": "$env{SENTRY_DSN}",
|
||||
"BOREALIS_SENTRY_ENVIRONMENT": "production",
|
||||
"Rust_RUSTUP_INSTALL_MISSING_TARGET": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
@@ -448,7 +448,7 @@
|
||||
"ci"
|
||||
],
|
||||
"cacheVariables": {
|
||||
"DUSK_ENABLE_SENTRY_NATIVE": {
|
||||
"BOREALIS_ENABLE_SENTRY": {
|
||||
"type": "BOOL",
|
||||
"value": false
|
||||
}
|
||||
|
||||
@@ -23,5 +23,5 @@ cp -r platforms/freedesktop/{16x16,32x32,48x48,64x64,128x128,256x256,512x512,102
|
||||
cp platforms/freedesktop/dev.twilitrealm.dusk.desktop build/appdir/usr/share/applications
|
||||
|
||||
cd build/install
|
||||
VERSION="$DUSK_VERSION" NO_STRIP=1 "$linuxdeploy" \
|
||||
VERSION="$APP_VERSION" NO_STRIP=1 "$linuxdeploy" \
|
||||
-l "$lib_dir/libusb-1.0.so" --appdir "$build_dir/appdir" --output appimage
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
# Version detection shared by the main build and the mod SDK (sdk/CMakeLists.txt)
|
||||
include_guard(GLOBAL)
|
||||
|
||||
get_filename_component(_DUSK_VERSION_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
||||
|
||||
set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN")
|
||||
set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment")
|
||||
|
||||
set(DUSK_VERSION_OVERRIDE "" CACHE STRING "Override version string (skips git detection and format validation)")
|
||||
|
||||
macro(detect_version)
|
||||
if (DUSK_VERSION_OVERRIDE)
|
||||
set(DUSK_WC_DESCRIBE "${DUSK_VERSION_OVERRIDE}")
|
||||
set(DUSK_VERSION_STRING "0.0.0.0")
|
||||
set(DUSK_SHORT_VERSION_STRING "0.0.0")
|
||||
set(DUSK_VERSION_CODE "1")
|
||||
set(DUSK_WC_REVISION "")
|
||||
set(DUSK_WC_BRANCH "")
|
||||
set(DUSK_WC_DATE "")
|
||||
message(STATUS "Dusklight version overridden to ${DUSK_WC_DESCRIBE}")
|
||||
else ()
|
||||
# obtain revision info from git
|
||||
find_package(Git)
|
||||
if (GIT_FOUND)
|
||||
# make sure version information gets re-run when the current Git HEAD changes
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD
|
||||
OUTPUT_VARIABLE dusk_git_head_filename
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
get_filename_component(dusk_git_head_filename "${dusk_git_head_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_filename}")
|
||||
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --symbolic-full-name HEAD
|
||||
OUTPUT_VARIABLE dusk_git_head_symbolic
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT}
|
||||
COMMAND ${GIT_EXECUTABLE} rev-parse --git-path ${dusk_git_head_symbolic}
|
||||
OUTPUT_VARIABLE dusk_git_head_symbolic_filename
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
get_filename_component(dusk_git_head_symbolic_filename "${dusk_git_head_symbolic_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_symbolic_filename}")
|
||||
|
||||
# defines DUSK_WC_REVISION
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
|
||||
OUTPUT_VARIABLE DUSK_WC_REVISION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
# defines DUSK_WC_DESCRIBE
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} describe --tags --long --dirty --match "v*"
|
||||
OUTPUT_VARIABLE DUSK_WC_DESCRIBE
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
|
||||
# remove the git hash, then collapse a clean "-0" suffix only
|
||||
string(REGEX REPLACE "-[^-]+(-dirty|)$" "\\1" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
|
||||
string(REGEX REPLACE "-0$" "" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
|
||||
|
||||
# defines DUSK_WC_BRANCH
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
|
||||
OUTPUT_VARIABLE DUSK_WC_BRANCH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
# defines DUSK_WC_DATE
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} log -1 --format=%ad
|
||||
OUTPUT_VARIABLE DUSK_WC_DATE
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
else ()
|
||||
message(STATUS "Unable to find git, commit information will not be available")
|
||||
endif ()
|
||||
|
||||
if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$")
|
||||
set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
|
||||
set(_ver_major ${CMAKE_MATCH_1})
|
||||
set(_ver_minor ${CMAKE_MATCH_2})
|
||||
set(_ver_patch ${CMAKE_MATCH_3})
|
||||
set(DUSK_VERSION_TWEAK "0")
|
||||
if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$")
|
||||
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
|
||||
elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$")
|
||||
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
|
||||
endif ()
|
||||
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}")
|
||||
if (DUSK_VERSION_TWEAK GREATER 999)
|
||||
set(_tweak 999)
|
||||
else ()
|
||||
set(_tweak ${DUSK_VERSION_TWEAK})
|
||||
endif ()
|
||||
# encoding: major*1e7 + minor*1e5 + patch*1e3 + tweak; collision-free for major<210, minor<100, patch<100, tweak<=999
|
||||
math(EXPR DUSK_VERSION_CODE
|
||||
"${_ver_major} * 10000000 + ${_ver_minor} * 100000 + ${_ver_patch} * 1000 + ${_tweak}")
|
||||
else ()
|
||||
set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION")
|
||||
set(DUSK_VERSION_STRING "0.0.0.0")
|
||||
set(DUSK_SHORT_VERSION_STRING "0.0.0")
|
||||
set(DUSK_VERSION_CODE "1")
|
||||
endif ()
|
||||
|
||||
endif ()
|
||||
|
||||
# Add version information to CI environment variables
|
||||
if (DEFINED ENV{GITHUB_ENV})
|
||||
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION=${DUSK_WC_DESCRIBE}\n")
|
||||
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION_CODE=${DUSK_VERSION_CODE}\n")
|
||||
endif ()
|
||||
message(STATUS "Dusklight version set to ${DUSK_WC_DESCRIBE}")
|
||||
endmacro()
|
||||
|
||||
# Sets PLATFORM_NAME and configures version.h into the caller's binary dir.
|
||||
macro(configure_version_header)
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL Windows)
|
||||
set(PLATFORM_NAME win32)
|
||||
elseif (CMAKE_SYSTEM_NAME STREQUAL Darwin)
|
||||
if (IOS)
|
||||
set(PLATFORM_NAME ios)
|
||||
elseif (TVOS)
|
||||
set(PLATFORM_NAME tvos)
|
||||
else ()
|
||||
set(PLATFORM_NAME macos)
|
||||
endif ()
|
||||
else ()
|
||||
string(TOLOWER CMAKE_SYSTEM_NAME PLATFORM_NAME)
|
||||
endif ()
|
||||
|
||||
configure_file(${_DUSK_VERSION_ROOT}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
|
||||
endmacro()
|
||||
Vendored
+1
-1
Submodule extern/aurora updated: 6c4c27f9e8...5027ed63a7
+1
Submodule extern/borealis added at eec74b6dec
+2
-18
@@ -1421,30 +1421,21 @@ set(DUSK_FILES
|
||||
src/dusk/achievements.cpp
|
||||
src/dusk/action_bindings.cpp
|
||||
src/dusk/action_bindings.h
|
||||
src/dusk/android_frame_rate.cpp
|
||||
src/dusk/android_frame_rate.hpp
|
||||
src/dusk/asserts.cpp
|
||||
src/dusk/autosave.cpp
|
||||
src/dusk/config.cpp
|
||||
src/dusk/config.hpp
|
||||
src/dusk/crash_handler.cpp
|
||||
src/dusk/crash_reporting.cpp
|
||||
src/dusk/data.cpp
|
||||
src/dusk/data.hpp
|
||||
src/dusk/discord.cpp
|
||||
src/dusk/discord.hpp
|
||||
src/dusk/discord_presence.cpp
|
||||
src/dusk/dvd_asset.cpp
|
||||
src/dusk/dvd_asset.hpp
|
||||
src/dusk/extras.c
|
||||
src/dusk/file_select.cpp
|
||||
src/dusk/file_select.hpp
|
||||
src/dusk/frame_interpolation.cpp
|
||||
src/dusk/game_clock.cpp
|
||||
src/dusk/gamepad_color.cpp
|
||||
src/dusk/globals.cpp
|
||||
src/dusk/gyro.cpp
|
||||
src/dusk/http/http.hpp
|
||||
src/dusk/imgui/ImGuiActorSpawner.cpp
|
||||
src/dusk/imgui/ImGuiBloomWindow.cpp
|
||||
src/dusk/imgui/ImGuiBloomWindow.hpp
|
||||
@@ -1507,6 +1498,8 @@ set(DUSK_FILES
|
||||
src/dusk/mods/svc/stage.cpp
|
||||
src/dusk/mods/svc/stage.hpp
|
||||
src/dusk/mouse.cpp
|
||||
src/dusk/presentation.cpp
|
||||
src/dusk/presentation.hpp
|
||||
src/dusk/scope_guard.hpp
|
||||
src/dusk/settings.cpp
|
||||
src/dusk/speedrun.cpp
|
||||
@@ -1582,8 +1575,6 @@ set(DUSK_FILES
|
||||
src/dusk/ui/warp.hpp
|
||||
src/dusk/ui/window.cpp
|
||||
src/dusk/ui/window.hpp
|
||||
src/dusk/update_check.cpp
|
||||
src/dusk/update_check.hpp
|
||||
src/dusk/version.cpp
|
||||
src/dusk/utilities.cpp
|
||||
src/helpers/batch.cpp
|
||||
@@ -1591,10 +1582,3 @@ set(DUSK_FILES
|
||||
src/helpers/offset_ptr.cpp
|
||||
src/helpers/string.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
|
||||
)
|
||||
|
||||
@@ -228,7 +228,7 @@
|
||||
ninjaFlags = [ "dusklight" ];
|
||||
|
||||
cmakeFlags = [
|
||||
"-DDUSK_VERSION_OVERRIDE=${versionSuffix}"
|
||||
"-DBOREALIS_APP_VERSION_OVERRIDE=${versionSuffix}"
|
||||
"-DFETCHCONTENT_FULLY_DISCONNECTED=ON"
|
||||
"-DAURORA_DAWN_PROVIDER=package"
|
||||
"-DAURORA_DAWN_LINKAGE=static"
|
||||
|
||||
+12
-14
@@ -1,6 +1,6 @@
|
||||
# Android Shell
|
||||
|
||||
This directory contains a minimal SDLActivity-based Android app wrapper for Dusklight.
|
||||
This directory contains Dusklight's Android shell built on top of Borealis.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -25,24 +25,19 @@ cmake --build --preset android-arm64
|
||||
|
||||
This build produces `build/android-arm64/libmain.so`
|
||||
|
||||
## Refresh SDL Java Shim (Optional)
|
||||
|
||||
If you update SDL and want to refresh the embedded Java shim files:
|
||||
|
||||
```bash
|
||||
./android/scripts/sync-sdl-java.sh
|
||||
```
|
||||
|
||||
## Build APK
|
||||
|
||||
```bash
|
||||
cd android
|
||||
cd platforms/android
|
||||
./gradlew :app:assembleDebug
|
||||
```
|
||||
|
||||
Output APK:
|
||||
|
||||
- `android/app/build/outputs/apk/debug/app-debug.apk`
|
||||
- `app/build/outputs/apk/debug/app-arm64-v8a-debug.apk`
|
||||
|
||||
Aurora needs a hardware-backed graphics adapter. If an AVD has GPU
|
||||
acceleration disabled, launch it with `-gpu host`.
|
||||
|
||||
## Launch With Runtime Args (adb)
|
||||
|
||||
@@ -50,10 +45,13 @@ You can pass command-line args through the activity intent:
|
||||
|
||||
```bash
|
||||
adb shell am start -n dev.twilitrealm.dusk/.DuskActivity \
|
||||
--es dusk_args "--backend vulkan"
|
||||
--es borealis_args "--backend vulkan"
|
||||
```
|
||||
|
||||
Supported extras:
|
||||
|
||||
- `dusk_args`: single shell-like argument string
|
||||
- `dusk_argv`: string-array argv
|
||||
- `borealis_args`: single shell-like argument string
|
||||
- `borealis_argv`: string-array argv
|
||||
|
||||
The legacy `dusk_args` and `dusk_argv` names remain accepted during the shell
|
||||
transition.
|
||||
|
||||
@@ -2,163 +2,32 @@ plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
def versionNameStr = (System.getenv("DUSK_VERSION") ?: "v0.1.0").replaceFirst("^v", "")
|
||||
def versionCodeInt = (System.getenv("DUSK_VERSION_CODE") ?: "100000").toInteger()
|
||||
|
||||
def duskRepoDir = rootProject.projectDir.parentFile.parentFile
|
||||
def androidNativeBuildDir = new File(duskRepoDir, 'build/android-arm64')
|
||||
def nativeLibrary = new File(androidNativeBuildDir, 'libmain.so')
|
||||
def stageStripValue = providers.gradleProperty('ANDROID_STAGE_STRIP')
|
||||
.orElse(providers.gradleProperty('androidStageStrip'))
|
||||
.orElse(providers.environmentVariable('ANDROID_STAGE_STRIP'))
|
||||
.orElse('1')
|
||||
def androidHome = {
|
||||
def sdkPath = System.getenv('ANDROID_HOME')
|
||||
if (!sdkPath) {
|
||||
throw new GradleException('ANDROID_HOME is not available')
|
||||
}
|
||||
def sdkDir = new File(sdkPath)
|
||||
if (!sdkDir.isDirectory()) {
|
||||
throw new GradleException("ANDROID_HOME points to a missing directory: ${sdkDir}")
|
||||
}
|
||||
sdkDir
|
||||
}.memoize()
|
||||
def borealisDir = new File(duskRepoDir, 'extern/borealis')
|
||||
|
||||
def androidNdkVersion = {
|
||||
def ndkVersion = System.getenv('ANDROID_NDK_VERSION')
|
||||
if (!ndkVersion) {
|
||||
throw new GradleException('ANDROID_NDK_VERSION is not available')
|
||||
}
|
||||
ndkVersion
|
||||
}.memoize()
|
||||
ext.borealisAndroid = [
|
||||
borealisDir: borealisDir,
|
||||
propertiesFile: new File(duskRepoDir, 'build/android-arm64/borealis-android.properties'),
|
||||
namespace: 'dev.twilitrealm.dusk',
|
||||
applicationId: 'dev.twilitrealm.dusk',
|
||||
abis: ['arm64-v8a'],
|
||||
assets: [
|
||||
[
|
||||
from: new File(duskRepoDir, 'res'),
|
||||
into: 'res',
|
||||
excludes: ['**/.DS_Store']
|
||||
],
|
||||
[
|
||||
from: new File(duskRepoDir, 'build/android-arm64/bundled_mods'),
|
||||
into: 'mods',
|
||||
includes: ['*.dusk']
|
||||
]
|
||||
],
|
||||
proguardRules: file('proguard-rules.pro')
|
||||
]
|
||||
|
||||
def androidNdkDir = {
|
||||
def ndkDir = new File(androidHome(), "ndk/${androidNdkVersion()}")
|
||||
if (!new File(ndkDir, 'build/cmake/android.toolchain.cmake').isFile()) {
|
||||
throw new GradleException(
|
||||
"Android NDK ${androidNdkVersion()} is missing or invalid at ${ndkDir}"
|
||||
)
|
||||
}
|
||||
ndkDir
|
||||
}.memoize()
|
||||
|
||||
def llvmPrebuiltDir = {
|
||||
def prebuiltRoot = new File(androidNdkDir(), 'toolchains/llvm/prebuilt')
|
||||
def prebuiltDir = (prebuiltRoot.listFiles()?.findAll { it.isDirectory() } ?: [])
|
||||
.sort { it.name }
|
||||
.find { candidate ->
|
||||
new File(
|
||||
candidate,
|
||||
'sysroot/usr/lib/aarch64-linux-android/libc++_shared.so'
|
||||
).isFile()
|
||||
}
|
||||
if (!prebuiltDir) {
|
||||
throw new GradleException("Cannot find NDK libc++_shared.so under ${prebuiltRoot}")
|
||||
}
|
||||
prebuiltDir
|
||||
}.memoize()
|
||||
|
||||
def stlLibrary = {
|
||||
def library = new File(
|
||||
llvmPrebuiltDir(),
|
||||
'sysroot/usr/lib/aarch64-linux-android/libc++_shared.so'
|
||||
)
|
||||
if (!library.isFile()) {
|
||||
throw new GradleException("libc++_shared.so is missing")
|
||||
}
|
||||
library
|
||||
}.memoize()
|
||||
|
||||
def stagedJniLibsDir = layout.buildDirectory.dir('generated/jniLibs/dusklight')
|
||||
|
||||
def stageJniLibs = tasks.register('stageJniLibs', Sync) {
|
||||
group = 'build'
|
||||
from(nativeLibrary) {
|
||||
rename { 'libmain.so' }
|
||||
into 'arm64-v8a'
|
||||
}
|
||||
from(providers.provider { stlLibrary() }) {
|
||||
rename { 'libc++_shared.so' }
|
||||
into 'arm64-v8a'
|
||||
}
|
||||
into(stagedJniLibsDir)
|
||||
|
||||
doFirst {
|
||||
if (!nativeLibrary.isFile()) {
|
||||
throw new GradleException("Native library is missing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def duskGeneratedAssetsDir = layout.buildDirectory.dir('generated/assets/dusklight')
|
||||
def syncDuskAssets = tasks.register('syncDuskAssets', Sync) {
|
||||
from(new File(duskRepoDir, 'res')) {
|
||||
into 'res'
|
||||
exclude '**/.DS_Store'
|
||||
}
|
||||
from(new File(androidNativeBuildDir, 'bundled_mods')) {
|
||||
into 'mods'
|
||||
include '*.dusk'
|
||||
}
|
||||
into(duskGeneratedAssetsDir)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'dev.twilitrealm.dusk'
|
||||
compileSdk 36
|
||||
ndkVersion androidNdkVersion()
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'dev.twilitrealm.dusk'
|
||||
minSdk 26
|
||||
targetSdk 36
|
||||
versionCode versionCodeInt
|
||||
versionName versionNameStr
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDirs = [stagedJniLibsDir]
|
||||
assets.srcDirs = [duskGeneratedAssetsDir]
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
if (stageStripValue.get() == '0') {
|
||||
keepDebugSymbols += '**/libmain.so'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include 'arm64-v8a'
|
||||
universalApk false
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
apply from: new File(borealisDir, 'platforms/android/gradle/borealis-application.gradle')
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
|
||||
tasks.named('preBuild').configure {
|
||||
dependsOn(stageJniLibs, syncDuskAssets)
|
||||
}
|
||||
|
||||
+1
-4
@@ -1,4 +1 @@
|
||||
# Keep SDL activity and related JNI bridge methods.
|
||||
-keep class org.libsdl.app.** { *; }
|
||||
-keep class dev.twilitrealm.dusk.DuskHttpClient { *; }
|
||||
-keep class dev.twilitrealm.dusk.DuskHttpClient$Response { *; }
|
||||
-keep class dev.twilitrealm.dusk.DuskActivity { *; }
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
android:resource="@xml/game_mode_config" />
|
||||
|
||||
<provider
|
||||
android:name="dev.twilitrealm.dusk.DuskDocumentsProvider"
|
||||
android:authorities="dev.twilitrealm.dusk.documents"
|
||||
android:name="dev.encounter.borealis.BorealisDocumentsProvider"
|
||||
android:authorities="${applicationId}.documents"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||
|
||||
@@ -1,102 +1,23 @@
|
||||
package dev.twilitrealm.dusk;
|
||||
|
||||
import android.app.ActionBar;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowInsetsController;
|
||||
|
||||
import org.libsdl.app.SDLActivity;
|
||||
import org.libsdl.app.SDLSurface;
|
||||
import dev.encounter.borealis.BorealisActivity;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DuskActivity extends SDLActivity {
|
||||
public class DuskActivity extends BorealisActivity {
|
||||
private static final String TAG = "DuskActivity";
|
||||
private static final float DEFAULT_SURFACE_FRAME_RATE = 60.0f;
|
||||
private static final int FOLDER_DIALOG_REQUEST_CODE = 0x4455;
|
||||
private static final int MANAGE_STORAGE_REQUEST_CODE = 0x4456;
|
||||
private static final String EXTERNAL_STORAGE_AUTHORITY =
|
||||
"com.android.externalstorage.documents";
|
||||
|
||||
private long folderDialogUserdata = 0;
|
||||
private boolean awaitingManageStoragePermission = false;
|
||||
|
||||
private static native void nativeFolderDialogResult(long userdata, String path, String error);
|
||||
|
||||
private static String[] splitArgs(String raw) {
|
||||
List<String> out = new ArrayList<>();
|
||||
StringBuilder current = new StringBuilder();
|
||||
boolean inSingle = false;
|
||||
boolean inDouble = false;
|
||||
boolean escaped = false;
|
||||
|
||||
for (int i = 0; i < raw.length(); ++i) {
|
||||
char c = raw.charAt(i);
|
||||
if (escaped) {
|
||||
current.append(c);
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (c == '\\' && !inSingle) {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (c == '"' && !inSingle) {
|
||||
inDouble = !inDouble;
|
||||
continue;
|
||||
}
|
||||
if (c == '\'' && !inDouble) {
|
||||
inSingle = !inSingle;
|
||||
continue;
|
||||
}
|
||||
if (!inSingle && !inDouble && Character.isWhitespace(c)) {
|
||||
if (current.length() > 0) {
|
||||
out.add(current.toString());
|
||||
current.setLength(0);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
current.append(c);
|
||||
}
|
||||
|
||||
if (escaped) {
|
||||
current.append('\\');
|
||||
}
|
||||
if (current.length() > 0) {
|
||||
out.add(current.toString());
|
||||
}
|
||||
return out.toArray(new String[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
extractBundledMods();
|
||||
super.onCreate(savedInstanceState);
|
||||
hideSystemBars();
|
||||
}
|
||||
|
||||
// Bundled mod packages ship as APK assets, which the native loader cannot read directly;
|
||||
@@ -143,459 +64,23 @@ public class DuskActivity extends SDLActivity {
|
||||
file.delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SDLSurface createSDLSurface(Context context) {
|
||||
return new DuskSurface(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
hideSystemBars();
|
||||
if (awaitingManageStoragePermission) {
|
||||
resumeFolderDialogAfterPermissionGrant();
|
||||
}
|
||||
}
|
||||
|
||||
@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) {
|
||||
window.setDecorFitsSystemWindows(false);
|
||||
WindowInsetsController ctrl = window.getDecorView().getWindowInsetsController();
|
||||
if (ctrl != null) {
|
||||
ctrl.setSystemBarsBehavior(
|
||||
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
ctrl.hide(WindowInsets.Type.systemBars());
|
||||
}
|
||||
} else {
|
||||
View decorView = window.getDecorView();
|
||||
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||
decorView.setSystemUiVisibility(uiOptions);
|
||||
ActionBar actionBar = getActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getLibraries() {
|
||||
// SDL3 is statically linked into libmain.so in this build.
|
||||
return new String[] {
|
||||
"main"
|
||||
};
|
||||
}
|
||||
|
||||
public void setPreferredSurfaceFrameRate(float frameRate) {
|
||||
runOnUiThread(() -> {
|
||||
if (mSurface instanceof DuskSurface) {
|
||||
((DuskSurface)mSurface).setPreferredFrameRate(frameRate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static final class DuskSurface extends SDLSurface {
|
||||
private float preferredFrameRate = DEFAULT_SURFACE_FRAME_RATE;
|
||||
|
||||
DuskSurface(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
super.surfaceChanged(holder, format, width, height);
|
||||
setTargetFrameRate(holder);
|
||||
}
|
||||
|
||||
void setPreferredFrameRate(float frameRate) {
|
||||
preferredFrameRate = frameRate;
|
||||
setTargetFrameRate(getHolder());
|
||||
}
|
||||
|
||||
private void setTargetFrameRate(SurfaceHolder holder) {
|
||||
if (!mIsSurfaceReady || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
return;
|
||||
}
|
||||
|
||||
Surface surface = holder != null ? holder.getSurface() : getHolder().getSurface();
|
||||
if (surface == null || !surface.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
float targetFrameRate = getMaxSupportedFrameRate();
|
||||
if (preferredFrameRate > 0.0f) {
|
||||
targetFrameRate = preferredFrameRate;
|
||||
}
|
||||
if (targetFrameRate <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
surface.setFrameRate(
|
||||
targetFrameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
|
||||
Log.v(TAG, "Requested surface frame rate " + targetFrameRate + " fps");
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(TAG, "Failed to request surface frame rate", e);
|
||||
}
|
||||
}
|
||||
|
||||
private float getMaxSupportedFrameRate() {
|
||||
if (mDisplay == null) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float maxFrameRate = mDisplay.getRefreshRate();
|
||||
Display.Mode[] modes = mDisplay.getSupportedModes();
|
||||
if (modes == null) {
|
||||
return maxFrameRate;
|
||||
}
|
||||
|
||||
for (Display.Mode mode : modes) {
|
||||
maxFrameRate = Math.max(maxFrameRate, mode.getRefreshRate());
|
||||
}
|
||||
return maxFrameRate;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getArguments() {
|
||||
String[] arguments = super.getArguments();
|
||||
if (arguments.length > 0) {
|
||||
return arguments;
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
if (intent != null) {
|
||||
String[] argv = intent.getStringArrayExtra("dusk_argv");
|
||||
if (argv != null && argv.length > 0) {
|
||||
return argv;
|
||||
}
|
||||
|
||||
String rawArgs = intent.getStringExtra("dusk_args");
|
||||
if (rawArgs != null) {
|
||||
String trimmed = rawArgs.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
return splitArgs(trimmed);
|
||||
}
|
||||
}
|
||||
if (intent == null) {
|
||||
return arguments;
|
||||
}
|
||||
return new String[0];
|
||||
String[] argv = intent.getStringArrayExtra("dusk_argv");
|
||||
if (argv != null && argv.length > 0) {
|
||||
return argv;
|
||||
}
|
||||
String rawArgs = intent.getStringExtra("dusk_args");
|
||||
return rawArgs == null ? arguments : splitArguments(rawArgs.trim());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
persistUriPermissions(data);
|
||||
}
|
||||
if (requestCode == FOLDER_DIALOG_REQUEST_CODE) {
|
||||
finishFolderDialog(resultCode, data);
|
||||
return;
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
public boolean showFolderDialog(long userdata) {
|
||||
if (userdata == 0 || folderDialogUserdata != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
folderDialogUserdata = userdata;
|
||||
if (requiresManageStoragePermission() && !hasManageStoragePermission()) {
|
||||
if (!requestManageStoragePermission()) {
|
||||
finishFolderDialogWithError("Unable to request Android file access permission");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
openFolderDialog();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void openFolderDialog() {
|
||||
runOnUiThread(() -> {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
|
||||
|
||||
try {
|
||||
startActivityForResult(intent, FOLDER_DIALOG_REQUEST_CODE);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w(TAG, "Unable to open folder dialog.", e);
|
||||
finishFolderDialog(Activity.RESULT_CANCELED, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean requiresManageStoragePermission() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;
|
||||
}
|
||||
|
||||
private boolean hasManageStoragePermission() {
|
||||
return !requiresManageStoragePermission() || Environment.isExternalStorageManager();
|
||||
}
|
||||
|
||||
private boolean requestManageStoragePermission() {
|
||||
if (!requiresManageStoragePermission()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
awaitingManageStoragePermission = true;
|
||||
runOnUiThread(() -> {
|
||||
if (tryStartManageStorageIntent(
|
||||
new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||
.setData(Uri.parse("package:" + getPackageName()))) ||
|
||||
tryStartManageStorageIntent(
|
||||
new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
finishFolderDialogWithError("Unable to request Android file access permission");
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean tryStartManageStorageIntent(Intent intent) {
|
||||
try {
|
||||
startActivityForResult(intent, MANAGE_STORAGE_REQUEST_CODE);
|
||||
return true;
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w(TAG, "Unable to open all-files access settings.", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void resumeFolderDialogAfterPermissionGrant() {
|
||||
awaitingManageStoragePermission = false;
|
||||
if (folderDialogUserdata == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasManageStoragePermission()) {
|
||||
openFolderDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
finishFolderDialogWithError(
|
||||
"Allow \"All files access\" for Dusklight before choosing a custom data folder");
|
||||
}
|
||||
|
||||
private void finishFolderDialogWithError(String error) {
|
||||
long userdata = folderDialogUserdata;
|
||||
folderDialogUserdata = 0;
|
||||
awaitingManageStoragePermission = false;
|
||||
if (userdata != 0) {
|
||||
nativeFolderDialogResult(userdata, null, error);
|
||||
}
|
||||
}
|
||||
|
||||
private void finishFolderDialog(int resultCode, Intent data) {
|
||||
long userdata = folderDialogUserdata;
|
||||
folderDialogUserdata = 0;
|
||||
if (userdata == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resultCode == RESULT_OK && data != null && data.getData() != null) {
|
||||
String path = getRealPathForUri(data.getData());
|
||||
if (path != null && !path.isEmpty()) {
|
||||
nativeFolderDialogResult(userdata, path, null);
|
||||
} else {
|
||||
nativeFolderDialogResult(
|
||||
userdata, null, "Selected folder is not available as a filesystem path");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
nativeFolderDialogResult(userdata, null, null);
|
||||
}
|
||||
|
||||
private String getRealPathForUri(Uri uri) {
|
||||
if (uri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String scheme = uri.getScheme();
|
||||
if ("file".equals(scheme)) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
if (!"content".equals(scheme) ||
|
||||
!EXTERNAL_STORAGE_AUTHORITY.equals(uri.getAuthority()) ||
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return getExternalStoragePathForDocumentId(getExternalStorageDocumentId(uri));
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.w(TAG, "Unable to resolve URI: " + uri, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getExternalStorageDocumentId(Uri uri) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && isTreeDocumentUri(uri)) {
|
||||
return DocumentsContract.getTreeDocumentId(uri);
|
||||
}
|
||||
|
||||
return DocumentsContract.getDocumentId(uri);
|
||||
}
|
||||
|
||||
private static boolean isTreeDocumentUri(Uri uri) {
|
||||
List<String> segments = uri.getPathSegments();
|
||||
return segments.size() >= 2 && "tree".equals(segments.get(0));
|
||||
}
|
||||
|
||||
private String getExternalStoragePathForDocumentId(String documentId) {
|
||||
if (documentId == null || documentId.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (documentId.startsWith("raw:")) {
|
||||
return documentId.substring("raw:".length());
|
||||
}
|
||||
|
||||
String[] parts = documentId.split(":", 2);
|
||||
String volumeId = parts[0];
|
||||
String relativePath = parts.length > 1 ? parts[1] : "";
|
||||
|
||||
File root = getExternalStorageRoot(volumeId);
|
||||
if (root == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return relativePath.isEmpty()
|
||||
? root.getAbsolutePath()
|
||||
: new File(root, relativePath).getAbsolutePath();
|
||||
}
|
||||
|
||||
private File getExternalStorageRoot(String volumeId) {
|
||||
if ("primary".equalsIgnoreCase(volumeId)) {
|
||||
return Environment.getExternalStorageDirectory();
|
||||
}
|
||||
if ("home".equalsIgnoreCase(volumeId)) {
|
||||
return new File(
|
||||
Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOCUMENTS);
|
||||
}
|
||||
|
||||
File[] externalFilesDirs = getExternalFilesDirs(null);
|
||||
if (externalFilesDirs != null) {
|
||||
for (File externalFilesDir : externalFilesDirs) {
|
||||
File root = getStorageRootForExternalFilesDir(externalFilesDir);
|
||||
if (root != null && volumeId.equalsIgnoreCase(root.getName())) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File fallback = new File("/storage", volumeId);
|
||||
return fallback.exists() ? fallback : null;
|
||||
}
|
||||
|
||||
private File getStorageRootForExternalFilesDir(File externalFilesDir) {
|
||||
if (externalFilesDir == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String path = externalFilesDir.getAbsolutePath();
|
||||
int androidDir = path.indexOf("/Android/");
|
||||
if (androidDir <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new File(path.substring(0, androidDir));
|
||||
}
|
||||
|
||||
private void persistUriPermissions(Intent data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 : "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,467 +0,0 @@
|
||||
package dev.twilitrealm.dusk;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
import android.provider.DocumentsContract.Root;
|
||||
import android.provider.DocumentsProvider;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class DuskDocumentsProvider extends DocumentsProvider {
|
||||
public static final String AUTHORITY = "dev.twilitrealm.dusk.documents";
|
||||
|
||||
private static final String ROOT_ID = "dusk";
|
||||
private static final String ROOT_DOCUMENT_ID = "root";
|
||||
private static final String LOCATION_DESCRIPTOR_NAME = "data_location.json";
|
||||
private static final String DIRECTORY_MIME_TYPE = Document.MIME_TYPE_DIR;
|
||||
|
||||
private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
|
||||
Root.COLUMN_ROOT_ID,
|
||||
Root.COLUMN_FLAGS,
|
||||
Root.COLUMN_TITLE,
|
||||
Root.COLUMN_DOCUMENT_ID,
|
||||
Root.COLUMN_ICON,
|
||||
Root.COLUMN_AVAILABLE_BYTES,
|
||||
Root.COLUMN_SUMMARY
|
||||
};
|
||||
|
||||
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
|
||||
Document.COLUMN_DOCUMENT_ID,
|
||||
Document.COLUMN_DISPLAY_NAME,
|
||||
Document.COLUMN_FLAGS,
|
||||
Document.COLUMN_MIME_TYPE,
|
||||
Document.COLUMN_LAST_MODIFIED,
|
||||
Document.COLUMN_SIZE
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
if (!isCustomDataPathEnabled()) {
|
||||
ensureUserDirectories();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
|
||||
final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
|
||||
if (isCustomDataPathEnabled()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
final File root = getRootDirectory();
|
||||
final MatrixCursor.RowBuilder row = result.newRow();
|
||||
|
||||
row.add(Root.COLUMN_ROOT_ID, ROOT_ID);
|
||||
row.add(Root.COLUMN_FLAGS,
|
||||
Root.FLAG_LOCAL_ONLY |
|
||||
Root.FLAG_SUPPORTS_CREATE |
|
||||
Root.FLAG_SUPPORTS_IS_CHILD);
|
||||
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.app_name));
|
||||
row.add(Root.COLUMN_DOCUMENT_ID, ROOT_DOCUMENT_ID);
|
||||
row.add(Root.COLUMN_ICON, R.mipmap.icon);
|
||||
row.add(Root.COLUMN_AVAILABLE_BYTES, root.getFreeSpace());
|
||||
row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.documents_provider_summary));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
|
||||
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
|
||||
includeDocument(result, documentId, getFileForDocumentId(documentId));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
|
||||
throws FileNotFoundException
|
||||
{
|
||||
return queryChildDocumentsInternal(parentDocumentId, projection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, Bundle queryArgs)
|
||||
throws FileNotFoundException
|
||||
{
|
||||
return queryChildDocumentsInternal(parentDocumentId, projection);
|
||||
}
|
||||
|
||||
private Cursor queryChildDocumentsInternal(String parentDocumentId, String[] projection)
|
||||
throws FileNotFoundException
|
||||
{
|
||||
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
|
||||
final File parent = getFileForDocumentId(parentDocumentId);
|
||||
final File[] files = parent.listFiles();
|
||||
result.setNotificationUri(getContext().getContentResolver(), getChildDocumentsUri(parentDocumentId));
|
||||
|
||||
if (files == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (File file : files) {
|
||||
includeDocument(result, getDocumentIdForFile(file), file);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChildDocument(String parentDocumentId, String documentId) {
|
||||
try {
|
||||
final File parent = getFileForDocumentId(parentDocumentId);
|
||||
final File child = getFileForDocumentId(documentId);
|
||||
return isInside(parent, child);
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createDocument(String parentDocumentId, String mimeType, String displayName)
|
||||
throws FileNotFoundException
|
||||
{
|
||||
final File parent = getFileForDocumentId(parentDocumentId);
|
||||
if (!parent.isDirectory()) {
|
||||
throw new FileNotFoundException("Parent is not a directory: " + parentDocumentId);
|
||||
}
|
||||
|
||||
final String safeDisplayName = sanitizeDisplayName(displayName);
|
||||
final File file = buildUniqueFile(parent, safeDisplayName);
|
||||
final boolean created;
|
||||
if (DIRECTORY_MIME_TYPE.equals(mimeType)) {
|
||||
created = file.mkdir();
|
||||
} else {
|
||||
try {
|
||||
created = file.createNewFile();
|
||||
} catch (IOException e) {
|
||||
throw asFileNotFound("Unable to create document", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!created) {
|
||||
throw new FileNotFoundException("Unable to create document: " + displayName);
|
||||
}
|
||||
|
||||
notifyChildrenChanged(parentDocumentId);
|
||||
return getDocumentIdForFile(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String renameDocument(String documentId, String displayName) throws FileNotFoundException {
|
||||
final File file = getFileForDocumentId(documentId);
|
||||
if (ROOT_DOCUMENT_ID.equals(documentId)) {
|
||||
throw new FileNotFoundException("Cannot rename root document");
|
||||
}
|
||||
|
||||
final File target = buildUniqueFile(file.getParentFile(), sanitizeDisplayName(displayName));
|
||||
final String parentDocumentId = getDocumentIdForFile(file.getParentFile());
|
||||
if (!file.renameTo(target)) {
|
||||
throw new FileNotFoundException("Unable to rename document: " + documentId);
|
||||
}
|
||||
notifyDocumentChanged(documentId);
|
||||
notifyDocumentChanged(getDocumentIdForFile(target));
|
||||
notifyChildrenChanged(parentDocumentId);
|
||||
return getDocumentIdForFile(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteDocument(String documentId) throws FileNotFoundException {
|
||||
if (ROOT_DOCUMENT_ID.equals(documentId)) {
|
||||
throw new FileNotFoundException("Cannot delete root document");
|
||||
}
|
||||
|
||||
final File file = getFileForDocumentId(documentId);
|
||||
final String parentDocumentId = getDocumentIdForFile(file.getParentFile());
|
||||
deleteRecursively(file);
|
||||
notifyDocumentChanged(documentId);
|
||||
notifyChildrenChanged(parentDocumentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal)
|
||||
throws FileNotFoundException
|
||||
{
|
||||
return ParcelFileDescriptor.open(getFileForDocumentId(documentId), modeToParcelMode(mode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AssetFileDescriptor openDocumentThumbnail(String documentId, android.graphics.Point sizeHint,
|
||||
CancellationSignal signal) throws FileNotFoundException
|
||||
{
|
||||
throw new FileNotFoundException("Thumbnails are not supported");
|
||||
}
|
||||
|
||||
private void includeDocument(MatrixCursor result, String documentId, File file) throws FileNotFoundException {
|
||||
final MatrixCursor.RowBuilder row = result.newRow();
|
||||
final boolean isDirectory = file.isDirectory();
|
||||
final String displayName = ROOT_DOCUMENT_ID.equals(documentId)
|
||||
? getContext().getString(R.string.documents_provider_root_name)
|
||||
: file.getName();
|
||||
|
||||
int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME;
|
||||
if (isDirectory) {
|
||||
flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
|
||||
} else if (file.canWrite()) {
|
||||
flags |= Document.FLAG_SUPPORTS_WRITE;
|
||||
}
|
||||
if (ROOT_DOCUMENT_ID.equals(documentId)) {
|
||||
flags &= ~(Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME);
|
||||
}
|
||||
|
||||
row.add(Document.COLUMN_DOCUMENT_ID, documentId);
|
||||
row.add(Document.COLUMN_DISPLAY_NAME, displayName);
|
||||
row.add(Document.COLUMN_FLAGS, flags);
|
||||
row.add(Document.COLUMN_MIME_TYPE, isDirectory ? DIRECTORY_MIME_TYPE : getMimeType(file));
|
||||
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
|
||||
row.add(Document.COLUMN_SIZE, isDirectory ? null : file.length());
|
||||
}
|
||||
|
||||
private File getRootDirectory() throws FileNotFoundException {
|
||||
if (isCustomDataPathEnabled()) {
|
||||
throw new FileNotFoundException(
|
||||
"Dusk DocumentsProvider is disabled while a custom data path is configured");
|
||||
}
|
||||
|
||||
final File root = getContext().getFilesDir();
|
||||
if (root == null) {
|
||||
throw new FileNotFoundException("Dusklight files directory is unavailable");
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
private File getFileForDocumentId(String documentId) throws FileNotFoundException {
|
||||
final File root = getRootDirectory();
|
||||
if (ROOT_DOCUMENT_ID.equals(documentId)) {
|
||||
return root;
|
||||
}
|
||||
if (!documentId.startsWith(ROOT_DOCUMENT_ID + "/")) {
|
||||
throw new FileNotFoundException("Invalid document id: " + documentId);
|
||||
}
|
||||
|
||||
final String relativePath = documentId.substring(ROOT_DOCUMENT_ID.length() + 1);
|
||||
final File file = new File(root, relativePath);
|
||||
if (!isInside(root, file)) {
|
||||
throw new FileNotFoundException("Document escapes Dusklight files directory: " + documentId);
|
||||
}
|
||||
if (!file.exists()) {
|
||||
throw new FileNotFoundException("Document does not exist: " + documentId);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private String getDocumentIdForFile(File file) throws FileNotFoundException {
|
||||
final File root = getRootDirectory();
|
||||
if (sameFile(root, file)) {
|
||||
return ROOT_DOCUMENT_ID;
|
||||
}
|
||||
if (!isInside(root, file)) {
|
||||
throw new FileNotFoundException("File escapes Dusklight files directory: " + file);
|
||||
}
|
||||
|
||||
final String rootPath = canonicalPath(root);
|
||||
final String filePath = canonicalPath(file);
|
||||
return ROOT_DOCUMENT_ID + "/" + filePath.substring(rootPath.length() + 1);
|
||||
}
|
||||
|
||||
private void ensureUserDirectories() {
|
||||
final File root = getContext().getFilesDir();
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
new File(root, "texture_replacements").mkdirs();
|
||||
new File(root, "USA/Card A").mkdirs();
|
||||
new File(root, "EUR/Card A").mkdirs();
|
||||
}
|
||||
|
||||
private boolean isCustomDataPathEnabled() {
|
||||
if (getContext() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final File filesDir = getContext().getFilesDir();
|
||||
if (filesDir == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final File descriptor = new File(filesDir, LOCATION_DESCRIPTOR_NAME);
|
||||
if (!descriptor.isFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final JSONObject json = new JSONObject(readText(descriptor));
|
||||
return "custom".equals(json.optString("mode", "default"));
|
||||
} catch (IOException | JSONException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String readText(File file) throws IOException {
|
||||
try (FileInputStream input = new FileInputStream(file);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream())
|
||||
{
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = input.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, bytesRead);
|
||||
}
|
||||
return output.toString(StandardCharsets.UTF_8.name());
|
||||
}
|
||||
}
|
||||
|
||||
private static String[] resolveRootProjection(String[] projection) {
|
||||
return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
|
||||
}
|
||||
|
||||
private static String[] resolveDocumentProjection(String[] projection) {
|
||||
return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
|
||||
}
|
||||
|
||||
private static String sanitizeDisplayName(String displayName) throws FileNotFoundException {
|
||||
if (displayName == null) {
|
||||
throw new FileNotFoundException("Document name is empty");
|
||||
}
|
||||
|
||||
final String sanitized = displayName.trim();
|
||||
if (sanitized.isEmpty() || ".".equals(sanitized) || "..".equals(sanitized) ||
|
||||
sanitized.contains("/") || sanitized.contains("\\"))
|
||||
{
|
||||
throw new FileNotFoundException("Invalid document name: " + displayName);
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private static File buildUniqueFile(File parent, String displayName) {
|
||||
File file = new File(parent, displayName);
|
||||
if (!file.exists()) {
|
||||
return file;
|
||||
}
|
||||
|
||||
final int dot = displayName.lastIndexOf('.');
|
||||
final String baseName = dot > 0 ? displayName.substring(0, dot) : displayName;
|
||||
final String extension = dot > 0 ? displayName.substring(dot) : "";
|
||||
for (int i = 1; i < 100; ++i) {
|
||||
file = new File(parent, baseName + " (" + i + ")" + extension);
|
||||
if (!file.exists()) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return new File(parent, baseName + " (" + System.currentTimeMillis() + ")" + extension);
|
||||
}
|
||||
|
||||
private static int modeToParcelMode(String mode) {
|
||||
if ("r".equals(mode)) {
|
||||
return ParcelFileDescriptor.MODE_READ_ONLY;
|
||||
}
|
||||
if ("w".equals(mode) || "wt".equals(mode)) {
|
||||
return ParcelFileDescriptor.MODE_WRITE_ONLY |
|
||||
ParcelFileDescriptor.MODE_CREATE |
|
||||
ParcelFileDescriptor.MODE_TRUNCATE;
|
||||
}
|
||||
if ("wa".equals(mode)) {
|
||||
return ParcelFileDescriptor.MODE_WRITE_ONLY |
|
||||
ParcelFileDescriptor.MODE_CREATE |
|
||||
ParcelFileDescriptor.MODE_APPEND;
|
||||
}
|
||||
if ("rw".equals(mode)) {
|
||||
return ParcelFileDescriptor.MODE_READ_WRITE |
|
||||
ParcelFileDescriptor.MODE_CREATE;
|
||||
}
|
||||
if ("rwt".equals(mode)) {
|
||||
return ParcelFileDescriptor.MODE_READ_WRITE |
|
||||
ParcelFileDescriptor.MODE_CREATE |
|
||||
ParcelFileDescriptor.MODE_TRUNCATE;
|
||||
}
|
||||
return ParcelFileDescriptor.MODE_READ_ONLY;
|
||||
}
|
||||
|
||||
private static String getMimeType(File file) {
|
||||
final int dot = file.getName().lastIndexOf('.');
|
||||
if (dot >= 0) {
|
||||
final String extension = file.getName().substring(dot + 1).toLowerCase();
|
||||
final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
if (mimeType != null) {
|
||||
return mimeType;
|
||||
}
|
||||
}
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
private Uri getChildDocumentsUri(String parentDocumentId) {
|
||||
return DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId);
|
||||
}
|
||||
|
||||
private void notifyChildrenChanged(String parentDocumentId) {
|
||||
final ContentResolver resolver = getContext().getContentResolver();
|
||||
resolver.notifyChange(getChildDocumentsUri(parentDocumentId), null, false);
|
||||
}
|
||||
|
||||
private void notifyDocumentChanged(String documentId) {
|
||||
final ContentResolver resolver = getContext().getContentResolver();
|
||||
resolver.notifyChange(DocumentsContract.buildDocumentUri(AUTHORITY, documentId), null, false);
|
||||
}
|
||||
|
||||
private static void deleteRecursively(File file) throws FileNotFoundException {
|
||||
if (file.isDirectory()) {
|
||||
final File[] children = file.listFiles();
|
||||
if (children != null) {
|
||||
for (File child : children) {
|
||||
deleteRecursively(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!file.delete()) {
|
||||
throw new FileNotFoundException("Unable to delete document: " + file);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isInside(File parent, File child) {
|
||||
try {
|
||||
final String parentPath = canonicalPath(parent);
|
||||
final String childPath = canonicalPath(child);
|
||||
return childPath.equals(parentPath) || childPath.startsWith(parentPath + File.separator);
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean sameFile(File a, File b) {
|
||||
try {
|
||||
return canonicalPath(a).equals(canonicalPath(b));
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String canonicalPath(File file) throws FileNotFoundException {
|
||||
try {
|
||||
return file.getCanonicalPath();
|
||||
} catch (IOException e) {
|
||||
throw asFileNotFound("Unable to resolve path", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static FileNotFoundException asFileNotFound(String message, IOException cause) {
|
||||
final FileNotFoundException exception = new FileNotFoundException(message + ": " + cause.getMessage());
|
||||
exception.initCause(cause);
|
||||
return exception;
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.hardware.usb.UsbDevice;
|
||||
|
||||
interface HIDDevice
|
||||
{
|
||||
public int getId();
|
||||
public int getVendorId();
|
||||
public int getProductId();
|
||||
public String getSerialNumber();
|
||||
public int getVersion();
|
||||
public String getManufacturerName();
|
||||
public String getProductName();
|
||||
public UsbDevice getDevice();
|
||||
public boolean open();
|
||||
public int writeReport(byte[] report, boolean feature);
|
||||
public boolean readReport(byte[] report, boolean feature);
|
||||
public void setFrozen(boolean frozen);
|
||||
public void close();
|
||||
public void shutdown();
|
||||
}
|
||||
@@ -1,829 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCallback;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothGattDescriptor;
|
||||
import android.bluetooth.BluetoothManager;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
import android.bluetooth.BluetoothGattService;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.os.*;
|
||||
|
||||
//import com.android.internal.util.HexDump;
|
||||
|
||||
import java.lang.Runnable;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.UUID;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice {
|
||||
|
||||
private static final String TAG = "hidapi";
|
||||
private HIDDeviceManager mManager;
|
||||
private BluetoothDevice mDevice;
|
||||
private int mDeviceId;
|
||||
private BluetoothGatt mGatt;
|
||||
private boolean mIsRegistered = false;
|
||||
private boolean mIsConnected = false;
|
||||
private boolean mIsChromebook = false;
|
||||
private boolean mIsReconnecting = false;
|
||||
private boolean mHasEnabledNotifications = false;
|
||||
private boolean mHasSeenInputUpdate = false;
|
||||
private boolean mFrozen = false;
|
||||
private LinkedList<GattOperation> mOperations;
|
||||
GattOperation mCurrentOperation = null;
|
||||
private Handler mHandler;
|
||||
private int mProductId = -1;
|
||||
private int mReportId = 0;
|
||||
private UUID mInputCharacteristic;
|
||||
|
||||
private static final int D0G_BLE2_PID = 0x1106;
|
||||
private static final int TRITON_BLE_PID = 0x1303;
|
||||
|
||||
|
||||
private static final int TRANSPORT_AUTO = 0;
|
||||
private static final int TRANSPORT_BREDR = 1;
|
||||
private static final int TRANSPORT_LE = 2;
|
||||
|
||||
private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000;
|
||||
|
||||
static final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3");
|
||||
static final UUID inputCharacteristicD0G = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3");
|
||||
static final UUID inputCharacteristicTriton_0x45 = UUID.fromString("100F6C7A-1735-4313-B402-38567131E5F3");
|
||||
static final UUID inputCharacteristicTriton_0x47 = UUID.fromString("100F6C7C-1735-4313-B402-38567131E5F3");
|
||||
static final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3");
|
||||
static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 };
|
||||
|
||||
private HashMap<Integer, BluetoothGattCharacteristic> mOutputReportChars = new HashMap<Integer, BluetoothGattCharacteristic>();
|
||||
|
||||
static class GattOperation {
|
||||
private enum Operation {
|
||||
CHR_READ,
|
||||
CHR_WRITE,
|
||||
ENABLE_NOTIFICATION
|
||||
}
|
||||
|
||||
Operation mOp;
|
||||
UUID mUuid;
|
||||
byte[] mValue;
|
||||
BluetoothGatt mGatt;
|
||||
boolean mResult = true;
|
||||
int mDelayMs = 0;
|
||||
|
||||
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
|
||||
mGatt = gatt;
|
||||
mOp = operation;
|
||||
mUuid = uuid;
|
||||
}
|
||||
|
||||
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, int delayMs) {
|
||||
mGatt = gatt;
|
||||
mOp = operation;
|
||||
mUuid = uuid;
|
||||
mDelayMs = delayMs;
|
||||
}
|
||||
|
||||
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
|
||||
mGatt = gatt;
|
||||
mOp = operation;
|
||||
mUuid = uuid;
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value, int delayMs) {
|
||||
mGatt = gatt;
|
||||
mOp = operation;
|
||||
mUuid = uuid;
|
||||
mValue = value;
|
||||
mDelayMs = delayMs;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
// This is executed in main thread
|
||||
BluetoothGattCharacteristic chr;
|
||||
|
||||
switch (mOp) {
|
||||
case CHR_READ:
|
||||
chr = getCharacteristic(mUuid);
|
||||
//Log.v(TAG, "Reading characteristic " + chr.getUuid());
|
||||
if (!mGatt.readCharacteristic(chr)) {
|
||||
Log.e(TAG, "Unable to read characteristic " + mUuid.toString());
|
||||
mResult = false;
|
||||
break;
|
||||
}
|
||||
mResult = true;
|
||||
break;
|
||||
case CHR_WRITE:
|
||||
chr = getCharacteristic(mUuid);
|
||||
//Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
|
||||
chr.setValue(mValue);
|
||||
if (!mGatt.writeCharacteristic(chr)) {
|
||||
Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
|
||||
mResult = false;
|
||||
break;
|
||||
}
|
||||
mResult = true;
|
||||
break;
|
||||
case ENABLE_NOTIFICATION:
|
||||
chr = getCharacteristic(mUuid);
|
||||
//Log.v(TAG, "Writing descriptor of " + chr.getUuid());
|
||||
if (chr != null) {
|
||||
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
|
||||
if (cccd != null) {
|
||||
int properties = chr.getProperties();
|
||||
byte[] value;
|
||||
if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
|
||||
value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
|
||||
} else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) {
|
||||
value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
|
||||
} else {
|
||||
Log.e(TAG, "Unable to start notifications on input characteristic");
|
||||
mResult = false;
|
||||
return;
|
||||
}
|
||||
|
||||
mGatt.setCharacteristicNotification(chr, true);
|
||||
cccd.setValue(value);
|
||||
if (!mGatt.writeDescriptor(cccd)) {
|
||||
Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
|
||||
mResult = false;
|
||||
return;
|
||||
}
|
||||
mResult = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean finish() {
|
||||
return mResult;
|
||||
}
|
||||
|
||||
public int getDelayMs() { return mDelayMs; }
|
||||
|
||||
private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
|
||||
BluetoothGattService valveService = mGatt.getService(steamControllerService);
|
||||
if (valveService == null)
|
||||
return null;
|
||||
return valveService.getCharacteristic(uuid);
|
||||
}
|
||||
|
||||
static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) {
|
||||
return new GattOperation(gatt, Operation.CHR_READ, uuid);
|
||||
}
|
||||
|
||||
static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) {
|
||||
return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value);
|
||||
}
|
||||
|
||||
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
|
||||
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid);
|
||||
}
|
||||
|
||||
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid, int delayMs) {
|
||||
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid, delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) {
|
||||
mManager = manager;
|
||||
mDevice = device;
|
||||
mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier());
|
||||
mIsRegistered = false;
|
||||
mIsChromebook = SDLActivity.isChromebook();
|
||||
mOperations = new LinkedList<GattOperation>();
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
mGatt = connectGatt();
|
||||
mHasEnabledNotifications = false;
|
||||
mHasSeenInputUpdate = false;
|
||||
// final HIDDeviceBLESteamController finalThis = this;
|
||||
// mHandler.postDelayed(new Runnable() {
|
||||
// @Override
|
||||
// void run() {
|
||||
// finalThis.checkConnectionForChromebookIssue();
|
||||
// }
|
||||
// }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
String getIdentifier() {
|
||||
return String.format("SteamController.%s", mDevice.getAddress());
|
||||
}
|
||||
|
||||
BluetoothGatt getGatt() {
|
||||
return mGatt;
|
||||
}
|
||||
|
||||
// Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
|
||||
// of TRANSPORT_LE. Let's force ourselves to connect low energy.
|
||||
private BluetoothGatt connectGatt(boolean managed) {
|
||||
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
|
||||
try {
|
||||
return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
|
||||
} catch (Exception e) {
|
||||
return mDevice.connectGatt(mManager.getContext(), managed, this);
|
||||
}
|
||||
} else {
|
||||
return mDevice.connectGatt(mManager.getContext(), managed, this);
|
||||
}
|
||||
}
|
||||
|
||||
private BluetoothGatt connectGatt() {
|
||||
return connectGatt(false);
|
||||
}
|
||||
|
||||
protected int getConnectionState() {
|
||||
|
||||
Context context = mManager.getContext();
|
||||
if (context == null) {
|
||||
// We are lacking any context to get our Bluetooth information. We'll just assume disconnected.
|
||||
return BluetoothProfile.STATE_DISCONNECTED;
|
||||
}
|
||||
|
||||
BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
|
||||
if (btManager == null) {
|
||||
// This device doesn't support Bluetooth. We should never be here, because how did
|
||||
// we instantiate a device to start with?
|
||||
return BluetoothProfile.STATE_DISCONNECTED;
|
||||
}
|
||||
|
||||
return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
|
||||
}
|
||||
|
||||
void reconnect() {
|
||||
|
||||
if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
|
||||
mGatt.disconnect();
|
||||
mGatt = connectGatt();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected void checkConnectionForChromebookIssue() {
|
||||
if (!mIsChromebook) {
|
||||
// We only do this on Chromebooks, because otherwise it's really annoying to just attempt
|
||||
// over and over.
|
||||
return;
|
||||
}
|
||||
|
||||
int connectionState = getConnectionState();
|
||||
|
||||
switch (connectionState) {
|
||||
case BluetoothProfile.STATE_CONNECTED:
|
||||
if (!mIsConnected) {
|
||||
// We are in the Bad Chromebook Place. We can force a disconnect
|
||||
// to try to recover.
|
||||
Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect.");
|
||||
mIsReconnecting = true;
|
||||
mGatt.disconnect();
|
||||
mGatt = connectGatt(false);
|
||||
break;
|
||||
}
|
||||
else if (!isRegistered()) {
|
||||
if (mGatt.getServices().size() > 0) {
|
||||
Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover.");
|
||||
probeService(this);
|
||||
}
|
||||
else {
|
||||
Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover.");
|
||||
mIsReconnecting = true;
|
||||
mGatt.disconnect();
|
||||
mGatt = connectGatt(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case BluetoothProfile.STATE_DISCONNECTED:
|
||||
Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover.");
|
||||
|
||||
mIsReconnecting = true;
|
||||
mGatt.disconnect();
|
||||
mGatt = connectGatt(false);
|
||||
break;
|
||||
|
||||
case BluetoothProfile.STATE_CONNECTING:
|
||||
Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer.");
|
||||
break;
|
||||
}
|
||||
|
||||
final HIDDeviceBLESteamController finalThis = this;
|
||||
mHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
finalThis.checkConnectionForChromebookIssue();
|
||||
}
|
||||
}, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
private boolean isRegistered() {
|
||||
return mIsRegistered;
|
||||
}
|
||||
|
||||
private void setRegistered() {
|
||||
mIsRegistered = true;
|
||||
}
|
||||
|
||||
private boolean probeService(HIDDeviceBLESteamController controller) {
|
||||
|
||||
if (isRegistered()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!mIsConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.v(TAG, "probeService controller=" + controller);
|
||||
|
||||
for (BluetoothGattService service : mGatt.getServices()) {
|
||||
if (service.getUuid().equals(steamControllerService)) {
|
||||
Log.v(TAG, "Found Valve steam controller service " + service.getUuid());
|
||||
|
||||
for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
|
||||
if (chr.getUuid().equals(inputCharacteristicTriton_0x45)) {
|
||||
Log.v(TAG, "Found Triton input characteristic 0x45");
|
||||
mProductId = TRITON_BLE_PID;
|
||||
mReportId = 0x45;
|
||||
mInputCharacteristic = chr.getUuid();
|
||||
} else if (chr.getUuid().equals(inputCharacteristicTriton_0x47)) {
|
||||
Log.v(TAG, "Found Triton input characteristic 0x47");
|
||||
mProductId = TRITON_BLE_PID;
|
||||
mReportId = 0x47;
|
||||
mInputCharacteristic = chr.getUuid();
|
||||
} else if (chr.getUuid().equals(inputCharacteristicD0G)) {
|
||||
Log.v(TAG, "Found D0G input characteristic");
|
||||
mProductId = D0G_BLE2_PID;
|
||||
mReportId = 0x03;
|
||||
mInputCharacteristic = chr.getUuid();
|
||||
} else {
|
||||
Pattern reportPattern = Pattern.compile("100F6C([0-9A-Z]{2})", Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = reportPattern.matcher(chr.getUuid().toString());
|
||||
|
||||
if (matcher.find()) {
|
||||
try {
|
||||
int reportId = Integer.parseInt(matcher.group(1), 16);
|
||||
|
||||
reportId -= 0x35;
|
||||
if (reportId >= 0x80) {
|
||||
// This is a Triton output report characteristic that we need to care about.
|
||||
Log.v(TAG, "Found Triton output report 0x" + Integer.toString(reportId, 16));
|
||||
mOutputReportChars.put(reportId, chr);
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException nfe) {
|
||||
Log.w(TAG, "Could not parse report characteristic " + chr.getUuid().toString() + ": " + nfe.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
|
||||
if (chr.getUuid().equals(mInputCharacteristic)) {
|
||||
// Start notifications
|
||||
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
|
||||
if (cccd != null) {
|
||||
enableNotification(chr.getUuid());
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) {
|
||||
Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us.");
|
||||
mIsConnected = false;
|
||||
mIsReconnecting = true;
|
||||
mGatt.disconnect();
|
||||
mGatt = connectGatt(false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void finishCurrentGattOperation() {
|
||||
GattOperation op = null;
|
||||
synchronized (mOperations) {
|
||||
if (mCurrentOperation != null) {
|
||||
op = mCurrentOperation;
|
||||
mCurrentOperation = null;
|
||||
}
|
||||
}
|
||||
if (op != null) {
|
||||
boolean result = op.finish(); // TODO: Maybe in main thread as well?
|
||||
|
||||
// Our operation failed, let's add it back to the beginning of our queue.
|
||||
if (!result) {
|
||||
mOperations.addFirst(op);
|
||||
}
|
||||
}
|
||||
executeNextGattOperation();
|
||||
}
|
||||
|
||||
private void executeNextGattOperation() {
|
||||
synchronized (mOperations) {
|
||||
if (mCurrentOperation != null)
|
||||
return;
|
||||
|
||||
if (mOperations.isEmpty())
|
||||
return;
|
||||
|
||||
mCurrentOperation = mOperations.removeFirst();
|
||||
}
|
||||
|
||||
Runnable gattOperationRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (mOperations) {
|
||||
if (mCurrentOperation == null) {
|
||||
Log.e(TAG, "Current operation null in executor?");
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentOperation.run();
|
||||
// now wait for the GATT callback and when it comes, finish this operation
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (mCurrentOperation.getDelayMs() == 0) {
|
||||
// Run in main thread
|
||||
mHandler.post(gattOperationRunnable);
|
||||
}
|
||||
else {
|
||||
// If we have a delay on this operation, wait before we post it.
|
||||
mHandler.postDelayed(gattOperationRunnable, mCurrentOperation.getDelayMs());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void queueGattOperation(GattOperation op) {
|
||||
synchronized (mOperations) {
|
||||
mOperations.add(op);
|
||||
}
|
||||
executeNextGattOperation();
|
||||
}
|
||||
|
||||
private void enableNotification(UUID chrUuid) {
|
||||
// Add a 500ms delay to notification write for Amazon Fire TV devices, as otherwise if we do this too quickly after connecting
|
||||
// it will return success and then silently drop the operation on the floor.
|
||||
GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid, 500);
|
||||
queueGattOperation(op);
|
||||
|
||||
// Amazon Fire devices can also silently timeout on writeDescriptor, so
|
||||
// set up a little delayed check that will attempt to write a second time.
|
||||
//
|
||||
// While this only seems to be needed on Amazon Fire TV devices at present, it
|
||||
// doesn't hurt to have a retry on other devices as well.
|
||||
//
|
||||
final HIDDeviceBLESteamController finalThis = this;
|
||||
final UUID finalUuid = chrUuid;
|
||||
mHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!finalThis.mHasEnabledNotifications) {
|
||||
|
||||
if (finalThis.mHasSeenInputUpdate) {
|
||||
// Amazon Five devices may have enabled notifications on the input characteristic and not given us a callback. If we've seen
|
||||
// input reports, though, somewhat by definition notifications are enabled.
|
||||
Log.w(TAG, "WriteDescriptor has never returned, but we've seen input reports. Moving on with controller initialization.");
|
||||
finalThis.mHasEnabledNotifications = true;
|
||||
finalThis.enableValveMode();
|
||||
return;
|
||||
}
|
||||
|
||||
// Give one more try.
|
||||
GattOperation retry = HIDDeviceBLESteamController.GattOperation.enableNotification(finalThis.mGatt, finalUuid, 500);
|
||||
finalThis.queueGattOperation(retry);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
void writeCharacteristic(UUID uuid, byte[] value) {
|
||||
GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value);
|
||||
queueGattOperation(op);
|
||||
}
|
||||
|
||||
void readCharacteristic(UUID uuid) {
|
||||
GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid);
|
||||
queueGattOperation(op);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////// BluetoothGattCallback overridden methods
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothGatt g, int status, int newState) {
|
||||
//Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
|
||||
mIsReconnecting = false;
|
||||
if (newState == 2) {
|
||||
mIsConnected = true;
|
||||
// Run directly, without GattOperation
|
||||
if (!isRegistered()) {
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mGatt.discoverServices();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (newState == 0) {
|
||||
mIsConnected = false;
|
||||
}
|
||||
|
||||
// Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
|
||||
//Log.v(TAG, "onServicesDiscovered status=" + status);
|
||||
if (status == 0) {
|
||||
if (gatt.getServices().size() == 0) {
|
||||
Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack.");
|
||||
mIsReconnecting = true;
|
||||
mIsConnected = false;
|
||||
gatt.disconnect();
|
||||
mGatt = connectGatt(false);
|
||||
} else {
|
||||
if (getProductId() == TRITON_BLE_PID) {
|
||||
// Android will not properly play well with Data Length Extensions without manually requesting a large MTU,
|
||||
// and Triton controllers require DLE support.
|
||||
//
|
||||
// 517 is basically a "magic number" as far as Android's bluetooth code is concerned, so do not change
|
||||
// this value. It is functionally "please enable data length extensions" on some Android builds.
|
||||
mGatt.requestMtu(517);
|
||||
}
|
||||
|
||||
probeService(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||
//Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid());
|
||||
|
||||
if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) {
|
||||
mManager.HIDDeviceReportResponse(getId(), characteristic.getValue());
|
||||
}
|
||||
|
||||
finishCurrentGattOperation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||
//Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
|
||||
|
||||
if (characteristic.getUuid().equals(reportCharacteristic)) {
|
||||
// Only register controller with the native side once it has been fully configured
|
||||
if (!isRegistered()) {
|
||||
Log.v(TAG, "Registering Steam Controller with ID: " + getId());
|
||||
mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true, mReportId);
|
||||
setRegistered();
|
||||
}
|
||||
}
|
||||
|
||||
finishCurrentGattOperation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||
// Enable this for verbose logging of controller input reports
|
||||
//Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue()));
|
||||
|
||||
if (characteristic.getUuid().equals(mInputCharacteristic) && !mFrozen) {
|
||||
mHasSeenInputUpdate = true;
|
||||
mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
|
||||
//Log.v(TAG, "onDescriptorRead status=" + status);
|
||||
}
|
||||
|
||||
private void enableValveMode()
|
||||
{
|
||||
BluetoothGattService valveService = mGatt.getService(steamControllerService);
|
||||
if (valveService == null)
|
||||
return;
|
||||
|
||||
BluetoothGattCharacteristic reportChr = valveService.getCharacteristic(reportCharacteristic);
|
||||
if (reportChr != null) {
|
||||
if (getProductId() == TRITON_BLE_PID) {
|
||||
// For Triton we just mark things registered.
|
||||
Log.v(TAG, "Registering Triton Steam Controller with ID: " + getId());
|
||||
mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true, mReportId);
|
||||
setRegistered();
|
||||
} else {
|
||||
// For the original controller, we need to manually enter Valve mode.
|
||||
Log.v(TAG, "Writing report characteristic to enter valve mode");
|
||||
reportChr.setValue(enterValveMode);
|
||||
mGatt.writeCharacteristic(reportChr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
|
||||
BluetoothGattCharacteristic chr = descriptor.getCharacteristic();
|
||||
//Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid());
|
||||
|
||||
if (chr.getUuid().equals(mInputCharacteristic)) {
|
||||
mHasEnabledNotifications = true;
|
||||
enableValveMode();
|
||||
}
|
||||
|
||||
finishCurrentGattOperation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
|
||||
//Log.v(TAG, "onReliableWriteCompleted status=" + status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
|
||||
//Log.v(TAG, "onReadRemoteRssi status=" + status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
|
||||
//Log.v(TAG, "onMtuChanged status=" + status);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////// Public API
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return mDeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVendorId() {
|
||||
// Valve Corporation
|
||||
final int VALVE_USB_VID = 0x28DE;
|
||||
return VALVE_USB_VID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProductId() {
|
||||
if (mProductId > 0) {
|
||||
// We've already set a product ID.
|
||||
return mProductId;
|
||||
}
|
||||
|
||||
if (mDevice.getName().startsWith("Steam Ctrl")) {
|
||||
// We're a newer Triton device
|
||||
mProductId = TRITON_BLE_PID;
|
||||
} else {
|
||||
// We're an OG Steam Controller
|
||||
mProductId = D0G_BLE2_PID;
|
||||
}
|
||||
|
||||
return mProductId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSerialNumber() {
|
||||
// This will be read later via feature report by Steam
|
||||
return "12345";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVersion() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturerName() {
|
||||
return "Valve Corporation";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProductName() {
|
||||
return "Steam Controller";
|
||||
}
|
||||
|
||||
@Override
|
||||
public UsbDevice getDevice() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean open() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int writeReport(byte[] report, boolean feature) {
|
||||
if (!isRegistered()) {
|
||||
Log.e(TAG, "Attempted writeReport before Steam Controller is registered!");
|
||||
if (mIsConnected) {
|
||||
probeService(this);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (feature) {
|
||||
// We need to skip the first byte, as that doesn't go over the air
|
||||
byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
|
||||
//Log.v(TAG, "writeFeatureReport " + HexDump.dumpHexString(actual_report));
|
||||
writeCharacteristic(reportCharacteristic, actual_report);
|
||||
return report.length;
|
||||
} else {
|
||||
// If we're an original-recipe Steam Controller we just write to the characteristic directly.
|
||||
if (getProductId() == D0G_BLE2_PID) {
|
||||
//Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report));
|
||||
writeCharacteristic(reportCharacteristic, report);
|
||||
return report.length;
|
||||
}
|
||||
|
||||
// If we're a Triton, we need to find the correct report characteristic.
|
||||
if (report.length > 0) {
|
||||
int reportId = report[0] & 0xFF;
|
||||
BluetoothGattCharacteristic targetedReportCharacteristic = mOutputReportChars.get(reportId);
|
||||
if (targetedReportCharacteristic != null) {
|
||||
byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
|
||||
//Log.v(TAG, "writeOutputReport 0x" + Integer.toString(reportId, 16) + " " + HexDump.dumpHexString(report));
|
||||
writeCharacteristic(targetedReportCharacteristic.getUuid(), actual_report);
|
||||
return report.length;
|
||||
} else {
|
||||
Log.w(TAG, "Got report write request for unknown report type 0x" + Integer.toString(reportId, 16));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readReport(byte[] report, boolean feature) {
|
||||
if (!isRegistered()) {
|
||||
Log.e(TAG, "Attempted readReport before Steam Controller is registered!");
|
||||
if (mIsConnected) {
|
||||
probeService(this);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (feature) {
|
||||
readCharacteristic(reportCharacteristic);
|
||||
return true;
|
||||
} else {
|
||||
// Not implemented
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFrozen(boolean frozen) {
|
||||
mFrozen = frozen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
close();
|
||||
|
||||
BluetoothGatt g = mGatt;
|
||||
if (g != null) {
|
||||
g.disconnect();
|
||||
g.close();
|
||||
mGatt = null;
|
||||
}
|
||||
mManager = null;
|
||||
mIsRegistered = false;
|
||||
mIsConnected = false;
|
||||
mOperations.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,698 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.PendingIntent;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothManager;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.hardware.usb.*;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class HIDDeviceManager {
|
||||
private static final String TAG = "hidapi";
|
||||
private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION";
|
||||
|
||||
private static HIDDeviceManager sManager;
|
||||
private static int sManagerRefCount = 0;
|
||||
|
||||
static public HIDDeviceManager acquire(Context context) {
|
||||
if (sManagerRefCount == 0) {
|
||||
sManager = new HIDDeviceManager(context);
|
||||
}
|
||||
++sManagerRefCount;
|
||||
return sManager;
|
||||
}
|
||||
|
||||
static public void release(HIDDeviceManager manager) {
|
||||
if (manager == sManager) {
|
||||
--sManagerRefCount;
|
||||
if (sManagerRefCount == 0) {
|
||||
sManager.close();
|
||||
sManager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Context mContext;
|
||||
private HashMap<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>();
|
||||
private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>();
|
||||
private int mNextDeviceId = 0;
|
||||
private SharedPreferences mSharedPreferences = null;
|
||||
private boolean mIsChromebook = false;
|
||||
private UsbManager mUsbManager;
|
||||
private Handler mHandler;
|
||||
private BluetoothManager mBluetoothManager;
|
||||
private List<BluetoothDevice> mLastBluetoothDevices;
|
||||
|
||||
private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
||||
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
handleUsbDeviceAttached(usbDevice);
|
||||
} else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
|
||||
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
handleUsbDeviceDetached(usbDevice);
|
||||
} else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) {
|
||||
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
// Bluetooth device was connected. If it was a Steam Controller, handle it
|
||||
if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
|
||||
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
Log.d(TAG, "Bluetooth device connected: " + device);
|
||||
|
||||
if (isSteamController(device)) {
|
||||
connectBluetoothDevice(device);
|
||||
}
|
||||
}
|
||||
|
||||
// Bluetooth device was disconnected, remove from controller manager (if any)
|
||||
if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
|
||||
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
Log.d(TAG, "Bluetooth device disconnected: " + device);
|
||||
|
||||
disconnectBluetoothDevice(device);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private HIDDeviceManager(final Context context) {
|
||||
mContext = context;
|
||||
|
||||
HIDDeviceRegisterCallback();
|
||||
|
||||
mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE);
|
||||
mIsChromebook = SDLActivity.isChromebook();
|
||||
|
||||
// if (shouldClear) {
|
||||
// SharedPreferences.Editor spedit = mSharedPreferences.edit();
|
||||
// spedit.clear();
|
||||
// spedit.apply();
|
||||
// }
|
||||
// else
|
||||
{
|
||||
mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0);
|
||||
}
|
||||
}
|
||||
|
||||
Context getContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
int getDeviceIDForIdentifier(String identifier) {
|
||||
SharedPreferences.Editor spedit = mSharedPreferences.edit();
|
||||
|
||||
int result = mSharedPreferences.getInt(identifier, 0);
|
||||
if (result == 0) {
|
||||
result = mNextDeviceId++;
|
||||
spedit.putInt("next_device_id", mNextDeviceId);
|
||||
}
|
||||
|
||||
spedit.putInt(identifier, result);
|
||||
spedit.apply();
|
||||
return result;
|
||||
}
|
||||
|
||||
private void initializeUSB() {
|
||||
mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE);
|
||||
if (mUsbManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
// Logging
|
||||
for (UsbDevice device : mUsbManager.getDeviceList().values()) {
|
||||
Log.i(TAG,"Path: " + device.getDeviceName());
|
||||
Log.i(TAG,"Manufacturer: " + device.getManufacturerName());
|
||||
Log.i(TAG,"Product: " + device.getProductName());
|
||||
Log.i(TAG,"ID: " + device.getDeviceId());
|
||||
Log.i(TAG,"Class: " + device.getDeviceClass());
|
||||
Log.i(TAG,"Protocol: " + device.getDeviceProtocol());
|
||||
Log.i(TAG,"Vendor ID " + device.getVendorId());
|
||||
Log.i(TAG,"Product ID: " + device.getProductId());
|
||||
Log.i(TAG,"Interface count: " + device.getInterfaceCount());
|
||||
Log.i(TAG,"---------------------------------------");
|
||||
|
||||
// Get interface details
|
||||
for (int index = 0; index < device.getInterfaceCount(); index++) {
|
||||
UsbInterface mUsbInterface = device.getInterface(index);
|
||||
Log.i(TAG," ***** *****");
|
||||
Log.i(TAG," Interface index: " + index);
|
||||
Log.i(TAG," Interface ID: " + mUsbInterface.getId());
|
||||
Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass());
|
||||
Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass());
|
||||
Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol());
|
||||
Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount());
|
||||
|
||||
// Get endpoint details
|
||||
for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++)
|
||||
{
|
||||
UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi);
|
||||
Log.i(TAG," ++++ ++++ ++++");
|
||||
Log.i(TAG," Endpoint index: " + epi);
|
||||
Log.i(TAG," Attributes: " + mEndpoint.getAttributes());
|
||||
Log.i(TAG," Direction: " + mEndpoint.getDirection());
|
||||
Log.i(TAG," Number: " + mEndpoint.getEndpointNumber());
|
||||
Log.i(TAG," Interval: " + mEndpoint.getInterval());
|
||||
Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize());
|
||||
Log.i(TAG," Type: " + mEndpoint.getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i(TAG," No more devices connected.");
|
||||
*/
|
||||
|
||||
// Register for USB broadcasts and permission completions
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
|
||||
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
|
||||
filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION);
|
||||
if (Build.VERSION.SDK_INT >= 33) { /* Android 13.0 (TIRAMISU) */
|
||||
mContext.registerReceiver(mUsbBroadcast, filter, Context.RECEIVER_EXPORTED);
|
||||
} else {
|
||||
mContext.registerReceiver(mUsbBroadcast, filter);
|
||||
}
|
||||
|
||||
for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
|
||||
handleUsbDeviceAttached(usbDevice);
|
||||
}
|
||||
}
|
||||
|
||||
UsbManager getUSBManager() {
|
||||
return mUsbManager;
|
||||
}
|
||||
|
||||
private void shutdownUSB() {
|
||||
try {
|
||||
mContext.unregisterReceiver(mUsbBroadcast);
|
||||
} catch (Exception e) {
|
||||
// We may not have registered, that's okay
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) {
|
||||
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
|
||||
return true;
|
||||
}
|
||||
if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) {
|
||||
final int XB360_IFACE_SUBCLASS = 93;
|
||||
final int XB360_IFACE_PROTOCOL = 1; // Wired
|
||||
final int XB360W_IFACE_PROTOCOL = 129; // Wireless
|
||||
final int[] SUPPORTED_VENDORS = {
|
||||
0x0079, // GPD Win 2
|
||||
0x044f, // Thrustmaster
|
||||
0x045e, // Microsoft
|
||||
0x046d, // Logitech
|
||||
0x056e, // Elecom
|
||||
0x06a3, // Saitek
|
||||
0x0738, // Mad Catz
|
||||
0x07ff, // Mad Catz
|
||||
0x0e6f, // PDP
|
||||
0x0f0d, // Hori
|
||||
0x1038, // SteelSeries
|
||||
0x11c9, // Nacon
|
||||
0x12ab, // Unknown
|
||||
0x1430, // RedOctane
|
||||
0x146b, // BigBen
|
||||
0x1532, // Razer Sabertooth
|
||||
0x15e4, // Numark
|
||||
0x162e, // Joytech
|
||||
0x1689, // Razer Onza
|
||||
0x1949, // Lab126, Inc.
|
||||
0x1bad, // Harmonix
|
||||
0x20d6, // PowerA
|
||||
0x24c6, // PowerA
|
||||
0x2c22, // Qanba
|
||||
0x2dc8, // 8BitDo
|
||||
0x3537, // GameSir
|
||||
0x37d7, // Flydigi
|
||||
0x9886, // ASTRO Gaming
|
||||
};
|
||||
|
||||
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||
usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
|
||||
(usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL ||
|
||||
usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) {
|
||||
int vendor_id = usbDevice.getVendorId();
|
||||
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||
if (vendor_id == supportedVid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) {
|
||||
final int XB1_IFACE_SUBCLASS = 71;
|
||||
final int XB1_IFACE_PROTOCOL = 208;
|
||||
final int[] SUPPORTED_VENDORS = {
|
||||
0x03f0, // HP
|
||||
0x044f, // Thrustmaster
|
||||
0x045e, // Microsoft
|
||||
0x0738, // Mad Catz
|
||||
0x0b05, // ASUS
|
||||
0x0e6f, // PDP
|
||||
0x0f0d, // Hori
|
||||
0x10f5, // Turtle Beach
|
||||
0x1532, // Razer Wildcat
|
||||
0x20d6, // PowerA
|
||||
0x24c6, // PowerA
|
||||
0x294b, // Snakebyte
|
||||
0x2dc8, // 8BitDo
|
||||
0x2e24, // Hyperkin
|
||||
0x2e95, // SCUF
|
||||
0x3285, // Nacon
|
||||
0x3537, // GameSir
|
||||
0x366c, // ByoWave
|
||||
};
|
||||
|
||||
if (usbInterface.getId() == 0 &&
|
||||
usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||
usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
|
||||
usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
|
||||
int vendor_id = usbDevice.getVendorId();
|
||||
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||
if (vendor_id == supportedVid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleUsbDeviceAttached(UsbDevice usbDevice) {
|
||||
connectHIDDeviceUSB(usbDevice);
|
||||
}
|
||||
|
||||
private void handleUsbDeviceDetached(UsbDevice usbDevice) {
|
||||
List<Integer> devices = new ArrayList<Integer>();
|
||||
for (HIDDevice device : mDevicesById.values()) {
|
||||
if (usbDevice.equals(device.getDevice())) {
|
||||
devices.add(device.getId());
|
||||
}
|
||||
}
|
||||
for (int id : devices) {
|
||||
HIDDevice device = mDevicesById.get(id);
|
||||
mDevicesById.remove(id);
|
||||
device.shutdown();
|
||||
HIDDeviceDisconnected(id);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) {
|
||||
for (HIDDevice device : mDevicesById.values()) {
|
||||
if (usbDevice.equals(device.getDevice())) {
|
||||
boolean opened = false;
|
||||
if (permission_granted) {
|
||||
opened = device.open();
|
||||
}
|
||||
HIDDeviceOpenResult(device.getId(), opened);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void connectHIDDeviceUSB(UsbDevice usbDevice) {
|
||||
synchronized (this) {
|
||||
int interface_mask = 0;
|
||||
for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) {
|
||||
UsbInterface usbInterface = usbDevice.getInterface(interface_index);
|
||||
if (isHIDDeviceInterface(usbDevice, usbInterface)) {
|
||||
// Check to see if we've already added this interface
|
||||
// This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive
|
||||
int interface_id = usbInterface.getId();
|
||||
if ((interface_mask & (1 << interface_id)) != 0) {
|
||||
continue;
|
||||
}
|
||||
interface_mask |= (1 << interface_id);
|
||||
|
||||
HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index);
|
||||
int id = device.getId();
|
||||
mDevicesById.put(id, device);
|
||||
HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol(), false, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeBluetooth() {
|
||||
Log.d(TAG, "Initializing Bluetooth");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 31 /* Android 12 */ &&
|
||||
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH_CONNECT");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT <= 30 /* Android 11.0 (R) */ &&
|
||||
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
|
||||
Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find bonded bluetooth controllers and create SteamControllers for them
|
||||
mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
|
||||
if (mBluetoothManager == null) {
|
||||
// This device doesn't support Bluetooth.
|
||||
return;
|
||||
}
|
||||
|
||||
BluetoothAdapter btAdapter = mBluetoothManager.getAdapter();
|
||||
if (btAdapter == null) {
|
||||
// This device has Bluetooth support in the codebase, but has no available adapters.
|
||||
return;
|
||||
}
|
||||
|
||||
// Get our bonded devices.
|
||||
for (BluetoothDevice device : btAdapter.getBondedDevices()) {
|
||||
|
||||
Log.d(TAG, "Bluetooth device available: " + device);
|
||||
if (isSteamController(device)) {
|
||||
connectBluetoothDevice(device);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// NOTE: These don't work on Chromebooks, to my undying dismay.
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
|
||||
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
|
||||
if (Build.VERSION.SDK_INT >= 33) { /* Android 13.0 (TIRAMISU) */
|
||||
mContext.registerReceiver(mBluetoothBroadcast, filter, Context.RECEIVER_EXPORTED);
|
||||
} else {
|
||||
mContext.registerReceiver(mBluetoothBroadcast, filter);
|
||||
}
|
||||
|
||||
if (mIsChromebook) {
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
mLastBluetoothDevices = new ArrayList<BluetoothDevice>();
|
||||
|
||||
// final HIDDeviceManager finalThis = this;
|
||||
// mHandler.postDelayed(new Runnable() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// finalThis.chromebookConnectionHandler();
|
||||
// }
|
||||
// }, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
private void shutdownBluetooth() {
|
||||
try {
|
||||
mContext.unregisterReceiver(mBluetoothBroadcast);
|
||||
} catch (Exception e) {
|
||||
// We may not have registered, that's okay
|
||||
}
|
||||
}
|
||||
|
||||
// Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly.
|
||||
// This function provides a sort of dummy version of that, watching for changes in the
|
||||
// connected devices and attempting to add controllers as things change.
|
||||
void chromebookConnectionHandler() {
|
||||
if (!mIsChromebook) {
|
||||
return;
|
||||
}
|
||||
|
||||
ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>();
|
||||
ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>();
|
||||
|
||||
List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
|
||||
|
||||
for (BluetoothDevice bluetoothDevice : currentConnected) {
|
||||
if (!mLastBluetoothDevices.contains(bluetoothDevice)) {
|
||||
connected.add(bluetoothDevice);
|
||||
}
|
||||
}
|
||||
for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) {
|
||||
if (!currentConnected.contains(bluetoothDevice)) {
|
||||
disconnected.add(bluetoothDevice);
|
||||
}
|
||||
}
|
||||
|
||||
mLastBluetoothDevices = currentConnected;
|
||||
|
||||
for (BluetoothDevice bluetoothDevice : disconnected) {
|
||||
disconnectBluetoothDevice(bluetoothDevice);
|
||||
}
|
||||
for (BluetoothDevice bluetoothDevice : connected) {
|
||||
connectBluetoothDevice(bluetoothDevice);
|
||||
}
|
||||
|
||||
final HIDDeviceManager finalThis = this;
|
||||
mHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
finalThis.chromebookConnectionHandler();
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) {
|
||||
Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice);
|
||||
synchronized (this) {
|
||||
if (mBluetoothDevices.containsKey(bluetoothDevice)) {
|
||||
Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect");
|
||||
|
||||
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
|
||||
device.reconnect();
|
||||
|
||||
return false;
|
||||
}
|
||||
HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice);
|
||||
int id = device.getId();
|
||||
mBluetoothDevices.put(bluetoothDevice, device);
|
||||
mDevicesById.put(id, device);
|
||||
|
||||
// The Steam Controller will mark itself connected once initialization is complete
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) {
|
||||
synchronized (this) {
|
||||
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
|
||||
if (device == null)
|
||||
return;
|
||||
|
||||
int id = device.getId();
|
||||
mBluetoothDevices.remove(bluetoothDevice);
|
||||
mDevicesById.remove(id);
|
||||
device.shutdown();
|
||||
HIDDeviceDisconnected(id);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isSteamController(BluetoothDevice bluetoothDevice) {
|
||||
// Sanity check. If you pass in a null device, by definition it is never a Steam Controller.
|
||||
if (bluetoothDevice == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the device has no local name, we really don't want to try an equality check against it.
|
||||
if (bluetoothDevice.getName() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Steam Controllers will always support Bluetooth Low Energy
|
||||
if ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Match on the name either the original Steam Controller or the new second-generation one advertise with.
|
||||
return bluetoothDevice.getName().equals("SteamController") || bluetoothDevice.getName().startsWith("Steam Ctrl");
|
||||
}
|
||||
|
||||
private void close() {
|
||||
shutdownUSB();
|
||||
shutdownBluetooth();
|
||||
synchronized (this) {
|
||||
for (HIDDevice device : mDevicesById.values()) {
|
||||
device.shutdown();
|
||||
}
|
||||
mDevicesById.clear();
|
||||
mBluetoothDevices.clear();
|
||||
HIDDeviceReleaseCallback();
|
||||
}
|
||||
}
|
||||
|
||||
public void setFrozen(boolean frozen) {
|
||||
synchronized (this) {
|
||||
for (HIDDevice device : mDevicesById.values()) {
|
||||
device.setFrozen(frozen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private HIDDevice getDevice(int id) {
|
||||
synchronized (this) {
|
||||
HIDDevice result = mDevicesById.get(id);
|
||||
if (result == null) {
|
||||
Log.v(TAG, "No device for id: " + id);
|
||||
Log.v(TAG, "Available devices: " + mDevicesById.keySet());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////// JNI interface functions
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
boolean initialize(boolean usb, boolean bluetooth) {
|
||||
Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")");
|
||||
|
||||
if (usb) {
|
||||
initializeUSB();
|
||||
}
|
||||
if (bluetooth) {
|
||||
initializeBluetooth();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean openDevice(int deviceID) {
|
||||
Log.v(TAG, "openDevice deviceID=" + deviceID);
|
||||
HIDDevice device = getDevice(deviceID);
|
||||
if (device == null) {
|
||||
HIDDeviceDisconnected(deviceID);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Look to see if this is a USB device and we have permission to access it
|
||||
UsbDevice usbDevice = device.getDevice();
|
||||
if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) {
|
||||
HIDDeviceOpenPending(deviceID);
|
||||
try {
|
||||
final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31
|
||||
int flags;
|
||||
if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
|
||||
flags = FLAG_MUTABLE;
|
||||
} else {
|
||||
flags = 0;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(HIDDeviceManager.ACTION_USB_PERMISSION);
|
||||
intent.setPackage(mContext.getPackageName());
|
||||
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, intent, flags));
|
||||
} catch (Exception e) {
|
||||
Log.v(TAG, "Couldn't request permission for USB device " + usbDevice);
|
||||
HIDDeviceOpenResult(deviceID, false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return device.open();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int writeReport(int deviceID, byte[] report, boolean feature) {
|
||||
try {
|
||||
//Log.v(TAG, "writeReport deviceID=" + deviceID + " length=" + report.length);
|
||||
HIDDevice device;
|
||||
device = getDevice(deviceID);
|
||||
if (device == null) {
|
||||
HIDDeviceDisconnected(deviceID);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return device.writeReport(report, feature);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
boolean readReport(int deviceID, byte[] report, boolean feature) {
|
||||
try {
|
||||
//Log.v(TAG, "readReport deviceID=" + deviceID);
|
||||
HIDDevice device;
|
||||
device = getDevice(deviceID);
|
||||
if (device == null) {
|
||||
HIDDeviceDisconnected(deviceID);
|
||||
return false;
|
||||
}
|
||||
|
||||
return device.readReport(report, feature);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void closeDevice(int deviceID) {
|
||||
try {
|
||||
Log.v(TAG, "closeDevice deviceID=" + deviceID);
|
||||
HIDDevice device;
|
||||
device = getDevice(deviceID);
|
||||
if (device == null) {
|
||||
HIDDeviceDisconnected(deviceID);
|
||||
return;
|
||||
}
|
||||
|
||||
device.close();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////// Native methods
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private native void HIDDeviceRegisterCallback();
|
||||
private native void HIDDeviceReleaseCallback();
|
||||
|
||||
native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol, boolean bBluetooth, int reportID);
|
||||
native void HIDDeviceOpenPending(int deviceID);
|
||||
native void HIDDeviceOpenResult(int deviceID, boolean opened);
|
||||
native void HIDDeviceDisconnected(int deviceID);
|
||||
|
||||
native void HIDDeviceInputReport(int deviceID, byte[] report);
|
||||
native void HIDDeviceReportResponse(int deviceID, byte[] report);
|
||||
}
|
||||
@@ -1,354 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.hardware.usb.*;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
class HIDDeviceUSB implements HIDDevice {
|
||||
|
||||
private static final String TAG = "hidapi";
|
||||
|
||||
protected HIDDeviceManager mManager;
|
||||
protected UsbDevice mDevice;
|
||||
protected int mInterfaceIndex;
|
||||
protected int mInterface;
|
||||
protected int mDeviceId;
|
||||
protected UsbDeviceConnection mConnection;
|
||||
protected UsbEndpoint mInputEndpoint;
|
||||
protected UsbEndpoint mOutputEndpoint;
|
||||
protected InputThread mInputThread;
|
||||
protected boolean mRunning;
|
||||
protected boolean mFrozen;
|
||||
protected boolean mClaimed;
|
||||
|
||||
public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) {
|
||||
mManager = manager;
|
||||
mDevice = usbDevice;
|
||||
mInterfaceIndex = interface_index;
|
||||
mInterface = mDevice.getInterface(mInterfaceIndex).getId();
|
||||
mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier());
|
||||
mRunning = false;
|
||||
mClaimed = false;
|
||||
}
|
||||
|
||||
String getIdentifier() {
|
||||
return String.format(Locale.ENGLISH, "%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return mDeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVendorId() {
|
||||
return mDevice.getVendorId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProductId() {
|
||||
return mDevice.getProductId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSerialNumber() {
|
||||
String result = null;
|
||||
try {
|
||||
result = mDevice.getSerialNumber();
|
||||
}
|
||||
catch (SecurityException exception) {
|
||||
//Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage());
|
||||
}
|
||||
if (result == null) {
|
||||
result = "";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVersion() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturerName() {
|
||||
String result;
|
||||
result = mDevice.getManufacturerName();
|
||||
if (result == null) {
|
||||
result = String.format("%x", getVendorId());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProductName() {
|
||||
String result;
|
||||
result = mDevice.getProductName();
|
||||
if (result == null) {
|
||||
result = String.format("%x", getProductId());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UsbDevice getDevice() {
|
||||
return mDevice;
|
||||
}
|
||||
|
||||
String getDeviceName() {
|
||||
return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean open() {
|
||||
mConnection = mManager.getUSBManager().openDevice(mDevice);
|
||||
if (mConnection == null) {
|
||||
Log.w(TAG, "Unable to open USB device " + getDeviceName());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Force claim our interface
|
||||
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
|
||||
if (!mConnection.claimInterface(iface, true)) {
|
||||
Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName());
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
mClaimed = true;
|
||||
|
||||
// Find the endpoints
|
||||
for (int j = 0; j < iface.getEndpointCount(); j++) {
|
||||
UsbEndpoint endpt = iface.getEndpoint(j);
|
||||
switch (endpt.getDirection()) {
|
||||
case UsbConstants.USB_DIR_IN:
|
||||
if (mInputEndpoint == null) {
|
||||
mInputEndpoint = endpt;
|
||||
}
|
||||
break;
|
||||
case UsbConstants.USB_DIR_OUT:
|
||||
if (mOutputEndpoint == null) {
|
||||
mOutputEndpoint = endpt;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the required endpoints were present. The original Steam Controller and the wireless dongle for it do NOT
|
||||
// actually have -- or require -- output endpoints, so we need to accept only an input one for them or else we'll fall
|
||||
// back to the Android system gamepad functionality (and lose our paddles et al).
|
||||
if (mInputEndpoint == null) {
|
||||
Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName());
|
||||
mConnection.releaseInterface(iface);
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start listening for input
|
||||
mRunning = true;
|
||||
mInputThread = new InputThread();
|
||||
mInputThread.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int writeReport(byte[] report, boolean feature) {
|
||||
if (mConnection == null) {
|
||||
Log.w(TAG, "writeReport() called with no device connection");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!mClaimed) {
|
||||
Log.w(TAG, "writeReport() called but some other process currently owns the USB device");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (feature) {
|
||||
int res = -1;
|
||||
int offset = 0;
|
||||
int length = report.length;
|
||||
boolean skipped_report_id = false;
|
||||
byte report_number = report[0];
|
||||
|
||||
if (report_number == 0x0) {
|
||||
++offset;
|
||||
--length;
|
||||
skipped_report_id = true;
|
||||
}
|
||||
|
||||
res = mConnection.controlTransfer(
|
||||
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT,
|
||||
0x09/*HID set_report*/,
|
||||
(3/*HID feature*/ << 8) | report_number,
|
||||
mInterface,
|
||||
report, offset, length,
|
||||
1000/*timeout millis*/);
|
||||
|
||||
if (res < 0) {
|
||||
Log.w(TAG, "writeFeatureReport() returned " + res + " on device " + getDeviceName());
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (skipped_report_id) {
|
||||
++length;
|
||||
}
|
||||
return length;
|
||||
} else {
|
||||
if (mOutputEndpoint == null)
|
||||
{
|
||||
Log.e(TAG, "Tried to write an output report to an interface with no output endpoint!");
|
||||
return -1;
|
||||
}
|
||||
int res = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000);
|
||||
if (res != report.length) {
|
||||
Log.w(TAG, "writeOutputReport() returned " + res + " on device " + getDeviceName());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readReport(byte[] report, boolean feature) {
|
||||
int res = -1;
|
||||
int offset = 0;
|
||||
int length = report.length;
|
||||
boolean skipped_report_id = false;
|
||||
byte report_number = report[0];
|
||||
|
||||
if (mConnection == null) {
|
||||
Log.w(TAG, "readReport() called with no device connection");
|
||||
return false;
|
||||
}
|
||||
if (!mClaimed) {
|
||||
if (feature) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (report_number == 0x0) {
|
||||
/* Offset the return buffer by 1, so that the report ID
|
||||
will remain in byte 0. */
|
||||
++offset;
|
||||
--length;
|
||||
skipped_report_id = true;
|
||||
}
|
||||
|
||||
res = mConnection.controlTransfer(
|
||||
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN,
|
||||
0x01/*HID get_report*/,
|
||||
((feature ? 3/*HID feature*/ : 1/*HID Input*/) << 8) | report_number,
|
||||
mInterface,
|
||||
report, offset, length,
|
||||
1000/*timeout millis*/);
|
||||
|
||||
if (res < 0) {
|
||||
Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (skipped_report_id) {
|
||||
++res;
|
||||
++length;
|
||||
}
|
||||
|
||||
byte[] data;
|
||||
if (res == length) {
|
||||
data = report;
|
||||
} else {
|
||||
data = Arrays.copyOfRange(report, 0, res);
|
||||
}
|
||||
mManager.HIDDeviceReportResponse(mDeviceId, data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
mRunning = false;
|
||||
if (mInputThread != null) {
|
||||
while (mInputThread.isAlive()) {
|
||||
mInputThread.interrupt();
|
||||
try {
|
||||
mInputThread.join();
|
||||
} catch (InterruptedException e) {
|
||||
// Keep trying until we're done
|
||||
}
|
||||
}
|
||||
mInputThread = null;
|
||||
}
|
||||
if (mConnection != null) {
|
||||
if (mClaimed) {
|
||||
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
|
||||
mConnection.releaseInterface(iface);
|
||||
}
|
||||
mConnection.close();
|
||||
mConnection = null;
|
||||
mClaimed = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
close();
|
||||
mManager = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFrozen(boolean frozen) {
|
||||
mFrozen = frozen;
|
||||
|
||||
/* If we have a valid device connection and the claim state doesn't match what we want, try to correct that. */
|
||||
if (mConnection != null && mClaimed == mFrozen) {
|
||||
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
|
||||
if (frozen) {
|
||||
mClaimed = !mConnection.releaseInterface(iface);
|
||||
if (mClaimed) {
|
||||
Log.e(TAG, "Tried to release claim on USB device, but failed!");
|
||||
}
|
||||
} else {
|
||||
mClaimed = mConnection.claimInterface(iface, true);
|
||||
if (!mClaimed) {
|
||||
Log.e(TAG, "Tried to regain claim on USB device, but failed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected class InputThread extends Thread {
|
||||
@Override
|
||||
public void run() {
|
||||
int packetSize = mInputEndpoint.getMaxPacketSize();
|
||||
byte[] packet = new byte[packetSize];
|
||||
while (mRunning) {
|
||||
int r;
|
||||
try
|
||||
{
|
||||
r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e);
|
||||
break;
|
||||
}
|
||||
if (r < 0) {
|
||||
// Could be a timeout or an I/O error
|
||||
}
|
||||
if (r > 0) {
|
||||
byte[] data;
|
||||
if (r == packetSize) {
|
||||
data = packet;
|
||||
} else {
|
||||
data = Arrays.copyOfRange(packet, 0, r);
|
||||
}
|
||||
|
||||
if (!mFrozen) {
|
||||
mManager.HIDDeviceInputReport(mDeviceId, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
SDL library initialization
|
||||
*/
|
||||
public class SDL {
|
||||
|
||||
// This function should be called first and sets up the native code
|
||||
// so it can call into the Java classes
|
||||
static public void setupJNI() {
|
||||
SDLActivity.nativeSetupJNI();
|
||||
SDLAudioManager.nativeSetupJNI();
|
||||
SDLControllerManager.nativeSetupJNI();
|
||||
}
|
||||
|
||||
// This function should be called each time the activity is started
|
||||
static public void initialize() {
|
||||
setContext(null);
|
||||
|
||||
SDLActivity.initialize();
|
||||
SDLAudioManager.initialize();
|
||||
SDLControllerManager.initialize();
|
||||
}
|
||||
|
||||
// This function stores the current activity (SDL or not)
|
||||
static public void setContext(Activity context) {
|
||||
SDLAudioManager.setContext(context);
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
static public Activity getContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
|
||||
loadLibrary(libraryName, mContext);
|
||||
}
|
||||
|
||||
static void loadLibrary(String libraryName, Context context) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
|
||||
|
||||
if (libraryName == null) {
|
||||
throw new NullPointerException("No library name provided.");
|
||||
}
|
||||
|
||||
try {
|
||||
// Let's see if we have ReLinker available in the project. This is necessary for
|
||||
// some projects that have huge numbers of local libraries bundled, and thus may
|
||||
// trip a bug in Android's native library loader which ReLinker works around. (If
|
||||
// loadLibrary works properly, ReLinker will simply use the normal Android method
|
||||
// internally.)
|
||||
//
|
||||
// To use ReLinker, just add it as a dependency. For more information, see
|
||||
// https://github.com/KeepSafe/ReLinker for ReLinker's repository.
|
||||
//
|
||||
Class<?> relinkClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
|
||||
Class<?> relinkListenerClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
|
||||
Class<?> contextClass = context.getClassLoader().loadClass("android.content.Context");
|
||||
Class<?> stringClass = context.getClassLoader().loadClass("java.lang.String");
|
||||
|
||||
// Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if
|
||||
// they've changed during updates.
|
||||
Method forceMethod = relinkClass.getDeclaredMethod("force");
|
||||
Object relinkInstance = forceMethod.invoke(null);
|
||||
Class<?> relinkInstanceClass = relinkInstance.getClass();
|
||||
|
||||
// Actually load the library!
|
||||
Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass);
|
||||
loadMethod.invoke(relinkInstance, context, libraryName, null, null);
|
||||
}
|
||||
catch (final Throwable e) {
|
||||
// Fall back
|
||||
try {
|
||||
System.loadLibrary(libraryName);
|
||||
}
|
||||
catch (final UnsatisfiedLinkError ule) {
|
||||
throw ule;
|
||||
}
|
||||
catch (final SecurityException se) {
|
||||
throw se;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static Activity mContext;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,126 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioDeviceCallback;
|
||||
import android.media.AudioDeviceInfo;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
|
||||
class SDLAudioManager {
|
||||
protected static final String TAG = "SDLAudio";
|
||||
|
||||
protected static Context mContext;
|
||||
|
||||
private static AudioDeviceCallback mAudioDeviceCallback;
|
||||
|
||||
static void initialize() {
|
||||
mAudioDeviceCallback = null;
|
||||
|
||||
if(Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */)
|
||||
{
|
||||
mAudioDeviceCallback = new AudioDeviceCallback() {
|
||||
@Override
|
||||
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
|
||||
for (AudioDeviceInfo deviceInfo : addedDevices) {
|
||||
nativeAddAudioDevice(deviceInfo.isSink(), deviceInfo.getProductName().toString(), deviceInfo.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
|
||||
for (AudioDeviceInfo deviceInfo : removedDevices) {
|
||||
nativeRemoveAudioDevice(deviceInfo.isSink(), deviceInfo.getId());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static void setContext(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
static void release(Context context) {
|
||||
// no-op atm
|
||||
}
|
||||
|
||||
// Audio
|
||||
|
||||
private static AudioDeviceInfo getInputAudioDeviceInfo(int deviceId) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) {
|
||||
if (deviceInfo.getId() == deviceId) {
|
||||
return deviceInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static AudioDeviceInfo getPlaybackAudioDeviceInfo(int deviceId) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
|
||||
if (deviceInfo.getId() == deviceId) {
|
||||
return deviceInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static void registerAudioDeviceCallback() {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
// get an initial list now, before hotplug callbacks fire.
|
||||
for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
|
||||
if (dev.getType() == AudioDeviceInfo.TYPE_TELEPHONY) {
|
||||
continue; // Device cannot be opened
|
||||
}
|
||||
nativeAddAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId());
|
||||
}
|
||||
for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) {
|
||||
nativeAddAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId());
|
||||
}
|
||||
audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null);
|
||||
}
|
||||
}
|
||||
|
||||
static void unregisterAudioDeviceCallback() {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback);
|
||||
}
|
||||
}
|
||||
|
||||
/** This method is called by SDL using JNI. */
|
||||
static void audioSetThreadPriority(boolean recording, int device_id) {
|
||||
try {
|
||||
|
||||
/* Set thread name */
|
||||
if (recording) {
|
||||
Thread.currentThread().setName("SDLAudioC" + device_id);
|
||||
} else {
|
||||
Thread.currentThread().setName("SDLAudioP" + device_id);
|
||||
}
|
||||
|
||||
/* Set thread priority */
|
||||
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.v(TAG, "modify thread properties failed " + e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
static native void nativeSetupJNI();
|
||||
|
||||
static native void nativeRemoveAudioDevice(boolean recording, int deviceId);
|
||||
|
||||
static native void nativeAddAudioDevice(boolean recording, String name, int deviceId);
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,66 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.content.*;
|
||||
import android.text.InputType;
|
||||
import android.view.*;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
/* This is a fake invisible editor view that receives the input and defines the
|
||||
* pan&scan region
|
||||
*/
|
||||
public class SDLDummyEdit extends View implements View.OnKeyListener
|
||||
{
|
||||
InputConnection ic;
|
||||
int input_type;
|
||||
|
||||
SDLDummyEdit(Context context) {
|
||||
super(context);
|
||||
setFocusableInTouchMode(true);
|
||||
setFocusable(true);
|
||||
setOnKeyListener(this);
|
||||
}
|
||||
|
||||
void setInputType(int input_type) {
|
||||
this.input_type = input_type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCheckIsTextEditor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
return SDLActivity.handleKeyEvent(v, keyCode, event, ic);
|
||||
}
|
||||
|
||||
//
|
||||
@Override
|
||||
public boolean onKeyPreIme (int keyCode, KeyEvent event) {
|
||||
// As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event
|
||||
// FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639
|
||||
// FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not
|
||||
// FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout
|
||||
// FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
|
||||
// FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :)
|
||||
if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) {
|
||||
SDLActivity.onNativeKeyboardFocusLost();
|
||||
}
|
||||
}
|
||||
return super.onKeyPreIme(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
||||
ic = new SDLInputConnection(this, true);
|
||||
|
||||
outAttrs.inputType = input_type;
|
||||
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI |
|
||||
EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */;
|
||||
|
||||
return ic;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.content.*;
|
||||
import android.os.Build;
|
||||
import android.text.Editable;
|
||||
import android.view.*;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
import android.widget.EditText;
|
||||
|
||||
class SDLInputConnection extends BaseInputConnection
|
||||
{
|
||||
protected EditText mEditText;
|
||||
protected String mCommittedText = "";
|
||||
|
||||
SDLInputConnection(View targetView, boolean fullEditor) {
|
||||
super(targetView, fullEditor);
|
||||
mEditText = new EditText(SDL.getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editable getEditable() {
|
||||
return mEditText.getEditableText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sendKeyEvent(KeyEvent event) {
|
||||
/*
|
||||
* This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard)
|
||||
* However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses
|
||||
* and so we need to generate them ourselves in commitText. To avoid duplicates on the handful of keys
|
||||
* that still do, we empty this out.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Return DOES still generate a key event, however. So rather than using it as the 'click a button' key
|
||||
* as we do with physical keyboards, let's just use it to hide the keyboard.
|
||||
*/
|
||||
|
||||
if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
|
||||
if (SDLActivity.onNativeSoftReturnKey()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return super.sendKeyEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commitText(CharSequence text, int newCursorPosition) {
|
||||
if (!super.commitText(text, newCursorPosition)) {
|
||||
return false;
|
||||
}
|
||||
updateText();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setComposingText(CharSequence text, int newCursorPosition) {
|
||||
if (!super.setComposingText(text, newCursorPosition)) {
|
||||
return false;
|
||||
}
|
||||
updateText();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
|
||||
// Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection
|
||||
// and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
|
||||
if (beforeLength > 0 && afterLength == 0) {
|
||||
// backspace(s)
|
||||
while (beforeLength-- > 0) {
|
||||
nativeGenerateScancodeForUnichar('\b');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!super.deleteSurroundingText(beforeLength, afterLength)) {
|
||||
return false;
|
||||
}
|
||||
updateText();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void updateText() {
|
||||
final Editable content = getEditable();
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String text = content.toString();
|
||||
int compareLength = Math.min(text.length(), mCommittedText.length());
|
||||
int matchLength, offset;
|
||||
|
||||
/* Backspace over characters that are no longer in the string */
|
||||
for (matchLength = 0; matchLength < compareLength; ) {
|
||||
int codePoint = mCommittedText.codePointAt(matchLength);
|
||||
if (codePoint != text.codePointAt(matchLength)) {
|
||||
break;
|
||||
}
|
||||
matchLength += Character.charCount(codePoint);
|
||||
}
|
||||
/* FIXME: This doesn't handle graphemes, like '🌬️' */
|
||||
for (offset = matchLength; offset < mCommittedText.length(); ) {
|
||||
int codePoint = mCommittedText.codePointAt(offset);
|
||||
nativeGenerateScancodeForUnichar('\b');
|
||||
offset += Character.charCount(codePoint);
|
||||
}
|
||||
|
||||
if (matchLength < text.length()) {
|
||||
String pendingText = text.subSequence(matchLength, text.length()).toString();
|
||||
if (!SDLActivity.dispatchingKeyEvent()) {
|
||||
for (offset = 0; offset < pendingText.length(); ) {
|
||||
int codePoint = pendingText.codePointAt(offset);
|
||||
if (codePoint == '\n') {
|
||||
if (SDLActivity.onNativeSoftReturnKey()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* Higher code points don't generate simulated scancodes */
|
||||
if (codePoint > 0 && codePoint < 128) {
|
||||
nativeGenerateScancodeForUnichar((char)codePoint);
|
||||
}
|
||||
offset += Character.charCount(codePoint);
|
||||
}
|
||||
}
|
||||
SDLInputConnection.nativeCommitText(pendingText, 0);
|
||||
}
|
||||
mCommittedText = text;
|
||||
}
|
||||
|
||||
public static native void nativeCommitText(String text, int newCursorPosition);
|
||||
|
||||
public static native void nativeGenerateScancodeForUnichar(char c);
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
|
||||
// This class coordinates synchronized access to sensor manager registration
|
||||
//
|
||||
// This prevents a java.util.ConcurrentModificationException exception on
|
||||
// Android 16, specifically on the Samsung Tab S9 Ultra.
|
||||
|
||||
class SDLSensorManager
|
||||
{
|
||||
static private SDLSensorManager mManager = new SDLSensorManager();
|
||||
|
||||
public static void registerListener(SensorManager manager, SensorEventListener listener, Sensor sensor, int samplingPeriodUs) {
|
||||
mManager.RegisterListener(manager, listener, sensor, samplingPeriodUs);
|
||||
}
|
||||
|
||||
public static void unregisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor) {
|
||||
mManager.UnregisterListener(manager, listener, sensor);
|
||||
}
|
||||
|
||||
private synchronized void RegisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor, int samplingPeriodUs) {
|
||||
manager.registerListener(listener, sensor, samplingPeriodUs, null);
|
||||
}
|
||||
|
||||
private synchronized void UnregisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor) {
|
||||
manager.unregisterListener(listener, sensor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,469 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.graphics.Insets;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.os.Build;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.PointerIcon;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import android.view.ScaleGestureDetector;
|
||||
|
||||
/**
|
||||
SDLSurface. This is what we draw on, so we need to know when it's created
|
||||
in order to do anything useful.
|
||||
|
||||
Because of this, that's where we set up the SDL thread
|
||||
*/
|
||||
public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
|
||||
View.OnApplyWindowInsetsListener, View.OnKeyListener, View.OnTouchListener,
|
||||
SensorEventListener, ScaleGestureDetector.OnScaleGestureListener {
|
||||
|
||||
private static native void auroraNativeSetSurfaceReady(boolean ready);
|
||||
|
||||
// Sensors
|
||||
protected SensorManager mSensorManager;
|
||||
protected Display mDisplay;
|
||||
|
||||
// Keep track of the surface size to normalize touch events
|
||||
protected float mWidth, mHeight;
|
||||
|
||||
// Is SurfaceView ready for rendering
|
||||
protected boolean mIsSurfaceReady;
|
||||
|
||||
// Is on-screen keyboard visible
|
||||
protected boolean mKeyboardVisible;
|
||||
|
||||
// Pinch events
|
||||
private final ScaleGestureDetector scaleGestureDetector;
|
||||
|
||||
// Startup
|
||||
protected SDLSurface(Context context) {
|
||||
super(context);
|
||||
getHolder().addCallback(this);
|
||||
|
||||
scaleGestureDetector = new ScaleGestureDetector(context, this);
|
||||
|
||||
setFocusable(true);
|
||||
setFocusableInTouchMode(true);
|
||||
requestFocus();
|
||||
setOnApplyWindowInsetsListener(this);
|
||||
setOnKeyListener(this);
|
||||
setOnTouchListener(this);
|
||||
|
||||
mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
|
||||
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
|
||||
|
||||
setOnGenericMotionListener(SDLActivity.getMotionListener());
|
||||
|
||||
// Some arbitrary defaults to avoid a potential division by zero
|
||||
mWidth = 1.0f;
|
||||
mHeight = 1.0f;
|
||||
|
||||
mIsSurfaceReady = false;
|
||||
}
|
||||
|
||||
protected void handlePause() {
|
||||
enableSensor(Sensor.TYPE_ACCELEROMETER, false);
|
||||
}
|
||||
|
||||
protected void handleResume() {
|
||||
setFocusable(true);
|
||||
setFocusableInTouchMode(true);
|
||||
requestFocus();
|
||||
setOnApplyWindowInsetsListener(this);
|
||||
setOnKeyListener(this);
|
||||
setOnTouchListener(this);
|
||||
enableSensor(Sensor.TYPE_ACCELEROMETER, true);
|
||||
}
|
||||
|
||||
protected Surface getNativeSurface() {
|
||||
return getHolder().getSurface();
|
||||
}
|
||||
|
||||
// Called when we have a valid drawing surface
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
Log.v("SDL", "surfaceCreated()");
|
||||
auroraNativeSetSurfaceReady(false);
|
||||
SDLActivity.onNativeSurfaceCreated();
|
||||
}
|
||||
|
||||
// Called when we lose the surface
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
Log.v("SDL", "surfaceDestroyed()");
|
||||
auroraNativeSetSurfaceReady(false);
|
||||
|
||||
// Transition to pause, if needed
|
||||
SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
|
||||
SDLActivity.handleNativeState();
|
||||
|
||||
mIsSurfaceReady = false;
|
||||
SDLActivity.onNativeSurfaceDestroyed();
|
||||
}
|
||||
|
||||
// Called when the surface is resized
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder,
|
||||
int format, int width, int height) {
|
||||
Log.v("SDL", "surfaceChanged()");
|
||||
|
||||
if (SDLActivity.mSingleton == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
int nDeviceWidth = width;
|
||||
int nDeviceHeight = height;
|
||||
float density = 1.0f;
|
||||
try
|
||||
{
|
||||
DisplayMetrics realMetrics = new DisplayMetrics();
|
||||
mDisplay.getRealMetrics( realMetrics );
|
||||
nDeviceWidth = realMetrics.widthPixels;
|
||||
nDeviceHeight = realMetrics.heightPixels;
|
||||
// Use densityDpi instead of density to more closely match what the UI scale is
|
||||
density = (float)realMetrics.densityDpi / 160.0f;
|
||||
} catch(Exception ignored) {
|
||||
}
|
||||
|
||||
synchronized(SDLActivity.getContext()) {
|
||||
// In case we're waiting on a size change after going fullscreen, send a notification.
|
||||
SDLActivity.getContext().notifyAll();
|
||||
}
|
||||
|
||||
Log.v("SDL", "Window size: " + width + "x" + height);
|
||||
Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
|
||||
SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, density, mDisplay.getRefreshRate());
|
||||
SDLActivity.onNativeResize();
|
||||
|
||||
// Prevent a screen distortion glitch,
|
||||
// for instance when the device is in Landscape and a Portrait App is resumed.
|
||||
boolean skip = false;
|
||||
int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
|
||||
|
||||
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
|
||||
if (mWidth > mHeight) {
|
||||
skip = true;
|
||||
}
|
||||
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
|
||||
if (mWidth < mHeight) {
|
||||
skip = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Special Patch for Square Resolution: Black Berry Passport
|
||||
if (skip) {
|
||||
double min = Math.min(mWidth, mHeight);
|
||||
double max = Math.max(mWidth, mHeight);
|
||||
|
||||
if (max / min < 1.20) {
|
||||
Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
|
||||
skip = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't skip if we might be multi-window or have popup dialogs
|
||||
if (skip) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
skip = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (skip) {
|
||||
Log.v("SDL", "Skip .. Surface is not ready.");
|
||||
mIsSurfaceReady = false;
|
||||
return;
|
||||
}
|
||||
|
||||
/* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
|
||||
SDLActivity.onNativeSurfaceChanged();
|
||||
|
||||
/* Surface is ready */
|
||||
mIsSurfaceReady = true;
|
||||
auroraNativeSetSurfaceReady(true);
|
||||
|
||||
SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
|
||||
SDLActivity.handleNativeState();
|
||||
}
|
||||
|
||||
// Window inset
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
if (Build.VERSION.SDK_INT >= 30 /* Android 11 (R) */) {
|
||||
Insets combined = insets.getInsets(WindowInsets.Type.systemBars() |
|
||||
WindowInsets.Type.systemGestures() |
|
||||
WindowInsets.Type.mandatorySystemGestures() |
|
||||
WindowInsets.Type.tappableElement() |
|
||||
WindowInsets.Type.displayCutout());
|
||||
|
||||
SDLActivity.onNativeInsetsChanged(combined.left, combined.right, combined.top, combined.bottom);
|
||||
|
||||
if (insets.isVisible(WindowInsets.Type.ime())) {
|
||||
if (!mKeyboardVisible) {
|
||||
mKeyboardVisible = true;
|
||||
SDLActivity.onNativeScreenKeyboardShown();
|
||||
}
|
||||
} else {
|
||||
if (mKeyboardVisible) {
|
||||
mKeyboardVisible = false;
|
||||
SDLActivity.onNativeScreenKeyboardHidden();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass these to any child views in case they need them
|
||||
return insets;
|
||||
}
|
||||
|
||||
// Key events
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
return SDLActivity.handleKeyEvent(v, keyCode, event, null);
|
||||
}
|
||||
|
||||
private float getNormalizedX(float x)
|
||||
{
|
||||
if (mWidth <= 1) {
|
||||
return 0.5f;
|
||||
} else {
|
||||
return (x / (mWidth - 1));
|
||||
}
|
||||
}
|
||||
|
||||
private float getNormalizedY(float y)
|
||||
{
|
||||
if (mHeight <= 1) {
|
||||
return 0.5f;
|
||||
} else {
|
||||
return (y / (mHeight - 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Touch events
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
/* Ref: http://developer.android.com/training/gestures/multi.html */
|
||||
int touchDevId = event.getDeviceId();
|
||||
final int pointerCount = event.getPointerCount();
|
||||
int action = event.getActionMasked();
|
||||
int pointerId;
|
||||
int i = 0;
|
||||
float x,y,p;
|
||||
|
||||
if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN)
|
||||
i = event.getActionIndex();
|
||||
|
||||
do {
|
||||
int toolType = event.getToolType(i);
|
||||
|
||||
if (toolType == MotionEvent.TOOL_TYPE_MOUSE) {
|
||||
int buttonState = event.getButtonState();
|
||||
boolean relative = false;
|
||||
|
||||
// We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
|
||||
// if we are. We'll leverage our existing mouse motion listener
|
||||
SDLGenericMotionListener_API14 motionListener = SDLActivity.getMotionListener();
|
||||
x = motionListener.getEventX(event, i);
|
||||
y = motionListener.getEventY(event, i);
|
||||
relative = motionListener.inRelativeMode();
|
||||
|
||||
SDLActivity.onNativeMouse(buttonState, action, x, y, relative);
|
||||
} else if (toolType == MotionEvent.TOOL_TYPE_STYLUS || toolType == MotionEvent.TOOL_TYPE_ERASER) {
|
||||
pointerId = event.getPointerId(i);
|
||||
x = event.getX(i);
|
||||
y = event.getY(i);
|
||||
p = event.getPressure(i);
|
||||
if (p > 1.0f) {
|
||||
// may be larger than 1.0f on some devices
|
||||
// see the documentation of getPressure(i)
|
||||
p = 1.0f;
|
||||
}
|
||||
|
||||
// BUTTON_STYLUS_PRIMARY is 2^5, so shift by 4, and apply SDL_PEN_INPUT_DOWN/SDL_PEN_INPUT_ERASER_TIP
|
||||
int buttonState = (event.getButtonState() >> 4) | (1 << (toolType == MotionEvent.TOOL_TYPE_STYLUS ? 0 : 30));
|
||||
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
|
||||
buttonState |= 0x08;
|
||||
}
|
||||
|
||||
SDLActivity.onNativePen(pointerId, SDLActivity.getMotionListener().getPenDeviceType(event.getDevice()), buttonState, action, x, y, p);
|
||||
} else { // MotionEvent.TOOL_TYPE_FINGER or MotionEvent.TOOL_TYPE_UNKNOWN
|
||||
pointerId = event.getPointerId(i);
|
||||
x = getNormalizedX(event.getX(i));
|
||||
y = getNormalizedY(event.getY(i));
|
||||
p = event.getPressure(i);
|
||||
if (p > 1.0f) {
|
||||
// may be larger than 1.0f on some devices
|
||||
// see the documentation of getPressure(i)
|
||||
p = 1.0f;
|
||||
}
|
||||
|
||||
SDLActivity.onNativeTouch(touchDevId, pointerId, action, x, y, p);
|
||||
}
|
||||
|
||||
// Non-primary up/down
|
||||
if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN)
|
||||
break;
|
||||
} while (++i < pointerCount);
|
||||
|
||||
scaleGestureDetector.onTouchEvent(event);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sensor events
|
||||
protected void enableSensor(int sensortype, boolean enabled) {
|
||||
// TODO: This uses getDefaultSensor - what if we have >1 accels?
|
||||
if (enabled) {
|
||||
SDLSensorManager.registerListener(mSensorManager, this,
|
||||
mSensorManager.getDefaultSensor(sensortype),
|
||||
SensorManager.SENSOR_DELAY_GAME);
|
||||
} else {
|
||||
SDLSensorManager.unregisterListener(mSensorManager, this,
|
||||
mSensorManager.getDefaultSensor(sensortype));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
|
||||
|
||||
// Since we may have an orientation set, we won't receive onConfigurationChanged events.
|
||||
// We thus should check here.
|
||||
int newRotation;
|
||||
|
||||
float x, y;
|
||||
switch (mDisplay.getRotation()) {
|
||||
case Surface.ROTATION_0:
|
||||
default:
|
||||
x = event.values[0];
|
||||
y = event.values[1];
|
||||
newRotation = 0;
|
||||
break;
|
||||
case Surface.ROTATION_90:
|
||||
x = -event.values[1];
|
||||
y = event.values[0];
|
||||
newRotation = 90;
|
||||
break;
|
||||
case Surface.ROTATION_180:
|
||||
x = -event.values[0];
|
||||
y = -event.values[1];
|
||||
newRotation = 180;
|
||||
break;
|
||||
case Surface.ROTATION_270:
|
||||
x = event.values[1];
|
||||
y = -event.values[0];
|
||||
newRotation = 270;
|
||||
break;
|
||||
}
|
||||
|
||||
if (newRotation != SDLActivity.mCurrentRotation) {
|
||||
SDLActivity.mCurrentRotation = newRotation;
|
||||
SDLActivity.onNativeRotationChanged(newRotation);
|
||||
}
|
||||
|
||||
SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
|
||||
y / SensorManager.GRAVITY_EARTH,
|
||||
event.values[2] / SensorManager.GRAVITY_EARTH);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent android internal NullPointerException (https://github.com/libsdl-org/SDL/issues/13306)
|
||||
@Override
|
||||
public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
|
||||
try {
|
||||
return super.onResolvePointerIcon(event, pointerIndex);
|
||||
} catch (NullPointerException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Captured pointer events for API 26.
|
||||
@Override
|
||||
public boolean onCapturedPointerEvent(MotionEvent event)
|
||||
{
|
||||
int action = event.getActionMasked();
|
||||
int pointerCount = event.getPointerCount();
|
||||
|
||||
for (int i = 0; i < pointerCount; i++) {
|
||||
float x, y;
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_SCROLL:
|
||||
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, i);
|
||||
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, i);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
x = event.getX(i);
|
||||
y = event.getY(i);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, true);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_BUTTON_PRESS:
|
||||
case MotionEvent.ACTION_BUTTON_RELEASE:
|
||||
|
||||
// Change our action value to what SDL's code expects.
|
||||
if (action == MotionEvent.ACTION_BUTTON_PRESS) {
|
||||
action = MotionEvent.ACTION_DOWN;
|
||||
} else { /* MotionEvent.ACTION_BUTTON_RELEASE */
|
||||
action = MotionEvent.ACTION_UP;
|
||||
}
|
||||
|
||||
x = event.getX(i);
|
||||
y = event.getY(i);
|
||||
int button = event.getButtonState();
|
||||
|
||||
SDLActivity.onNativeMouse(button, action, x, y, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector) {
|
||||
float scale = detector.getScaleFactor();
|
||||
SDLActivity.onNativePinchUpdate(scale);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScaleBegin(ScaleGestureDetector detector) {
|
||||
SDLActivity.onNativePinchStart();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScaleEnd(ScaleGestureDetector detector) {
|
||||
SDLActivity.onNativePinchEnd();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
SRC_DEFAULT="$ROOT_DIR/build/android-arm64/_deps/sdl-src/android-project/app/src/main/java/org/libsdl/app"
|
||||
SRC_DIR="${1:-$SRC_DEFAULT}"
|
||||
DST_DIR="$ROOT_DIR/platforms/android/app/src/main/java/org/libsdl/app"
|
||||
|
||||
if [[ ! -d "$SRC_DIR" ]]; then
|
||||
echo "SDL Java source directory not found: $SRC_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DST_DIR"
|
||||
cp -f "$SRC_DIR"/*.java "$DST_DIR"/
|
||||
echo "Synced SDL Java sources from $SRC_DIR to $DST_DIR"
|
||||
@@ -23,12 +23,12 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "@DUSK_COMPANY_NAME@\0"
|
||||
VALUE "FileDescription", "@DUSK_FILE_DESCRIPTION@\0"
|
||||
VALUE "FileVersion", "@DUSK_VERSION_STRING@\0"
|
||||
VALUE "FileVersion", "@BOREALIS_APP_VERSION@\0"
|
||||
VALUE "InternalName", "dusklight\0"
|
||||
VALUE "LegalCopyright", "@DUSK_COPYRIGHT@\0"
|
||||
VALUE "OriginalFilename", "dusklight.exe\0"
|
||||
VALUE "ProductName", "@DUSK_PRODUCT_NAME@\0"
|
||||
VALUE "ProductVersion", "@DUSK_VERSION_STRING@\0"
|
||||
VALUE "ProductVersion", "@BOREALIS_APP_VERSION@\0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
@@ -25,11 +25,6 @@ endif ()
|
||||
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
# Version detection & version.h
|
||||
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/DetectVersion.cmake")
|
||||
detect_version()
|
||||
configure_version_header()
|
||||
|
||||
# Mod API and optional feature header surfaces
|
||||
include("${CMAKE_CURRENT_SOURCE_DIR}/../cmake/GameABIConfig.cmake")
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#include <memory>
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/os.h"
|
||||
#include "os_report.h"
|
||||
|
||||
aurora::Module Log("dusk::osReport");
|
||||
static constexpr borealis::Log Log{"dusk::osReport"};
|
||||
|
||||
bool dusk::OSReportReallyForceEnable = false;
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
#include "dusk/android_frame_rate.hpp"
|
||||
|
||||
#if defined(TARGET_ANDROID) || defined(__ANDROID__) || defined(ANDROID)
|
||||
#include "dusk/settings.h"
|
||||
|
||||
#include <SDL3/SDL_system.h>
|
||||
#include <jni.h>
|
||||
|
||||
namespace dusk::android {
|
||||
namespace {
|
||||
|
||||
float preferred_surface_frame_rate() {
|
||||
switch (getSettings().game.enableFrameInterpolation.getValue()) {
|
||||
case FrameInterpMode::Off:
|
||||
return 30.0f;
|
||||
case FrameInterpMode::Unlimited:
|
||||
default:
|
||||
return 0.0f;
|
||||
case FrameInterpMode::Capped:
|
||||
return static_cast<float>(getSettings().video.maxFrameRate.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
bool clear_pending_exception(JNIEnv* env) {
|
||||
if (env == nullptr || !env->ExceptionCheck()) {
|
||||
return false;
|
||||
}
|
||||
env->ExceptionClear();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void update_surface_frame_rate() {
|
||||
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 setPreferredFrameRate =
|
||||
env->GetMethodID(activityClass, "setPreferredSurfaceFrameRate", "(F)V");
|
||||
env->DeleteLocalRef(activityClass);
|
||||
if (setPreferredFrameRate == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(activity);
|
||||
return;
|
||||
}
|
||||
|
||||
jvalue args[1]{};
|
||||
args[0].f = preferred_surface_frame_rate();
|
||||
env->CallVoidMethodA(activity, setPreferredFrameRate, args);
|
||||
env->DeleteLocalRef(activity);
|
||||
clear_pending_exception(env);
|
||||
}
|
||||
|
||||
} // namespace dusk::android
|
||||
#else
|
||||
namespace dusk::android {
|
||||
void update_surface_frame_rate() {}
|
||||
} // namespace dusk::android
|
||||
#endif
|
||||
@@ -1,7 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk::android {
|
||||
|
||||
void update_surface_frame_rate();
|
||||
|
||||
} // namespace dusk::android
|
||||
+11
-11
@@ -1,6 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <borealis/app_info.hpp>
|
||||
|
||||
namespace dusk {
|
||||
/** Application identity fields for Borealis modules */
|
||||
inline constexpr borealis::AppInfo AppInfo{
|
||||
.orgName = "TwilitRealm",
|
||||
.appName = "Dusklight",
|
||||
.githubOwner = "TwilitRealm",
|
||||
.githubRepo = "dusklight",
|
||||
.discordApplicationId = "1495632471994405035",
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief The internal application name for the game.
|
||||
*
|
||||
@@ -8,15 +19,4 @@ namespace dusk {
|
||||
*/
|
||||
constexpr auto AppName = "Dusklight";
|
||||
|
||||
/**
|
||||
* Previous AppName to migrate data from.
|
||||
*/
|
||||
constexpr auto LegacyAppName = "Dusk";
|
||||
|
||||
/**
|
||||
* \brief The internal organization name for the game.
|
||||
*
|
||||
* This gets used for file paths and such, and cannot be changed!
|
||||
*/
|
||||
constexpr auto OrgName = "TwilitRealm";
|
||||
}
|
||||
|
||||
+4
-4
@@ -3,8 +3,8 @@
|
||||
#include "fmt/format.h"
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/io.hpp"
|
||||
#include <borealis/io.hpp>
|
||||
#include "dusk/settings.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -28,7 +28,7 @@ constexpr auto ConfigFileName = "config.json";
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
aurora::Module DuskConfigLog("dusk::config");
|
||||
constexpr borealis::Log DuskConfigLog{"dusk::config"};
|
||||
|
||||
absl::flat_hash_map<std::string, ConfigVarBase*> RegisteredConfigVars;
|
||||
absl::flat_hash_map<std::string, nlohmann::json> UnregisteredConfigVars;
|
||||
@@ -464,7 +464,7 @@ void load_from_user_preferences() {
|
||||
if (configJsonPath.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto configPathString = io::fs_path_to_string(configJsonPath);
|
||||
const auto configPathString = borealis::io::fs_path_to_string(configJsonPath);
|
||||
load_from_file_name(configPathString.c_str());
|
||||
}
|
||||
|
||||
@@ -532,7 +532,7 @@ void save() {
|
||||
if (configJsonPath.empty()) {
|
||||
return;
|
||||
}
|
||||
const auto configPathString = io::fs_path_to_string(configJsonPath);
|
||||
const auto configPathString = borealis::io::fs_path_to_string(configJsonPath);
|
||||
|
||||
DuskConfigLog.info("Saving config to '{}'", configPathString);
|
||||
|
||||
|
||||
@@ -1,965 +0,0 @@
|
||||
#if !defined(_WIN32) && !defined(_GNU_SOURCE)
|
||||
#define _GNU_SOURCE
|
||||
#endif
|
||||
|
||||
#include "dusk/crash_handler.h"
|
||||
|
||||
#include "dusk/logging.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <exception>
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <io.h>
|
||||
|
||||
#if defined(DUSK_CRASH_DBGHELP)
|
||||
#include <dbghelp.h>
|
||||
#endif
|
||||
|
||||
#else
|
||||
|
||||
#include <csignal>
|
||||
#include <cstdlib>
|
||||
#include <dlfcn.h>
|
||||
#include <sys/ucontext.h>
|
||||
#include <unistd.h>
|
||||
#include <unwind.h>
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <mach-o/dyld.h>
|
||||
#include <mach-o/loader.h>
|
||||
#include <TargetConditionals.h>
|
||||
#else
|
||||
#include <elf.h>
|
||||
#include <link.h>
|
||||
#ifndef NT_GNU_BUILD_ID
|
||||
#define NT_GNU_BUILD_ID 3
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
#ifndef DUSK_ARCH
|
||||
#define DUSK_ARCH "unknown"
|
||||
#endif
|
||||
|
||||
namespace dusk::crash_handler {
|
||||
namespace {
|
||||
|
||||
constexpr int kStderrFd = 2;
|
||||
constexpr int kMaxFrames = 128;
|
||||
constexpr char kHexDigits[] = "0123456789abcdef";
|
||||
|
||||
struct CrashContext {
|
||||
uintptr_t moduleBase = 0;
|
||||
char modulePath[1024] = {};
|
||||
uint8_t buildId[64] = {};
|
||||
unsigned buildIdLen = 0;
|
||||
unsigned pdbAge = 0;
|
||||
};
|
||||
CrashContext g_ctx;
|
||||
|
||||
struct ModuleInfo {
|
||||
uintptr_t base = 0;
|
||||
uintptr_t size = 0;
|
||||
char path[1024] = {};
|
||||
uint8_t buildId[64] = {};
|
||||
unsigned buildIdLen = 0;
|
||||
unsigned pdbAge = 0;
|
||||
};
|
||||
|
||||
void rawWrite(int fd, const char* data, size_t len) {
|
||||
if (fd < 0) {
|
||||
return;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
_write(fd, data, static_cast<unsigned int>(len));
|
||||
#else
|
||||
while (len > 0) {
|
||||
const ssize_t written = ::write(fd, data, len);
|
||||
if (written <= 0) {
|
||||
return;
|
||||
}
|
||||
data += written;
|
||||
len -= static_cast<size_t>(written);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void writeStr(int fd, const char* s) {
|
||||
if (s != nullptr) {
|
||||
rawWrite(fd, s, std::strlen(s));
|
||||
}
|
||||
}
|
||||
|
||||
void writeHex(int fd, unsigned long long value) {
|
||||
char buf[2 + 16];
|
||||
size_t o = sizeof(buf);
|
||||
do {
|
||||
buf[--o] = kHexDigits[value & 0xF];
|
||||
value >>= 4;
|
||||
} while (value != 0);
|
||||
buf[--o] = 'x';
|
||||
buf[--o] = '0';
|
||||
rawWrite(fd, buf + o, sizeof(buf) - o);
|
||||
}
|
||||
|
||||
void writeDec(int fd, unsigned int value) {
|
||||
char buf[10];
|
||||
size_t o = sizeof(buf);
|
||||
do {
|
||||
buf[--o] = static_cast<char>('0' + value % 10);
|
||||
value /= 10;
|
||||
} while (value != 0);
|
||||
rawWrite(fd, buf + o, sizeof(buf) - o);
|
||||
}
|
||||
|
||||
void writeHexBytes(int fd, const uint8_t* data, unsigned len) {
|
||||
char buf[2];
|
||||
for (unsigned i = 0; i < len; ++i) {
|
||||
buf[0] = kHexDigits[data[i] >> 4];
|
||||
buf[1] = kHexDigits[data[i] & 0xF];
|
||||
rawWrite(fd, buf, 2);
|
||||
}
|
||||
}
|
||||
|
||||
void writeHexByte(int fd, uint8_t value) {
|
||||
char buf[2];
|
||||
buf[0] = kHexDigits[value >> 4];
|
||||
buf[1] = kHexDigits[value & 0xF];
|
||||
rawWrite(fd, buf, 2);
|
||||
}
|
||||
|
||||
void writeQuoted(int fd, const char* s) {
|
||||
writeStr(fd, "\"");
|
||||
if (s != nullptr) {
|
||||
for (const char* p = s; *p != '\0'; ++p) {
|
||||
if (*p == '"' || *p == '\\') {
|
||||
rawWrite(fd, "\\", 1);
|
||||
}
|
||||
rawWrite(fd, p, 1);
|
||||
}
|
||||
}
|
||||
writeStr(fd, "\"");
|
||||
}
|
||||
|
||||
const char* baseName(const char* path) {
|
||||
const char* name = path;
|
||||
for (const char* p = path; p != nullptr && *p != '\0'; ++p) {
|
||||
if (*p == '/' || *p == '\\') {
|
||||
name = p + 1;
|
||||
}
|
||||
}
|
||||
return name[0] != '\0' ? name : "(unknown)";
|
||||
}
|
||||
|
||||
void writeBuildId(int fd, const uint8_t* buildId, unsigned buildIdLen, unsigned pdbAge) {
|
||||
if (buildIdLen == 0) {
|
||||
writeStr(fd, "(unavailable)");
|
||||
return;
|
||||
}
|
||||
#if defined(_WIN32)
|
||||
if (buildIdLen == 16) {
|
||||
writeHexByte(fd, buildId[3]);
|
||||
writeHexByte(fd, buildId[2]);
|
||||
writeHexByte(fd, buildId[1]);
|
||||
writeHexByte(fd, buildId[0]);
|
||||
writeStr(fd, "-");
|
||||
writeHexByte(fd, buildId[5]);
|
||||
writeHexByte(fd, buildId[4]);
|
||||
writeStr(fd, "-");
|
||||
writeHexByte(fd, buildId[7]);
|
||||
writeHexByte(fd, buildId[6]);
|
||||
writeStr(fd, "-");
|
||||
writeHexByte(fd, buildId[8]);
|
||||
writeHexByte(fd, buildId[9]);
|
||||
writeStr(fd, "-");
|
||||
writeHexBytes(fd, buildId + 10, 6);
|
||||
if (pdbAge != 0) {
|
||||
writeStr(fd, "-");
|
||||
writeDec(fd, pdbAge);
|
||||
}
|
||||
return;
|
||||
}
|
||||
#else
|
||||
(void)pdbAge;
|
||||
#endif
|
||||
writeHexBytes(fd, buildId, buildIdLen);
|
||||
}
|
||||
|
||||
const char* symbolFor(uintptr_t pc, unsigned long long* disp) {
|
||||
#if defined(_WIN32) && defined(DUSK_CRASH_DBGHELP)
|
||||
alignas(SYMBOL_INFO) static char storage[sizeof(SYMBOL_INFO) + 512];
|
||||
auto* sym = reinterpret_cast<SYMBOL_INFO*>(storage);
|
||||
sym->SizeOfStruct = sizeof(SYMBOL_INFO);
|
||||
sym->MaxNameLen = 511;
|
||||
DWORD64 d = 0;
|
||||
if (SymFromAddr(GetCurrentProcess(), pc, &d, sym)) {
|
||||
*disp = d;
|
||||
return sym->Name;
|
||||
}
|
||||
return nullptr;
|
||||
#elif defined(_WIN32)
|
||||
(void)pc;
|
||||
(void)disp;
|
||||
return nullptr;
|
||||
#else
|
||||
Dl_info info;
|
||||
if (dladdr(reinterpret_cast<void*>(pc), &info) != 0 && info.dli_sname != nullptr) {
|
||||
const auto base = reinterpret_cast<uintptr_t>(info.dli_saddr);
|
||||
*disp = pc >= base ? pc - base : 0;
|
||||
return info.dli_sname;
|
||||
}
|
||||
return nullptr;
|
||||
#endif
|
||||
}
|
||||
|
||||
void fallbackModuleInfo(ModuleInfo& info) {
|
||||
info = {};
|
||||
info.base = g_ctx.moduleBase;
|
||||
std::strncpy(info.path, g_ctx.modulePath, sizeof(info.path) - 1);
|
||||
if (g_ctx.buildIdLen > sizeof(info.buildId)) {
|
||||
info.buildIdLen = sizeof(info.buildId);
|
||||
} else {
|
||||
info.buildIdLen = g_ctx.buildIdLen;
|
||||
}
|
||||
if (info.buildIdLen != 0) {
|
||||
std::memcpy(info.buildId, g_ctx.buildId, info.buildIdLen);
|
||||
}
|
||||
info.pdbAge = g_ctx.pdbAge;
|
||||
}
|
||||
|
||||
bool findModuleInfo(uintptr_t pc, ModuleInfo& info);
|
||||
|
||||
void emitAddressDetail(int fd, uintptr_t pc) {
|
||||
ModuleInfo info;
|
||||
findModuleInfo(pc, info);
|
||||
const uintptr_t rva = pc >= info.base ? pc - info.base : 0ull;
|
||||
writeHex(fd, pc);
|
||||
writeStr(fd, " module_base=");
|
||||
writeHex(fd, info.base);
|
||||
if (info.size != 0) {
|
||||
writeStr(fd, " image_size=");
|
||||
writeHex(fd, info.size);
|
||||
}
|
||||
writeStr(fd, " rva=");
|
||||
writeHex(fd, rva);
|
||||
writeStr(fd, " module=");
|
||||
writeQuoted(fd, info.path[0] != '\0' ? info.path : baseName(g_ctx.modulePath));
|
||||
writeStr(fd, " build_id=");
|
||||
writeBuildId(fd, info.buildId, info.buildIdLen, info.pdbAge);
|
||||
}
|
||||
|
||||
void emitFrame(int fd, int index, uintptr_t pc) {
|
||||
ModuleInfo info;
|
||||
findModuleInfo(pc, info);
|
||||
const uintptr_t rva = pc >= info.base ? pc - info.base : 0ull;
|
||||
|
||||
writeStr(fd, "#");
|
||||
if (index < 10) {
|
||||
writeStr(fd, "0");
|
||||
}
|
||||
writeDec(fd, static_cast<unsigned int>(index));
|
||||
writeStr(fd, " abs=");
|
||||
writeHex(fd, pc);
|
||||
writeStr(fd, " module_base=");
|
||||
writeHex(fd, info.base);
|
||||
if (info.size != 0) {
|
||||
writeStr(fd, " image_size=");
|
||||
writeHex(fd, info.size);
|
||||
}
|
||||
writeStr(fd, " rva=");
|
||||
writeHex(fd, rva);
|
||||
writeStr(fd, " module=");
|
||||
writeQuoted(fd, info.path[0] != '\0' ? info.path : baseName(g_ctx.modulePath));
|
||||
writeStr(fd, " build_id=");
|
||||
writeBuildId(fd, info.buildId, info.buildIdLen, info.pdbAge);
|
||||
unsigned long long disp = 0;
|
||||
const char* sym = symbolFor(pc, &disp);
|
||||
if (sym != nullptr && sym[0] != '\0') {
|
||||
writeStr(fd, " ");
|
||||
writeStr(fd, sym);
|
||||
writeStr(fd, "+");
|
||||
writeHex(fd, disp);
|
||||
}
|
||||
writeStr(fd, "\n");
|
||||
}
|
||||
|
||||
void emitHeader(int fd, const char* reason, unsigned long long code, bool hasCode,
|
||||
uintptr_t faultAddr, uintptr_t crashPc, bool crashPcKnown) {
|
||||
writeStr(fd, "\n==================== DUSKLIGHT CRASHED ====================\n");
|
||||
writeStr(fd, "Build: " DUSK_WC_DESCRIBE " (" DUSK_WC_BRANCH ")\n");
|
||||
writeStr(fd, "Revision: " DUSK_WC_REVISION " Date: " DUSK_WC_DATE
|
||||
" Type: " DUSK_BUILD_TYPE "\n");
|
||||
writeStr(fd, "Platform: " DUSK_PLATFORM_NAME " / " DUSK_ARCH "\n");
|
||||
writeStr(fd, "Module: ");
|
||||
writeStr(fd, g_ctx.modulePath[0] != '\0' ? g_ctx.modulePath : "(unknown)");
|
||||
writeStr(fd, "\nModule base: ");
|
||||
writeHex(fd, g_ctx.moduleBase);
|
||||
writeStr(fd, "\nBuild-ID: ");
|
||||
writeBuildId(fd, g_ctx.buildId, g_ctx.buildIdLen, g_ctx.pdbAge);
|
||||
writeStr(fd, "\nReason: ");
|
||||
writeStr(fd, reason);
|
||||
if (hasCode) {
|
||||
writeStr(fd, " (");
|
||||
writeHex(fd, code);
|
||||
writeStr(fd, ")");
|
||||
}
|
||||
writeStr(fd, "\nFault addr: ");
|
||||
writeHex(fd, faultAddr);
|
||||
writeStr(fd, "\nCrash PC: ");
|
||||
if (crashPcKnown) {
|
||||
emitAddressDetail(fd, crashPc);
|
||||
} else {
|
||||
writeStr(fd, "(unavailable on this platform)");
|
||||
}
|
||||
writeStr(fd, "\n");
|
||||
writeStr(fd, "Backtrace:\n");
|
||||
}
|
||||
|
||||
void emitFooter(int fd) {
|
||||
writeStr(fd, "========================================================\n");
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
LONG g_inHandler = 0;
|
||||
LPTOP_LEVEL_EXCEPTION_FILTER g_prevFilter = nullptr;
|
||||
|
||||
bool readPeModuleInfo(uintptr_t moduleBase, ModuleInfo& info) {
|
||||
const auto* base = reinterpret_cast<const uint8_t*>(moduleBase);
|
||||
if (base == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(base);
|
||||
if (dos->e_magic != IMAGE_DOS_SIGNATURE) {
|
||||
return false;
|
||||
}
|
||||
const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(base + dos->e_lfanew);
|
||||
if (nt->Signature != IMAGE_NT_SIGNATURE) {
|
||||
return false;
|
||||
}
|
||||
info.base = moduleBase;
|
||||
info.size = nt->OptionalHeader.SizeOfImage;
|
||||
const IMAGE_DATA_DIRECTORY& dir =
|
||||
nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG];
|
||||
if (dir.VirtualAddress == 0 || dir.Size == 0) {
|
||||
return true;
|
||||
}
|
||||
const auto* dbg = reinterpret_cast<const IMAGE_DEBUG_DIRECTORY*>(base + dir.VirtualAddress);
|
||||
const unsigned count = dir.Size / sizeof(IMAGE_DEBUG_DIRECTORY);
|
||||
for (unsigned i = 0; i < count; ++i) {
|
||||
if (dbg[i].Type != IMAGE_DEBUG_TYPE_CODEVIEW) {
|
||||
continue;
|
||||
}
|
||||
const auto* cv = base + dbg[i].AddressOfRawData;
|
||||
if (std::memcmp(cv, "RSDS", 4) != 0) {
|
||||
continue;
|
||||
}
|
||||
std::memcpy(info.buildId, cv + 4, sizeof(GUID));
|
||||
info.buildIdLen = sizeof(GUID);
|
||||
std::memcpy(&info.pdbAge, cv + 4 + sizeof(GUID), sizeof(info.pdbAge));
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void captureBuildId() {
|
||||
ModuleInfo info;
|
||||
if (!readPeModuleInfo(g_ctx.moduleBase, info)) {
|
||||
return;
|
||||
}
|
||||
g_ctx.buildIdLen = info.buildIdLen;
|
||||
if (g_ctx.buildIdLen != 0) {
|
||||
std::memcpy(g_ctx.buildId, info.buildId, g_ctx.buildIdLen);
|
||||
}
|
||||
g_ctx.pdbAge = info.pdbAge;
|
||||
}
|
||||
|
||||
bool findModuleInfo(uintptr_t pc, ModuleInfo& info) {
|
||||
fallbackModuleInfo(info);
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
if (VirtualQuery(reinterpret_cast<LPCVOID>(pc), &mbi, sizeof(mbi)) == 0 ||
|
||||
mbi.AllocationBase == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const auto moduleBase = reinterpret_cast<uintptr_t>(mbi.AllocationBase);
|
||||
info = {};
|
||||
info.base = moduleBase;
|
||||
GetModuleFileNameA(reinterpret_cast<HMODULE>(moduleBase), info.path,
|
||||
static_cast<DWORD>(sizeof(info.path) - 1));
|
||||
readPeModuleInfo(moduleBase, info);
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* exceptionName(DWORD code) {
|
||||
switch (code) {
|
||||
case EXCEPTION_ACCESS_VIOLATION:
|
||||
return "EXCEPTION_ACCESS_VIOLATION";
|
||||
case EXCEPTION_ILLEGAL_INSTRUCTION:
|
||||
return "EXCEPTION_ILLEGAL_INSTRUCTION";
|
||||
case EXCEPTION_INT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_INT_DIVIDE_BY_ZERO";
|
||||
case EXCEPTION_STACK_OVERFLOW:
|
||||
return "EXCEPTION_STACK_OVERFLOW";
|
||||
case EXCEPTION_DATATYPE_MISALIGNMENT:
|
||||
return "EXCEPTION_DATATYPE_MISALIGNMENT";
|
||||
case EXCEPTION_FLT_DIVIDE_BY_ZERO:
|
||||
return "EXCEPTION_FLT_DIVIDE_BY_ZERO";
|
||||
default:
|
||||
return "EXCEPTION";
|
||||
}
|
||||
}
|
||||
|
||||
int captureBacktraceWin(CONTEXT ctx, uintptr_t* out, int cap) {
|
||||
int n = 0;
|
||||
while (n < cap) {
|
||||
#if defined(_M_X64)
|
||||
const DWORD64 ip = ctx.Rip;
|
||||
#elif defined(_M_ARM64)
|
||||
const DWORD64 ip = ctx.Pc;
|
||||
#else
|
||||
const DWORD64 ip = 0;
|
||||
#endif
|
||||
if (ip == 0) {
|
||||
break;
|
||||
}
|
||||
out[n++] = static_cast<uintptr_t>(ip);
|
||||
#if defined(_M_X64) || defined(_M_ARM64)
|
||||
DWORD64 imageBase = 0;
|
||||
PRUNTIME_FUNCTION fn = RtlLookupFunctionEntry(ip, &imageBase, nullptr);
|
||||
if (fn != nullptr) {
|
||||
PVOID handlerData = nullptr;
|
||||
DWORD64 establisherFrame = 0;
|
||||
RtlVirtualUnwind(UNW_FLAG_NHANDLER, imageBase, ip, fn, &ctx, &handlerData,
|
||||
&establisherFrame, nullptr);
|
||||
continue;
|
||||
}
|
||||
#if defined(_M_X64)
|
||||
if (ctx.Rsp == 0) {
|
||||
break;
|
||||
}
|
||||
ctx.Rip = *reinterpret_cast<const DWORD64*>(ctx.Rsp);
|
||||
ctx.Rsp += sizeof(DWORD64);
|
||||
#else
|
||||
if (ctx.Lr == 0 || ctx.Lr == ip) {
|
||||
break;
|
||||
}
|
||||
ctx.Pc = ctx.Lr;
|
||||
ctx.Lr = 0;
|
||||
#endif
|
||||
#else
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
void emit(int fd, EXCEPTION_POINTERS* ep) {
|
||||
if (fd < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const DWORD code = ep->ExceptionRecord->ExceptionCode;
|
||||
const uintptr_t pc = reinterpret_cast<uintptr_t>(ep->ExceptionRecord->ExceptionAddress);
|
||||
uintptr_t faultAddr = 0;
|
||||
if (code == EXCEPTION_ACCESS_VIOLATION && ep->ExceptionRecord->NumberParameters >= 2) {
|
||||
faultAddr = static_cast<uintptr_t>(ep->ExceptionRecord->ExceptionInformation[1]);
|
||||
}
|
||||
|
||||
emitHeader(fd, exceptionName(code), code, true, faultAddr, pc, true);
|
||||
|
||||
uintptr_t frames[kMaxFrames];
|
||||
const int frameCount = captureBacktraceWin(*ep->ContextRecord, frames, kMaxFrames);
|
||||
for (int i = 0; i < frameCount; ++i) {
|
||||
emitFrame(fd, i, frames[i]);
|
||||
}
|
||||
|
||||
emitFooter(fd);
|
||||
}
|
||||
|
||||
LONG WINAPI windowsHandler(EXCEPTION_POINTERS* ep) {
|
||||
if (InterlockedCompareExchange(&g_inHandler, 1, 0) != 0) {
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
emit(kStderrFd, ep);
|
||||
const int logFd = dusk::GetLogFileDescriptor();
|
||||
if (logFd >= 0) {
|
||||
emit(logFd, ep);
|
||||
}
|
||||
if (g_prevFilter != nullptr) {
|
||||
return g_prevFilter(ep);
|
||||
}
|
||||
return EXCEPTION_CONTINUE_SEARCH;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
constexpr int kSignals[] = {SIGSEGV, SIGBUS, SIGABRT, SIGILL, SIGFPE};
|
||||
constexpr int kSignalCount = static_cast<int>(sizeof(kSignals) / sizeof(kSignals[0]));
|
||||
constexpr int kAltStackSize = 128 * 1024;
|
||||
|
||||
volatile std::sig_atomic_t g_inHandler = 0;
|
||||
char g_altStack[kAltStackSize];
|
||||
struct sigaction g_prev[kSignalCount];
|
||||
std::terminate_handler g_prevTerminate = nullptr;
|
||||
|
||||
void crashRegs(void* ucv, uintptr_t& pc, uintptr_t& lr, uintptr_t& fp) {
|
||||
pc = 0;
|
||||
lr = 0;
|
||||
fp = 0;
|
||||
if (ucv == nullptr) {
|
||||
return;
|
||||
}
|
||||
auto* uc = static_cast<ucontext_t*>(ucv);
|
||||
#if defined(__APPLE__)
|
||||
#if defined(__aarch64__) || defined(__arm64__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext->__ss.__pc);
|
||||
lr = static_cast<uintptr_t>(uc->uc_mcontext->__ss.__lr);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext->__ss.__fp);
|
||||
#elif defined(__x86_64__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext->__ss.__rip);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext->__ss.__rbp);
|
||||
#endif
|
||||
#elif defined(__ANDROID__)
|
||||
#if defined(__aarch64__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext.pc);
|
||||
lr = static_cast<uintptr_t>(uc->uc_mcontext.regs[30]);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext.regs[29]);
|
||||
#elif defined(__x86_64__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RIP]);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RBP]);
|
||||
#elif defined(__arm__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext.arm_pc);
|
||||
lr = static_cast<uintptr_t>(uc->uc_mcontext.arm_lr);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext.arm_fp);
|
||||
#elif defined(__i386__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_EIP]);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_EBP]);
|
||||
#endif
|
||||
#elif defined(__linux__)
|
||||
#if defined(__x86_64__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RIP]);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_RBP]);
|
||||
#elif defined(__aarch64__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext.pc);
|
||||
lr = static_cast<uintptr_t>(uc->uc_mcontext.regs[30]);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext.regs[29]);
|
||||
#elif defined(__i386__)
|
||||
pc = static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_EIP]);
|
||||
fp = static_cast<uintptr_t>(uc->uc_mcontext.gregs[REG_EBP]);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
bool pcNearFunctionEntry(uintptr_t pc) {
|
||||
constexpr uintptr_t kPrologueWindow = 20;
|
||||
Dl_info info;
|
||||
if (dladdr(reinterpret_cast<void*>(pc), &info) == 0 || info.dli_saddr == nullptr) {
|
||||
return false;
|
||||
}
|
||||
const auto start = reinterpret_cast<uintptr_t>(info.dli_saddr);
|
||||
return pc >= start && pc - start <= kPrologueWindow;
|
||||
}
|
||||
|
||||
int captureBacktraceFP(uintptr_t pc, uintptr_t lr, uintptr_t fp, uintptr_t* out, int cap) {
|
||||
int n = 0;
|
||||
if (pc != 0 && n < cap) {
|
||||
out[n++] = pc;
|
||||
}
|
||||
bool dedupeLr = false;
|
||||
if (lr != 0 && lr != pc && n < cap && pcNearFunctionEntry(pc)) {
|
||||
out[n++] = lr;
|
||||
dedupeLr = true;
|
||||
}
|
||||
uintptr_t cur = fp;
|
||||
uintptr_t prev = 0;
|
||||
constexpr uintptr_t kMaxFrameSpan = 16u << 20;
|
||||
while (n < cap) {
|
||||
if (cur == 0 || (cur & (sizeof(uintptr_t) - 1)) != 0 || cur <= prev) {
|
||||
break;
|
||||
}
|
||||
const auto* slot = reinterpret_cast<const uintptr_t*>(cur);
|
||||
const uintptr_t next = slot[0];
|
||||
const uintptr_t ret = slot[1];
|
||||
if (ret == 0) {
|
||||
break;
|
||||
}
|
||||
const bool skip = dedupeLr && ret == lr;
|
||||
dedupeLr = false;
|
||||
if (!skip) {
|
||||
out[n++] = ret;
|
||||
}
|
||||
if (next != 0 && next > cur && next - cur > kMaxFrameSpan) {
|
||||
break;
|
||||
}
|
||||
prev = cur;
|
||||
cur = next;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
struct UnwindState {
|
||||
uintptr_t* pcs;
|
||||
int count;
|
||||
int cap;
|
||||
int skip;
|
||||
};
|
||||
|
||||
_Unwind_Reason_Code unwindCb(struct _Unwind_Context* ctx, void* arg) {
|
||||
auto* s = static_cast<UnwindState*>(arg);
|
||||
const uintptr_t ip = static_cast<uintptr_t>(_Unwind_GetIP(ctx));
|
||||
if (ip == 0) {
|
||||
return _URC_END_OF_STACK;
|
||||
}
|
||||
if (s->skip > 0) {
|
||||
--s->skip;
|
||||
return _URC_NO_REASON;
|
||||
}
|
||||
if (s->count >= s->cap) {
|
||||
return _URC_END_OF_STACK;
|
||||
}
|
||||
s->pcs[s->count++] = ip;
|
||||
return _URC_NO_REASON;
|
||||
}
|
||||
|
||||
int captureBacktrace(uintptr_t* pcs, int cap, int skip) {
|
||||
UnwindState s{pcs, 0, cap, skip};
|
||||
_Unwind_Backtrace(&unwindCb, &s);
|
||||
return s.count;
|
||||
}
|
||||
|
||||
void prewarmUnwinder() {
|
||||
uintptr_t warm[4];
|
||||
captureBacktrace(warm, 4, 0);
|
||||
}
|
||||
|
||||
#if defined(__APPLE__)
|
||||
|
||||
bool readMachBuildId(uintptr_t moduleBase, ModuleInfo& info) {
|
||||
const auto* header = reinterpret_cast<const struct mach_header_64*>(moduleBase);
|
||||
if (header == nullptr || header->magic != MH_MAGIC_64) {
|
||||
return false;
|
||||
}
|
||||
const auto* lc = reinterpret_cast<const struct load_command*>(
|
||||
reinterpret_cast<const char*>(header) + sizeof(struct mach_header_64));
|
||||
for (uint32_t i = 0; i < header->ncmds; ++i) {
|
||||
if (lc->cmd == LC_UUID) {
|
||||
const auto* uuid = reinterpret_cast<const struct uuid_command*>(lc);
|
||||
std::memcpy(info.buildId, uuid->uuid, sizeof(uuid->uuid));
|
||||
info.buildIdLen = sizeof(uuid->uuid);
|
||||
return true;
|
||||
}
|
||||
lc = reinterpret_cast<const struct load_command*>(
|
||||
reinterpret_cast<const char*>(lc) + lc->cmdsize);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void captureBuildId() {
|
||||
ModuleInfo info;
|
||||
if (!readMachBuildId(g_ctx.moduleBase, info)) {
|
||||
return;
|
||||
}
|
||||
g_ctx.buildIdLen = info.buildIdLen;
|
||||
if (g_ctx.buildIdLen != 0) {
|
||||
std::memcpy(g_ctx.buildId, info.buildId, g_ctx.buildIdLen);
|
||||
}
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
bool segmentContains(const dl_phdr_info* info, uintptr_t addr) {
|
||||
for (int i = 0; i < info->dlpi_phnum; ++i) {
|
||||
const ElfW(Phdr)& ph = info->dlpi_phdr[i];
|
||||
if (ph.p_type != PT_LOAD) {
|
||||
continue;
|
||||
}
|
||||
const uintptr_t start = info->dlpi_addr + ph.p_vaddr;
|
||||
if (addr >= start && addr < start + ph.p_memsz) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void readElfModuleInfo(const dl_phdr_info* info, ModuleInfo& module) {
|
||||
uintptr_t minAddr = ~static_cast<uintptr_t>(0);
|
||||
uintptr_t maxAddr = 0;
|
||||
for (int i = 0; i < info->dlpi_phnum; ++i) {
|
||||
const ElfW(Phdr)& ph = info->dlpi_phdr[i];
|
||||
if (ph.p_type != PT_LOAD) {
|
||||
continue;
|
||||
}
|
||||
const uintptr_t start = info->dlpi_addr + ph.p_vaddr;
|
||||
const uintptr_t end = start + ph.p_memsz;
|
||||
if (start < minAddr) {
|
||||
minAddr = start;
|
||||
}
|
||||
if (end > maxAddr) {
|
||||
maxAddr = end;
|
||||
}
|
||||
}
|
||||
if (minAddr <= maxAddr && maxAddr != 0) {
|
||||
module.base = minAddr;
|
||||
module.size = maxAddr - minAddr;
|
||||
}
|
||||
|
||||
for (int i = 0; i < info->dlpi_phnum; ++i) {
|
||||
const ElfW(Phdr)& ph = info->dlpi_phdr[i];
|
||||
if (ph.p_type != PT_NOTE) {
|
||||
continue;
|
||||
}
|
||||
const auto* p = reinterpret_cast<const uint8_t*>(info->dlpi_addr + ph.p_vaddr);
|
||||
const uint8_t* end = p + ph.p_memsz;
|
||||
while (p + sizeof(ElfW(Nhdr)) <= end) {
|
||||
const auto* nh = reinterpret_cast<const ElfW(Nhdr)*>(p);
|
||||
const char* name = reinterpret_cast<const char*>(nh + 1);
|
||||
const uint8_t* desc =
|
||||
reinterpret_cast<const uint8_t*>(name + ((nh->n_namesz + 3) & ~3u));
|
||||
if (nh->n_type == NT_GNU_BUILD_ID && nh->n_namesz == 4 &&
|
||||
std::memcmp(name, "GNU", 4) == 0) {
|
||||
unsigned n = nh->n_descsz;
|
||||
if (n > sizeof(module.buildId)) {
|
||||
n = sizeof(module.buildId);
|
||||
}
|
||||
std::memcpy(module.buildId, desc, n);
|
||||
module.buildIdLen = n;
|
||||
return;
|
||||
}
|
||||
p = desc + ((nh->n_descsz + 3) & ~3u);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int elfBuildIdCallback(dl_phdr_info* info, size_t, void* arg) {
|
||||
const auto self = *static_cast<const uintptr_t*>(arg);
|
||||
if (!segmentContains(info, self)) {
|
||||
return 0;
|
||||
}
|
||||
ModuleInfo module;
|
||||
readElfModuleInfo(info, module);
|
||||
g_ctx.buildIdLen = module.buildIdLen;
|
||||
if (g_ctx.buildIdLen != 0) {
|
||||
std::memcpy(g_ctx.buildId, module.buildId, g_ctx.buildIdLen);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
void captureBuildId() {
|
||||
uintptr_t self = reinterpret_cast<uintptr_t>(&install);
|
||||
dl_iterate_phdr(&elfBuildIdCallback, &self);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if !defined(__APPLE__)
|
||||
struct ElfModuleSearch {
|
||||
uintptr_t pc;
|
||||
ModuleInfo* module;
|
||||
};
|
||||
|
||||
int elfModuleInfoCallback(dl_phdr_info* info, size_t, void* arg) {
|
||||
auto* search = static_cast<ElfModuleSearch*>(arg);
|
||||
if (!segmentContains(info, search->pc)) {
|
||||
return 0;
|
||||
}
|
||||
if (info->dlpi_name != nullptr && info->dlpi_name[0] != '\0') {
|
||||
std::strncpy(search->module->path, info->dlpi_name,
|
||||
sizeof(search->module->path) - 1);
|
||||
}
|
||||
readElfModuleInfo(info, *search->module);
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
bool findModuleInfo(uintptr_t pc, ModuleInfo& info) {
|
||||
fallbackModuleInfo(info);
|
||||
Dl_info moduleInfo;
|
||||
if (dladdr(reinterpret_cast<void*>(pc), &moduleInfo) == 0) {
|
||||
return false;
|
||||
}
|
||||
if (moduleInfo.dli_fbase != nullptr) {
|
||||
info.base = reinterpret_cast<uintptr_t>(moduleInfo.dli_fbase);
|
||||
}
|
||||
if (moduleInfo.dli_fname != nullptr && moduleInfo.dli_fname[0] != '\0') {
|
||||
info.path[0] = '\0';
|
||||
std::strncpy(info.path, moduleInfo.dli_fname, sizeof(info.path) - 1);
|
||||
}
|
||||
info.buildIdLen = 0;
|
||||
info.pdbAge = 0;
|
||||
#if defined(__APPLE__)
|
||||
readMachBuildId(info.base, info);
|
||||
#else
|
||||
ElfModuleSearch search{pc, &info};
|
||||
dl_iterate_phdr(&elfModuleInfoCallback, &search);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
const char* signalName(int sig) {
|
||||
switch (sig) {
|
||||
case SIGSEGV:
|
||||
return "SIGSEGV (segmentation fault)";
|
||||
case SIGBUS:
|
||||
return "SIGBUS (bus error)";
|
||||
case SIGABRT:
|
||||
return "SIGABRT (abort)";
|
||||
case SIGILL:
|
||||
return "SIGILL (illegal instruction)";
|
||||
case SIGFPE:
|
||||
return "SIGFPE (floating point exception)";
|
||||
default:
|
||||
return "unknown signal";
|
||||
}
|
||||
}
|
||||
|
||||
void emit(int fd, int sig, siginfo_t* info, const uintptr_t* frames, int frameCount,
|
||||
uintptr_t pc) {
|
||||
if (fd < 0) {
|
||||
return;
|
||||
}
|
||||
const uintptr_t faultAddr =
|
||||
info != nullptr ? reinterpret_cast<uintptr_t>(info->si_addr) : 0;
|
||||
emitHeader(fd, signalName(sig), 0, false, faultAddr, pc, pc != 0);
|
||||
for (int i = 0; i < frameCount; ++i) {
|
||||
emitFrame(fd, i, frames[i]);
|
||||
}
|
||||
emitFooter(fd);
|
||||
}
|
||||
|
||||
void chainPrevious(int sig, siginfo_t* info, void* uc) {
|
||||
for (int i = 0; i < kSignalCount; ++i) {
|
||||
if (kSignals[i] != sig) {
|
||||
continue;
|
||||
}
|
||||
const struct sigaction& o = g_prev[i];
|
||||
if ((o.sa_flags & SA_SIGINFO) != 0) {
|
||||
if (o.sa_sigaction != nullptr) {
|
||||
o.sa_sigaction(sig, info, uc);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (o.sa_handler == SIG_IGN) {
|
||||
return;
|
||||
}
|
||||
if (o.sa_handler != SIG_DFL && o.sa_handler != nullptr) {
|
||||
o.sa_handler(sig);
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
::signal(sig, SIG_DFL);
|
||||
::raise(sig);
|
||||
}
|
||||
|
||||
void handler(int sig, siginfo_t* info, void* ucv) {
|
||||
if (g_inHandler != 0) {
|
||||
_exit(128 + sig);
|
||||
}
|
||||
g_inHandler = 1;
|
||||
|
||||
uintptr_t pc = 0;
|
||||
uintptr_t lr = 0;
|
||||
uintptr_t fp = 0;
|
||||
crashRegs(ucv, pc, lr, fp);
|
||||
uintptr_t frames[kMaxFrames];
|
||||
int frameCount = captureBacktraceFP(pc, lr, fp, frames, kMaxFrames);
|
||||
if (frameCount < 2) {
|
||||
frameCount = captureBacktrace(frames, kMaxFrames, 2);
|
||||
}
|
||||
|
||||
emit(kStderrFd, sig, info, frames, frameCount, pc);
|
||||
const int logFd = dusk::GetLogFileDescriptor();
|
||||
if (logFd >= 0) {
|
||||
emit(logFd, sig, info, frames, frameCount, pc);
|
||||
::fsync(logFd);
|
||||
}
|
||||
|
||||
chainPrevious(sig, info, ucv);
|
||||
}
|
||||
|
||||
void writeTerminateMessage(int fd, const char* body, const char* what) {
|
||||
writeStr(fd, "\nterminate: ");
|
||||
writeStr(fd, body);
|
||||
writeStr(fd, what);
|
||||
writeStr(fd, "\n");
|
||||
}
|
||||
|
||||
void onTerminate() {
|
||||
const char* body = "unknown reason";
|
||||
const char* what = nullptr;
|
||||
if (std::exception_ptr ep = std::current_exception()) {
|
||||
try {
|
||||
std::rethrow_exception(ep);
|
||||
} catch (const std::exception& e) {
|
||||
body = "uncaught exception: ";
|
||||
what = e.what();
|
||||
} catch (...) {
|
||||
body = "uncaught non-std exception";
|
||||
}
|
||||
} else {
|
||||
body = "no active exception";
|
||||
}
|
||||
writeTerminateMessage(kStderrFd, body, what);
|
||||
writeTerminateMessage(dusk::GetLogFileDescriptor(), body, what);
|
||||
if (g_prevTerminate != nullptr) {
|
||||
g_prevTerminate();
|
||||
}
|
||||
std::abort();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void install() {
|
||||
#if defined(_WIN32)
|
||||
g_ctx.moduleBase = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
|
||||
GetModuleFileNameA(nullptr, g_ctx.modulePath, sizeof(g_ctx.modulePath) - 1);
|
||||
captureBuildId();
|
||||
#if defined(DUSK_CRASH_DBGHELP)
|
||||
SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS | SYMOPT_LOAD_LINES);
|
||||
SymInitialize(GetCurrentProcess(), nullptr, TRUE);
|
||||
#endif
|
||||
g_prevFilter = SetUnhandledExceptionFilter(&windowsHandler);
|
||||
#elif !defined(__APPLE__) || !TARGET_OS_TV
|
||||
Dl_info moduleInfo;
|
||||
if (dladdr(reinterpret_cast<void*>(&install), &moduleInfo) != 0) {
|
||||
g_ctx.moduleBase = reinterpret_cast<uintptr_t>(moduleInfo.dli_fbase);
|
||||
if (moduleInfo.dli_fname != nullptr) {
|
||||
std::strncpy(g_ctx.modulePath, moduleInfo.dli_fname,
|
||||
sizeof(g_ctx.modulePath) - 1);
|
||||
}
|
||||
}
|
||||
captureBuildId();
|
||||
prewarmUnwinder();
|
||||
|
||||
static stack_t altStack;
|
||||
altStack.ss_sp = g_altStack;
|
||||
altStack.ss_size = sizeof(g_altStack);
|
||||
altStack.ss_flags = 0;
|
||||
sigaltstack(&altStack, nullptr);
|
||||
|
||||
struct sigaction sa;
|
||||
std::memset(&sa, 0, sizeof(sa));
|
||||
sa.sa_sigaction = &handler;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
|
||||
|
||||
for (int i = 0; i < kSignalCount; ++i) {
|
||||
sigaction(kSignals[i], &sa, &g_prev[i]);
|
||||
}
|
||||
|
||||
g_prevTerminate = std::set_terminate(&onTerminate);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace dusk::crash_handler
|
||||
@@ -1,7 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk::crash_handler {
|
||||
|
||||
void install();
|
||||
|
||||
} // namespace dusk::crash_handler
|
||||
@@ -1,188 +0,0 @@
|
||||
#include "dusk/crash_reporting.h"
|
||||
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/dusk.h"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/main.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
#include <sentry.h>
|
||||
#endif
|
||||
|
||||
namespace dusk::crash_reporting {
|
||||
|
||||
namespace {
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
bool g_sentryInitialized = false;
|
||||
|
||||
bool truthy(std::string_view value) {
|
||||
return value == "1" || value == "true" || value == "TRUE" || value == "yes" || value == "YES" ||
|
||||
value == "on" || value == "ON";
|
||||
}
|
||||
|
||||
std::string env_or_empty(const char* name) {
|
||||
if (const char* value = std::getenv(name)) {
|
||||
return value;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool disabled_by_env() {
|
||||
const std::string env = env_or_empty("DUSK_SENTRY_ENABLED");
|
||||
return !env.empty() && !truthy(env);
|
||||
}
|
||||
|
||||
std::string effective_dsn() {
|
||||
const std::string env = env_or_empty("DUSK_SENTRY_DSN");
|
||||
if (!env.empty()) {
|
||||
return env;
|
||||
}
|
||||
return DUSK_SENTRY_DSN;
|
||||
}
|
||||
|
||||
bool effective_debug() {
|
||||
const std::string env = env_or_empty("DUSK_SENTRY_DEBUG");
|
||||
if (!env.empty()) {
|
||||
return truthy(env);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string release_name() {
|
||||
return std::string(AppName) + "@" DUSK_WC_DESCRIBE;
|
||||
}
|
||||
|
||||
std::filesystem::path sentry_database_path() {
|
||||
return dusk::CachePath / "sentry";
|
||||
}
|
||||
|
||||
std::filesystem::path log_attachment_path() {
|
||||
if (const char* logPath = GetLogFilePath()) {
|
||||
return logPath;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void configure_path_options(sentry_options_t* options) {
|
||||
const auto databasePath = sentry_database_path();
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(databasePath, ec);
|
||||
if (ec) {
|
||||
DuskLog.warn(
|
||||
"Unable to create Sentry database path '{}': {}", databasePath.string(), ec.message());
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
const std::wstring databasePathWide = databasePath.wstring();
|
||||
sentry_options_set_database_pathw(options, databasePathWide.c_str());
|
||||
#else
|
||||
const std::string databasePathUtf8 = databasePath.string();
|
||||
sentry_options_set_database_path(options, databasePathUtf8.c_str());
|
||||
#endif
|
||||
|
||||
const auto logPath = log_attachment_path();
|
||||
if (!logPath.empty()) {
|
||||
#if _WIN32
|
||||
sentry_options_add_attachmentw(options, logPath.wstring().c_str());
|
||||
#else
|
||||
sentry_options_add_attachment(options, logPath.string().c_str());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void initialize() {
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
if (g_sentryInitialized || disabled_by_env()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string dsn = effective_dsn();
|
||||
if (dsn.empty()) {
|
||||
DuskLog.warn("Crash reporting is enabled but no Sentry DSN is configured");
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string release = release_name();
|
||||
|
||||
sentry_options_t* options = sentry_options_new();
|
||||
sentry_options_set_dsn(options, dsn.c_str());
|
||||
sentry_options_set_release(options, release.c_str());
|
||||
sentry_options_set_environment(options, DUSK_SENTRY_ENVIRONMENT);
|
||||
sentry_options_set_debug(options, effective_debug() ? 1 : 0);
|
||||
sentry_options_set_require_user_consent(options, 1);
|
||||
sentry_options_set_cache_keep(options, 1);
|
||||
sentry_options_set_max_breadcrumbs(options, 100);
|
||||
configure_path_options(options);
|
||||
|
||||
if (sentry_init(options) != 0) {
|
||||
DuskLog.warn("Failed to initialize Sentry crash reporting");
|
||||
return;
|
||||
}
|
||||
|
||||
sentry_set_tag("git_branch", DUSK_WC_BRANCH);
|
||||
sentry_set_tag("build_type", DUSK_BUILD_TYPE);
|
||||
g_sentryInitialized = true;
|
||||
|
||||
DuskLog.info("Initialized Sentry crash reporting");
|
||||
#endif
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
if (!g_sentryInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
sentry_close();
|
||||
g_sentryInitialized = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
Consent get_consent() {
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
if (!g_sentryInitialized) {
|
||||
return Consent::Unavailable;
|
||||
}
|
||||
|
||||
switch (sentry_user_consent_get()) {
|
||||
case SENTRY_USER_CONSENT_GIVEN:
|
||||
return Consent::Given;
|
||||
case SENTRY_USER_CONSENT_REVOKED:
|
||||
return Consent::Revoked;
|
||||
case SENTRY_USER_CONSENT_UNKNOWN:
|
||||
default:
|
||||
return Consent::Unknown;
|
||||
}
|
||||
#else
|
||||
return Consent::Unavailable;
|
||||
#endif
|
||||
}
|
||||
|
||||
void set_consent(bool enabled) {
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
if (!g_sentryInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (enabled) {
|
||||
sentry_user_consent_give();
|
||||
} else {
|
||||
sentry_user_consent_revoke();
|
||||
}
|
||||
#else
|
||||
(void)enabled;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace dusk::crash_reporting
|
||||
@@ -1,17 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk::crash_reporting {
|
||||
|
||||
enum class Consent {
|
||||
Unavailable,
|
||||
Unknown,
|
||||
Given,
|
||||
Revoked,
|
||||
};
|
||||
|
||||
void initialize();
|
||||
void shutdown();
|
||||
Consent get_consent();
|
||||
void set_consent(bool enabled);
|
||||
|
||||
} // namespace dusk::crash_reporting
|
||||
+129
-991
File diff suppressed because it is too large
Load Diff
+5
-11
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <borealis/data.hpp>
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
@@ -15,20 +17,12 @@
|
||||
#define DUSK_CAN_OPEN_DATA_FOLDER 0
|
||||
#endif
|
||||
|
||||
#if (defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST)
|
||||
#define DUSK_CAN_CHANGE_DATA_FOLDER 0
|
||||
#else
|
||||
#define DUSK_CAN_CHANGE_DATA_FOLDER 1
|
||||
#endif
|
||||
|
||||
namespace dusk::data {
|
||||
|
||||
struct Paths {
|
||||
std::filesystem::path userPath;
|
||||
std::filesystem::path cachePath;
|
||||
};
|
||||
using Paths = borealis::data::Paths;
|
||||
|
||||
Paths initialize_data();
|
||||
borealis::data::Manager& manager();
|
||||
Paths initialize_data(const std::filesystem::path& userDirectoryOverride = {});
|
||||
std::filesystem::path base_path_relative(const std::filesystem::path& path);
|
||||
std::filesystem::path configured_data_path();
|
||||
std::filesystem::path cache_path();
|
||||
|
||||
@@ -1,867 +0,0 @@
|
||||
#ifdef DUSK_DISCORD
|
||||
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
|
||||
#include "discord.hpp"
|
||||
|
||||
#include "dusk/logging.h"
|
||||
#include "nlohmann/json.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#ifdef _WIN32
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#define NOMCX
|
||||
#define NOSERVICE
|
||||
#define NOIME
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <fcntl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace dusk::discord::rpc {
|
||||
namespace {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
constexpr uint32_t kRpcVersion = 1;
|
||||
constexpr size_t kFrameHeaderSize = sizeof(uint32_t) * 2;
|
||||
constexpr size_t kMaxFramePayloadSize = 64 * 1024;
|
||||
constexpr auto kIoWait = std::chrono::milliseconds(500);
|
||||
constexpr auto kShutdownClearTimeout = std::chrono::milliseconds(200);
|
||||
constexpr auto kInitialReconnectDelay = std::chrono::milliseconds(500);
|
||||
constexpr auto kMaxReconnectDelay = std::chrono::milliseconds(60 * 1000);
|
||||
|
||||
enum class Opcode : uint32_t {
|
||||
Handshake = 0,
|
||||
Frame = 1,
|
||||
Close = 2,
|
||||
Ping = 3,
|
||||
Pong = 4,
|
||||
};
|
||||
|
||||
enum class ConnectionState {
|
||||
Disconnected,
|
||||
SentHandshake,
|
||||
Connected,
|
||||
};
|
||||
|
||||
enum class DisconnectCode : int {
|
||||
PipeClosed = 1,
|
||||
ReadCorrupt = 2,
|
||||
BadFrame = 3,
|
||||
};
|
||||
|
||||
struct Frame {
|
||||
Opcode opcode = Opcode::Frame;
|
||||
std::string payload;
|
||||
};
|
||||
|
||||
struct QueuedEvent {
|
||||
enum class Type {
|
||||
Ready,
|
||||
Disconnected,
|
||||
Error,
|
||||
};
|
||||
|
||||
Type type = Type::Ready;
|
||||
User user;
|
||||
int code = 0;
|
||||
std::string message;
|
||||
};
|
||||
|
||||
class Backoff {
|
||||
public:
|
||||
std::chrono::milliseconds next_delay() {
|
||||
const auto delay = currentDelay;
|
||||
currentDelay = std::min(currentDelay * 2, kMaxReconnectDelay);
|
||||
return delay;
|
||||
}
|
||||
|
||||
void reset() { currentDelay = kInitialReconnectDelay; }
|
||||
|
||||
private:
|
||||
std::chrono::milliseconds currentDelay = kInitialReconnectDelay;
|
||||
};
|
||||
|
||||
class IpcConnection {
|
||||
public:
|
||||
IpcConnection() = default;
|
||||
~IpcConnection() { close(); }
|
||||
|
||||
IpcConnection(const IpcConnection&) = delete;
|
||||
IpcConnection& operator=(const IpcConnection&) = delete;
|
||||
|
||||
bool open() {
|
||||
#ifdef _WIN32
|
||||
wchar_t pipeName[] = L"\\\\?\\pipe\\discord-ipc-0";
|
||||
constexpr size_t kPipeDigit = sizeof(pipeName) / sizeof(wchar_t) - 2;
|
||||
|
||||
for (wchar_t pipeNumber = L'0'; pipeNumber <= L'9'; ++pipeNumber) {
|
||||
pipeName[kPipeDigit] = pipeNumber;
|
||||
pipe = CreateFileW(
|
||||
pipeName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
if (pipe != INVALID_HANDLE_VALUE) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (GetLastError() == ERROR_PIPE_BUSY && WaitNamedPipeW(pipeName, 10000)) {
|
||||
--pipeNumber;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
#else
|
||||
const auto tempPaths = get_temp_paths();
|
||||
for (const std::string& tempPath : tempPaths) {
|
||||
for (int pipeNumber = 0; pipeNumber < 10; ++pipeNumber) {
|
||||
socketFd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (socketFd == -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sockaddr_un pipeAddress{};
|
||||
pipeAddress.sun_family = AF_UNIX;
|
||||
const std::string socketPath =
|
||||
tempPath + "/discord-ipc-" + std::to_string(pipeNumber);
|
||||
if (socketPath.size() >= sizeof(pipeAddress.sun_path)) {
|
||||
close();
|
||||
continue;
|
||||
}
|
||||
|
||||
std::strncpy(
|
||||
pipeAddress.sun_path, socketPath.c_str(), sizeof(pipeAddress.sun_path) - 1);
|
||||
if (connect(socketFd, reinterpret_cast<const sockaddr*>(&pipeAddress),
|
||||
sizeof(pipeAddress)) == 0)
|
||||
{
|
||||
fcntl(socketFd, F_SETFL, O_NONBLOCK);
|
||||
#ifdef SO_NOSIGPIPE
|
||||
int optval = 1;
|
||||
setsockopt(socketFd, SOL_SOCKET, SO_NOSIGPIPE, &optval, sizeof(optval));
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
close();
|
||||
}
|
||||
}
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void close() {
|
||||
#ifdef _WIN32
|
||||
if (pipe != INVALID_HANDLE_VALUE) {
|
||||
CloseHandle(pipe);
|
||||
pipe = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
#else
|
||||
if (socketFd != -1) {
|
||||
::close(socketFd);
|
||||
socketFd = -1;
|
||||
}
|
||||
#endif
|
||||
pendingRead.clear();
|
||||
}
|
||||
|
||||
bool is_open() const {
|
||||
#ifdef _WIN32
|
||||
return pipe != INVALID_HANDLE_VALUE;
|
||||
#else
|
||||
return socketFd != -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool write_frame(const Frame& frame) {
|
||||
std::array<uint8_t, kFrameHeaderSize> header{};
|
||||
write_u32(header.data(), static_cast<uint32_t>(frame.opcode));
|
||||
write_u32(header.data() + sizeof(uint32_t), static_cast<uint32_t>(frame.payload.size()));
|
||||
|
||||
return write_all(header.data(), header.size()) &&
|
||||
write_all(
|
||||
reinterpret_cast<const uint8_t*>(frame.payload.data()), frame.payload.size());
|
||||
}
|
||||
|
||||
enum class ReadStatus {
|
||||
None,
|
||||
Frame,
|
||||
Closed,
|
||||
Corrupt,
|
||||
};
|
||||
|
||||
ReadStatus read_frame(Frame& frame) {
|
||||
if (!read_available()) {
|
||||
return is_open() ? ReadStatus::None : ReadStatus::Closed;
|
||||
}
|
||||
|
||||
if (pendingRead.size() < kFrameHeaderSize) {
|
||||
return ReadStatus::None;
|
||||
}
|
||||
|
||||
const uint32_t payloadLength = read_u32(pendingRead.data() + sizeof(uint32_t));
|
||||
if (payloadLength > kMaxFramePayloadSize) {
|
||||
return ReadStatus::Corrupt;
|
||||
}
|
||||
|
||||
const size_t frameLength = kFrameHeaderSize + payloadLength;
|
||||
if (pendingRead.size() < frameLength) {
|
||||
return ReadStatus::None;
|
||||
}
|
||||
|
||||
frame.opcode = static_cast<Opcode>(read_u32(pendingRead.data()));
|
||||
frame.payload.assign(
|
||||
reinterpret_cast<const char*>(pendingRead.data() + kFrameHeaderSize), payloadLength);
|
||||
pendingRead.erase(
|
||||
pendingRead.begin(), pendingRead.begin() + static_cast<std::ptrdiff_t>(frameLength));
|
||||
return ReadStatus::Frame;
|
||||
}
|
||||
|
||||
private:
|
||||
#ifndef _WIN32
|
||||
static std::vector<std::string> get_temp_paths() {
|
||||
std::vector<std::string> paths;
|
||||
for (const char* name : {"XDG_RUNTIME_DIR", "TMPDIR", "TMP", "TEMP"}) {
|
||||
if (const char* value = std::getenv(name); value && value[0] != '\0') {
|
||||
if (std::find(paths.begin(), paths.end(), value) == paths.end()) {
|
||||
paths.emplace_back(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (std::find(paths.begin(), paths.end(), "/tmp") == paths.end()) {
|
||||
paths.emplace_back("/tmp");
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
#endif
|
||||
|
||||
static void write_u32(uint8_t* out, uint32_t value) {
|
||||
out[0] = static_cast<uint8_t>(value & 0xff);
|
||||
out[1] = static_cast<uint8_t>((value >> 8) & 0xff);
|
||||
out[2] = static_cast<uint8_t>((value >> 16) & 0xff);
|
||||
out[3] = static_cast<uint8_t>((value >> 24) & 0xff);
|
||||
}
|
||||
|
||||
static uint32_t read_u32(const uint8_t* in) {
|
||||
return static_cast<uint32_t>(in[0]) | (static_cast<uint32_t>(in[1]) << 8) |
|
||||
(static_cast<uint32_t>(in[2]) << 16) | (static_cast<uint32_t>(in[3]) << 24);
|
||||
}
|
||||
|
||||
bool write_all(const uint8_t* data, size_t length) {
|
||||
size_t written = 0;
|
||||
while (written < length) {
|
||||
#ifdef _WIN32
|
||||
DWORD bytesWritten = 0;
|
||||
if (WriteFile(pipe, data + written, static_cast<DWORD>(length - written), &bytesWritten,
|
||||
nullptr) == FALSE ||
|
||||
bytesWritten == 0)
|
||||
{
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
written += bytesWritten;
|
||||
#else
|
||||
#ifdef MSG_NOSIGNAL
|
||||
constexpr int kMsgFlags = MSG_NOSIGNAL;
|
||||
#else
|
||||
constexpr int kMsgFlags = 0;
|
||||
#endif
|
||||
const ssize_t bytesWritten =
|
||||
send(socketFd, data + written, length - written, kMsgFlags);
|
||||
if (bytesWritten < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
continue;
|
||||
}
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
if (bytesWritten == 0) {
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
written += static_cast<size_t>(bytesWritten);
|
||||
#endif
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool read_available() {
|
||||
std::array<uint8_t, 4096> buffer{};
|
||||
bool readAny = false;
|
||||
|
||||
for (;;) {
|
||||
#ifdef _WIN32
|
||||
DWORD bytesAvailable = 0;
|
||||
if (PeekNamedPipe(pipe, nullptr, 0, nullptr, &bytesAvailable, nullptr) == FALSE) {
|
||||
close();
|
||||
return readAny;
|
||||
}
|
||||
if (bytesAvailable == 0) {
|
||||
return readAny;
|
||||
}
|
||||
|
||||
const DWORD bytesToRead =
|
||||
std::min<DWORD>(bytesAvailable, static_cast<DWORD>(buffer.size()));
|
||||
DWORD bytesRead = 0;
|
||||
if (ReadFile(pipe, buffer.data(), bytesToRead, &bytesRead, nullptr) == FALSE) {
|
||||
close();
|
||||
return readAny;
|
||||
}
|
||||
if (bytesRead == 0) {
|
||||
close();
|
||||
return readAny;
|
||||
}
|
||||
pendingRead.insert(pendingRead.end(), buffer.begin(), buffer.begin() + bytesRead);
|
||||
readAny = true;
|
||||
#else
|
||||
#ifdef MSG_NOSIGNAL
|
||||
constexpr int kMsgFlags = MSG_NOSIGNAL;
|
||||
#else
|
||||
constexpr int kMsgFlags = 0;
|
||||
#endif
|
||||
const ssize_t bytesRead = recv(socketFd, buffer.data(), buffer.size(), kMsgFlags);
|
||||
if (bytesRead < 0) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
return readAny;
|
||||
}
|
||||
close();
|
||||
return readAny;
|
||||
}
|
||||
if (bytesRead == 0) {
|
||||
close();
|
||||
return readAny;
|
||||
}
|
||||
pendingRead.insert(pendingRead.end(), buffer.begin(), buffer.begin() + bytesRead);
|
||||
readAny = true;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
HANDLE pipe = INVALID_HANDLE_VALUE;
|
||||
#else
|
||||
int socketFd = -1;
|
||||
#endif
|
||||
std::vector<uint8_t> pendingRead;
|
||||
};
|
||||
|
||||
int current_process_id() {
|
||||
#ifdef _WIN32
|
||||
return static_cast<int>(GetCurrentProcessId());
|
||||
#else
|
||||
return static_cast<int>(getpid());
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string next_nonce() {
|
||||
static std::atomic_uint64_t sNonce{1};
|
||||
return std::to_string(sNonce.fetch_add(1));
|
||||
}
|
||||
|
||||
const json* find_member(const json& object, const char* key) {
|
||||
if (!object.is_object()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const auto member = object.find(key);
|
||||
if (member == object.end()) {
|
||||
return nullptr;
|
||||
}
|
||||
return &*member;
|
||||
}
|
||||
|
||||
std::string json_string_member(const json& object, const char* key) {
|
||||
const json* member = find_member(object, key);
|
||||
if (!member || !member->is_string()) {
|
||||
return {};
|
||||
}
|
||||
return member->get<std::string>();
|
||||
}
|
||||
|
||||
int json_int_member(const json& object, const char* key, int defaultValue) {
|
||||
const json* member = find_member(object, key);
|
||||
if (!member || !member->is_number_integer()) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
try {
|
||||
return member->get<int>();
|
||||
} catch (const json::exception&) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
json make_presence_activity(const Presence& presence) {
|
||||
json activity = json::object();
|
||||
|
||||
if (!presence.state.empty()) {
|
||||
activity["state"] = presence.state;
|
||||
}
|
||||
if (!presence.details.empty()) {
|
||||
activity["details"] = presence.details;
|
||||
}
|
||||
if (presence.startTimestamp != 0) {
|
||||
activity["timestamps"] = {{"start", presence.startTimestamp}};
|
||||
}
|
||||
if (!presence.largeImageKey.empty() || !presence.largeImageText.empty()) {
|
||||
json assets = json::object();
|
||||
if (!presence.largeImageKey.empty()) {
|
||||
assets["large_image"] = presence.largeImageKey;
|
||||
}
|
||||
if (!presence.largeImageText.empty()) {
|
||||
assets["large_text"] = presence.largeImageText;
|
||||
}
|
||||
activity["assets"] = std::move(assets);
|
||||
}
|
||||
|
||||
return activity;
|
||||
}
|
||||
|
||||
Frame make_handshake_frame(std::string_view applicationId) {
|
||||
return {
|
||||
Opcode::Handshake,
|
||||
json{
|
||||
{"v", kRpcVersion},
|
||||
{"client_id", std::string(applicationId)},
|
||||
}
|
||||
.dump(),
|
||||
};
|
||||
}
|
||||
|
||||
Frame make_set_activity_frame(std::string nonce, int pid, std::optional<Presence> presence) {
|
||||
json args = {{"pid", pid}};
|
||||
if (presence) {
|
||||
args["activity"] = make_presence_activity(*presence);
|
||||
} else {
|
||||
args["activity"] = nullptr;
|
||||
}
|
||||
|
||||
return {
|
||||
Opcode::Frame,
|
||||
json{
|
||||
{"cmd", "SET_ACTIVITY"},
|
||||
{"nonce", std::move(nonce)},
|
||||
{"args", std::move(args)},
|
||||
}
|
||||
.dump(),
|
||||
};
|
||||
}
|
||||
|
||||
class Client {
|
||||
public:
|
||||
void initialize(std::string applicationId, EventHandlers handlers) {
|
||||
shutdown();
|
||||
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
this->applicationId = std::move(applicationId);
|
||||
this->handlers = std::move(handlers);
|
||||
shouldRun = true;
|
||||
queuedPresence.reset();
|
||||
hasQueuedPresence = false;
|
||||
clearRequested = false;
|
||||
sentInitialConnectLog = false;
|
||||
}
|
||||
|
||||
ioThread = std::thread([this] { io_loop(); });
|
||||
}
|
||||
|
||||
void run_callbacks() {
|
||||
std::deque<QueuedEvent> events;
|
||||
EventHandlers localHandlers;
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
events.swap(queuedEvents);
|
||||
localHandlers = handlers;
|
||||
}
|
||||
|
||||
for (const QueuedEvent& event : events) {
|
||||
switch (event.type) {
|
||||
case QueuedEvent::Type::Ready:
|
||||
if (localHandlers.ready) {
|
||||
localHandlers.ready(event.user);
|
||||
}
|
||||
break;
|
||||
case QueuedEvent::Type::Disconnected:
|
||||
if (localHandlers.disconnected) {
|
||||
localHandlers.disconnected(event.code, event.message);
|
||||
}
|
||||
break;
|
||||
case QueuedEvent::Type::Error:
|
||||
if (localHandlers.error) {
|
||||
localHandlers.error(event.code, event.message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void update_presence(Presence presence) {
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (!shouldRun) {
|
||||
return;
|
||||
}
|
||||
queuedPresence = std::move(presence);
|
||||
hasQueuedPresence = true;
|
||||
}
|
||||
cv.notify_all();
|
||||
}
|
||||
|
||||
void clear_presence() {
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (!shouldRun) {
|
||||
return;
|
||||
}
|
||||
queuedPresence.reset();
|
||||
hasQueuedPresence = false;
|
||||
clearRequested = true;
|
||||
}
|
||||
cv.notify_all();
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (!shouldRun && !ioThread.joinable()) {
|
||||
return;
|
||||
}
|
||||
shouldRun = false;
|
||||
clearRequested = true;
|
||||
}
|
||||
cv.notify_all();
|
||||
|
||||
if (ioThread.joinable()) {
|
||||
ioThread.join();
|
||||
}
|
||||
|
||||
std::lock_guard lock(mutex);
|
||||
queuedPresence.reset();
|
||||
hasQueuedPresence = false;
|
||||
clearRequested = false;
|
||||
queuedEvents.clear();
|
||||
handlers = {};
|
||||
applicationId.clear();
|
||||
}
|
||||
|
||||
private:
|
||||
void io_loop() {
|
||||
IpcConnection connection;
|
||||
ConnectionState state = ConnectionState::Disconnected;
|
||||
Backoff reconnectBackoff;
|
||||
auto nextConnect = std::chrono::steady_clock::now();
|
||||
const int pid = current_process_id();
|
||||
std::string localApplicationId;
|
||||
|
||||
for (;;) {
|
||||
{
|
||||
std::unique_lock lock(mutex);
|
||||
if (!shouldRun) {
|
||||
break;
|
||||
}
|
||||
localApplicationId = applicationId;
|
||||
}
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (state == ConnectionState::Disconnected && now >= nextConnect) {
|
||||
if (connection.open()) {
|
||||
if (connection.write_frame(make_handshake_frame(localApplicationId))) {
|
||||
state = ConnectionState::SentHandshake;
|
||||
} else {
|
||||
connection.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (state == ConnectionState::Disconnected) {
|
||||
log_waiting_for_discord_once();
|
||||
nextConnect = now + reconnectBackoff.next_delay();
|
||||
}
|
||||
}
|
||||
|
||||
if (state != ConnectionState::Disconnected) {
|
||||
process_reads(connection, state, reconnectBackoff, nextConnect);
|
||||
}
|
||||
|
||||
if (state == ConnectionState::Connected) {
|
||||
flush_pending_presence(connection, pid);
|
||||
}
|
||||
|
||||
std::unique_lock lock(mutex);
|
||||
if (!shouldRun) {
|
||||
break;
|
||||
}
|
||||
cv.wait_for(lock, kIoWait);
|
||||
}
|
||||
|
||||
flush_shutdown_clear(connection, state, pid);
|
||||
connection.close();
|
||||
}
|
||||
|
||||
void process_reads(IpcConnection& connection, ConnectionState& state, Backoff& reconnectBackoff,
|
||||
std::chrono::steady_clock::time_point& nextConnect) {
|
||||
for (;;) {
|
||||
Frame frame;
|
||||
const auto status = connection.read_frame(frame);
|
||||
if (status == IpcConnection::ReadStatus::None) {
|
||||
return;
|
||||
}
|
||||
if (status == IpcConnection::ReadStatus::Closed) {
|
||||
handle_disconnect(connection, state, reconnectBackoff, nextConnect,
|
||||
static_cast<int>(DisconnectCode::PipeClosed), "Pipe closed");
|
||||
return;
|
||||
}
|
||||
if (status == IpcConnection::ReadStatus::Corrupt) {
|
||||
handle_disconnect(connection, state, reconnectBackoff, nextConnect,
|
||||
static_cast<int>(DisconnectCode::ReadCorrupt), "Oversized Discord IPC frame");
|
||||
return;
|
||||
}
|
||||
|
||||
switch (frame.opcode) {
|
||||
case Opcode::Frame:
|
||||
process_json_frame(frame.payload, state, reconnectBackoff);
|
||||
break;
|
||||
case Opcode::Close:
|
||||
process_close_frame(
|
||||
frame.payload, connection, state, reconnectBackoff, nextConnect);
|
||||
return;
|
||||
case Opcode::Ping:
|
||||
connection.write_frame({Opcode::Pong, frame.payload});
|
||||
break;
|
||||
case Opcode::Pong:
|
||||
break;
|
||||
case Opcode::Handshake:
|
||||
default:
|
||||
handle_disconnect(connection, state, reconnectBackoff, nextConnect,
|
||||
static_cast<int>(DisconnectCode::BadFrame), "Bad Discord IPC frame");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void process_json_frame(
|
||||
const std::string& payload, ConnectionState& state, Backoff& reconnectBackoff) {
|
||||
json message;
|
||||
try {
|
||||
message = json::parse(payload);
|
||||
} catch (const json::parse_error&) {
|
||||
enqueue_error(
|
||||
static_cast<int>(DisconnectCode::ReadCorrupt), "Invalid Discord IPC JSON");
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string cmd = json_string_member(message, "cmd");
|
||||
const std::string evt = json_string_member(message, "evt");
|
||||
|
||||
if (state == ConnectionState::SentHandshake && cmd == "DISPATCH" && evt == "READY") {
|
||||
state = ConnectionState::Connected;
|
||||
reconnectBackoff.reset();
|
||||
enqueue_ready(message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (evt == "ERROR") {
|
||||
const json* data = find_member(message, "data");
|
||||
enqueue_error(data ? json_int_member(*data, "code", 0) : 0,
|
||||
data ? json_string_member(*data, "message") : std::string{});
|
||||
}
|
||||
}
|
||||
|
||||
void process_close_frame(const std::string& payload, IpcConnection& connection,
|
||||
ConnectionState& state, Backoff& reconnectBackoff,
|
||||
std::chrono::steady_clock::time_point& nextConnect) {
|
||||
int code = static_cast<int>(DisconnectCode::PipeClosed);
|
||||
std::string message = "Discord closed IPC connection";
|
||||
|
||||
try {
|
||||
const json closePayload = json::parse(payload);
|
||||
code = json_int_member(closePayload, "code", code);
|
||||
const std::string closeMessage = json_string_member(closePayload, "message");
|
||||
if (!closeMessage.empty()) {
|
||||
message = closeMessage;
|
||||
}
|
||||
} catch (const json::exception&) {
|
||||
}
|
||||
|
||||
handle_disconnect(connection, state, reconnectBackoff, nextConnect, code, message);
|
||||
}
|
||||
|
||||
void handle_disconnect(IpcConnection& connection, ConnectionState& state,
|
||||
Backoff& reconnectBackoff, std::chrono::steady_clock::time_point& nextConnect, int code,
|
||||
std::string_view message) {
|
||||
const bool wasConnected =
|
||||
state == ConnectionState::Connected || state == ConnectionState::SentHandshake;
|
||||
connection.close();
|
||||
state = ConnectionState::Disconnected;
|
||||
nextConnect = std::chrono::steady_clock::now() + reconnectBackoff.next_delay();
|
||||
if (wasConnected) {
|
||||
enqueue_disconnected(code, message);
|
||||
}
|
||||
}
|
||||
|
||||
void flush_pending_presence(IpcConnection& connection, int pid) {
|
||||
std::optional<Presence> presence;
|
||||
bool shouldClear = false;
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (hasQueuedPresence) {
|
||||
presence = queuedPresence;
|
||||
hasQueuedPresence = false;
|
||||
} else if (clearRequested) {
|
||||
shouldClear = true;
|
||||
clearRequested = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (presence) {
|
||||
if (!connection.write_frame(
|
||||
make_set_activity_frame(next_nonce(), pid, std::move(presence))))
|
||||
{
|
||||
requeue_presence();
|
||||
}
|
||||
} else if (shouldClear) {
|
||||
connection.write_frame(make_set_activity_frame(next_nonce(), pid, std::nullopt));
|
||||
}
|
||||
}
|
||||
|
||||
void flush_shutdown_clear(IpcConnection& connection, ConnectionState state, int pid) {
|
||||
if (state != ConnectionState::Connected || !connection.is_open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
connection.write_frame(make_set_activity_frame(next_nonce(), pid, std::nullopt));
|
||||
const auto deadline = std::chrono::steady_clock::now() + kShutdownClearTimeout;
|
||||
while (std::chrono::steady_clock::now() < deadline) {
|
||||
Frame frame;
|
||||
const auto status = connection.read_frame(frame);
|
||||
if (status == IpcConnection::ReadStatus::None) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
continue;
|
||||
}
|
||||
if (status != IpcConnection::ReadStatus::Frame) {
|
||||
break;
|
||||
}
|
||||
if (frame.opcode == Opcode::Ping) {
|
||||
connection.write_frame({Opcode::Pong, frame.payload});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void requeue_presence() {
|
||||
std::lock_guard lock(mutex);
|
||||
if (queuedPresence) {
|
||||
hasQueuedPresence = true;
|
||||
}
|
||||
}
|
||||
|
||||
void enqueue_ready(const json& readyMessage) {
|
||||
User user;
|
||||
const auto data = readyMessage.find("data");
|
||||
if (data != readyMessage.end() && data->is_object()) {
|
||||
const auto userIt = data->find("user");
|
||||
if (userIt != data->end() && userIt->is_object()) {
|
||||
user.id = json_string_member(*userIt, "id");
|
||||
user.username = json_string_member(*userIt, "username");
|
||||
user.discriminator = json_string_member(*userIt, "discriminator");
|
||||
user.avatar = json_string_member(*userIt, "avatar");
|
||||
}
|
||||
}
|
||||
|
||||
std::lock_guard lock(mutex);
|
||||
queuedEvents.push_back({QueuedEvent::Type::Ready, std::move(user)});
|
||||
}
|
||||
|
||||
void enqueue_disconnected(int code, std::string_view message) {
|
||||
std::lock_guard lock(mutex);
|
||||
QueuedEvent event;
|
||||
event.type = QueuedEvent::Type::Disconnected;
|
||||
event.code = code;
|
||||
event.message = message;
|
||||
queuedEvents.push_back(std::move(event));
|
||||
}
|
||||
|
||||
void enqueue_error(int code, std::string_view message) {
|
||||
std::lock_guard lock(mutex);
|
||||
QueuedEvent event;
|
||||
event.type = QueuedEvent::Type::Error;
|
||||
event.code = code;
|
||||
event.message = message;
|
||||
queuedEvents.push_back(std::move(event));
|
||||
}
|
||||
|
||||
void log_waiting_for_discord_once() {
|
||||
bool shouldLog = false;
|
||||
{
|
||||
std::lock_guard lock(mutex);
|
||||
if (!sentInitialConnectLog) {
|
||||
sentInitialConnectLog = true;
|
||||
shouldLog = true;
|
||||
}
|
||||
}
|
||||
if (shouldLog) {
|
||||
DuskLog.info("Discord: Waiting for local Discord IPC");
|
||||
}
|
||||
}
|
||||
|
||||
std::mutex mutex;
|
||||
std::condition_variable cv;
|
||||
std::thread ioThread;
|
||||
std::string applicationId;
|
||||
EventHandlers handlers;
|
||||
std::deque<QueuedEvent> queuedEvents;
|
||||
std::optional<Presence> queuedPresence;
|
||||
bool hasQueuedPresence = false;
|
||||
bool clearRequested = false;
|
||||
bool shouldRun = false;
|
||||
bool sentInitialConnectLog = false;
|
||||
};
|
||||
|
||||
Client& client() {
|
||||
static Client sClient;
|
||||
return sClient;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void initialize(std::string applicationId, EventHandlers handlers) {
|
||||
client().initialize(std::move(applicationId), std::move(handlers));
|
||||
}
|
||||
|
||||
void run_callbacks() {
|
||||
client().run_callbacks();
|
||||
}
|
||||
|
||||
void update_presence(Presence presence) {
|
||||
client().update_presence(std::move(presence));
|
||||
}
|
||||
|
||||
void clear_presence() {
|
||||
client().clear_presence();
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
client().shutdown();
|
||||
}
|
||||
|
||||
} // namespace dusk::discord::rpc
|
||||
|
||||
#endif // DUSK_DISCORD
|
||||
@@ -1,41 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef DUSK_DISCORD
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace dusk::discord::rpc {
|
||||
|
||||
struct User {
|
||||
std::string id;
|
||||
std::string username;
|
||||
std::string discriminator;
|
||||
std::string avatar;
|
||||
};
|
||||
|
||||
struct Presence {
|
||||
std::string state;
|
||||
std::string details;
|
||||
int64_t startTimestamp = 0;
|
||||
std::string largeImageKey;
|
||||
std::string largeImageText;
|
||||
};
|
||||
|
||||
struct EventHandlers {
|
||||
std::function<void(const User&)> ready;
|
||||
std::function<void(int, std::string_view)> disconnected;
|
||||
std::function<void(int, std::string_view)> error;
|
||||
};
|
||||
|
||||
void initialize(std::string applicationId, EventHandlers handlers);
|
||||
void run_callbacks();
|
||||
void update_presence(Presence presence);
|
||||
void clear_presence();
|
||||
void shutdown();
|
||||
|
||||
} // namespace dusk::discord::rpc
|
||||
|
||||
#endif // DUSK_DISCORD
|
||||
@@ -1,34 +1,37 @@
|
||||
#ifdef DUSK_DISCORD
|
||||
#if BOREALIS_HAS_DISCORD
|
||||
|
||||
#include "dusk/discord_presence.hpp"
|
||||
#include "d/d_com_inf_game.h"
|
||||
#include "discord.hpp"
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/map_loader_definitions.h"
|
||||
#include "fmt/format.h"
|
||||
|
||||
#include <borealis/discord.hpp>
|
||||
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
|
||||
namespace dusk::discord {
|
||||
namespace {
|
||||
constexpr borealis::Log Log{"dusk::discord"};
|
||||
int64_t g_startTime = 0;
|
||||
bool g_initialized = false;
|
||||
} // namespace
|
||||
|
||||
static int64_t g_startTime = 0;
|
||||
static bool g_initialized = false;
|
||||
static constexpr const char* kApplicationId = "1495632471994405035";
|
||||
|
||||
static void on_ready(const rpc::User& user) {
|
||||
DuskLog.info("Discord: Connected as {}", user.username);
|
||||
static void on_ready(const borealis::discord::User& user) {
|
||||
Log.info("Connected as {}", user.username);
|
||||
}
|
||||
|
||||
static void on_disconnected(int errorCode, std::string_view message) {
|
||||
DuskLog.warn("Discord: Disconnected ({}: {})", errorCode, message);
|
||||
Log.warn("Disconnected ({}: {})", errorCode, message);
|
||||
}
|
||||
|
||||
static void on_error(int errorCode, std::string_view message) {
|
||||
DuskLog.warn("Discord: Error ({}: {})", errorCode, message);
|
||||
Log.warn("Error ({}: {})", errorCode, message);
|
||||
}
|
||||
|
||||
static const char* lookup_map_name(const char* mapFile) {
|
||||
@@ -49,20 +52,22 @@ void initialize() {
|
||||
std::chrono::system_clock::now().time_since_epoch())
|
||||
.count();
|
||||
|
||||
rpc::EventHandlers handlers{};
|
||||
borealis::discord::EventHandlers handlers{};
|
||||
handlers.ready = on_ready;
|
||||
handlers.disconnected = on_disconnected;
|
||||
handlers.error = on_error;
|
||||
rpc::initialize(kApplicationId, std::move(handlers));
|
||||
g_initialized = true;
|
||||
g_initialized = borealis::discord::initialize(AppInfo, std::move(handlers));
|
||||
if (!g_initialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
DuskLog.info("Discord Rich Presence initialized");
|
||||
Log.info("Discord Rich Presence initialized");
|
||||
}
|
||||
|
||||
void run_callbacks() {
|
||||
if (!g_initialized)
|
||||
return;
|
||||
rpc::run_callbacks();
|
||||
borealis::discord::run_callbacks();
|
||||
}
|
||||
|
||||
void update_presence() {
|
||||
@@ -78,7 +83,7 @@ void update_presence() {
|
||||
static std::string sDetailsBuf;
|
||||
static std::string sStateBuf;
|
||||
|
||||
rpc::Presence presence{};
|
||||
borealis::discord::Presence presence{};
|
||||
presence.startTimestamp = g_startTime;
|
||||
presence.largeImageKey = "icon";
|
||||
presence.largeImageText = "Dusklight";
|
||||
@@ -105,19 +110,20 @@ void update_presence() {
|
||||
}
|
||||
}
|
||||
|
||||
rpc::update_presence(std::move(presence));
|
||||
DuskLog.debug("Discord Rich Presence sent");
|
||||
if (borealis::discord::update_presence(std::move(presence))) {
|
||||
Log.debug("Discord Rich Presence changed");
|
||||
}
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
if (!g_initialized)
|
||||
return;
|
||||
rpc::clear_presence();
|
||||
rpc::shutdown();
|
||||
borealis::discord::clear_presence();
|
||||
borealis::discord::shutdown();
|
||||
g_initialized = false;
|
||||
DuskLog.info("Discord Rich Presence shut down");
|
||||
Log.info("Discord Rich Presence shut down");
|
||||
}
|
||||
|
||||
} // namespace dusk::discord
|
||||
|
||||
#endif // DUSK_DISCORD
|
||||
#endif // BOREALIS_HAS_DISCORD
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef DUSK_DISCORD
|
||||
#if BOREALIS_HAS_DISCORD
|
||||
|
||||
namespace dusk::discord {
|
||||
|
||||
@@ -11,4 +11,4 @@ void shutdown();
|
||||
|
||||
} // namespace dusk::discord
|
||||
|
||||
#endif // DUSK_DISCORD
|
||||
#endif // BOREALIS_HAS_DISCORD
|
||||
|
||||
@@ -1,314 +0,0 @@
|
||||
#include "file_select.hpp"
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
|
||||
#include <SDL3/SDL_dialog.h>
|
||||
#include <SDL3/SDL_error.h>
|
||||
#include <SDL3/SDL_init.h>
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
|
||||
#if defined(__ANDROID__) || defined(ANDROID)
|
||||
#include <SDL3/SDL_system.h>
|
||||
#include <jni.h>
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__) && !TARGET_OS_IOS && !TARGET_OS_TV && !TARGET_OS_MACCATALYST
|
||||
#define USE_MACOS_FOLDER_DIALOG 1
|
||||
#else
|
||||
#define USE_MACOS_FOLDER_DIALOG 0
|
||||
#endif
|
||||
|
||||
#if defined(__APPLE__) && TARGET_OS_IOS && !TARGET_OS_MACCATALYST
|
||||
#define USE_IOS_DIALOG 1
|
||||
#include "ios/FileSelectDialog.h"
|
||||
#else
|
||||
#define USE_IOS_DIALOG 0
|
||||
#endif
|
||||
|
||||
#if USE_MACOS_FOLDER_DIALOG
|
||||
namespace dusk {
|
||||
bool ShowMacOSFolderSelect(
|
||||
FileCallback callback, void* userdata, SDL_Window* window, const char* default_location);
|
||||
} // namespace dusk
|
||||
#endif
|
||||
|
||||
namespace dusk {
|
||||
namespace {
|
||||
|
||||
std::string fallback_display_name(std::string_view path) {
|
||||
if (path.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string pathString(path);
|
||||
while (pathString.size() > 1 && (pathString.back() == '/' || pathString.back() == '\\')) {
|
||||
pathString.pop_back();
|
||||
}
|
||||
|
||||
const std::size_t slash = pathString.find_last_of("/\\");
|
||||
if (slash == std::string::npos || slash + 1 >= pathString.size()) {
|
||||
return pathString;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
struct AndroidFolderDialogState {
|
||||
FileCallback callback;
|
||||
void* userdata;
|
||||
std::string path;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
void onAndroidFolderDialogFinished(void* userdata) {
|
||||
std::unique_ptr<AndroidFolderDialogState> state(
|
||||
static_cast<AndroidFolderDialogState*>(userdata));
|
||||
|
||||
const char* path = state->path.empty() ? nullptr : state->path.c_str();
|
||||
const char* error = state->error.empty() ? nullptr : state->error.c_str();
|
||||
state->callback(state->userdata, path, error);
|
||||
}
|
||||
|
||||
bool show_android_folder_select(AndroidFolderDialogState* state) {
|
||||
auto* env = static_cast<JNIEnv*>(SDL_GetAndroidJNIEnv());
|
||||
if (env == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
jobject activity = static_cast<jobject>(SDL_GetAndroidActivity());
|
||||
if (activity == nullptr || clear_pending_exception(env)) {
|
||||
if (activity != nullptr) {
|
||||
env->DeleteLocalRef(activity);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
jclass activityClass = env->GetObjectClass(activity);
|
||||
if (activityClass == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(activity);
|
||||
return false;
|
||||
}
|
||||
|
||||
jmethodID showFolderDialog =
|
||||
env->GetMethodID(activityClass, "showFolderDialog", "(J)Z");
|
||||
env->DeleteLocalRef(activityClass);
|
||||
if (showFolderDialog == nullptr || clear_pending_exception(env)) {
|
||||
env->DeleteLocalRef(activity);
|
||||
return false;
|
||||
}
|
||||
|
||||
const jboolean shown = env->CallBooleanMethod(
|
||||
activity, showFolderDialog, reinterpret_cast<jlong>(state));
|
||||
env->DeleteLocalRef(activity);
|
||||
if (clear_pending_exception(env)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return shown == JNI_TRUE;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_dev_twilitrealm_dusk_DuskActivity_nativeFolderDialogResult(
|
||||
JNIEnv* env, jclass, jlong userdata, jstring path, jstring error) {
|
||||
auto* state = reinterpret_cast<AndroidFolderDialogState*>(userdata);
|
||||
if (state == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
state->path = to_string(env, path);
|
||||
state->error = to_string(env, error);
|
||||
|
||||
if (!SDL_RunOnMainThread(&onAndroidFolderDialogFinished, state, false)) {
|
||||
onAndroidFolderDialogFinished(state);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if USE_IOS_DIALOG
|
||||
struct IOSDialogCallbackState {
|
||||
FileCallback callback;
|
||||
void* userdata;
|
||||
};
|
||||
|
||||
void onIOSDialogFinished(void* userdata, const char* path, const char* error) {
|
||||
std::unique_ptr<IOSDialogCallbackState> state(static_cast<IOSDialogCallbackState*>(userdata));
|
||||
|
||||
if (error != nullptr) {
|
||||
state->callback(state->userdata, nullptr, error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (path == nullptr) {
|
||||
state->callback(state->userdata, nullptr, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
state->callback(state->userdata, path, nullptr);
|
||||
}
|
||||
#else
|
||||
struct SDLDialogCallbackState {
|
||||
FileCallback callback;
|
||||
void* userdata;
|
||||
};
|
||||
|
||||
void onSDLDialogFinished(void* userdata, const char* const* filelist, [[maybe_unused]] int filter) {
|
||||
std::unique_ptr<SDLDialogCallbackState> state(static_cast<SDLDialogCallbackState*>(userdata));
|
||||
|
||||
if (filelist == nullptr) {
|
||||
state->callback(state->userdata, nullptr, SDL_GetError());
|
||||
return;
|
||||
}
|
||||
|
||||
if (filelist[0] == nullptr) {
|
||||
state->callback(state->userdata, nullptr, nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
state->callback(state->userdata, filelist[0], nullptr);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window,
|
||||
const SDL_DialogFileFilter* filters, int nfilters, const char* default_location,
|
||||
bool allow_many) {
|
||||
if (callback == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if USE_IOS_DIALOG
|
||||
auto state = std::make_unique<IOSDialogCallbackState>();
|
||||
state->callback = callback;
|
||||
state->userdata = userdata;
|
||||
|
||||
Dusk_iOS_ShowFileSelect(&onIOSDialogFinished, state.release(), window, filters, nfilters,
|
||||
default_location, allow_many);
|
||||
#else
|
||||
auto state = std::make_unique<SDLDialogCallbackState>();
|
||||
state->callback = callback;
|
||||
state->userdata = userdata;
|
||||
|
||||
SDL_ShowOpenFileDialog(&onSDLDialogFinished, state.release(), window, filters, nfilters,
|
||||
default_location, allow_many);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ShowFolderSelect(
|
||||
FileCallback callback, void* userdata, SDL_Window* window, const char* default_location) {
|
||||
if (callback == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if USE_IOS_DIALOG
|
||||
callback(userdata, nullptr, "Folder selection is not supported on this platform");
|
||||
#elif USE_MACOS_FOLDER_DIALOG
|
||||
ShowMacOSFolderSelect(callback, userdata, window, default_location);
|
||||
#elif defined(__ANDROID__) || defined(ANDROID)
|
||||
auto state = std::make_unique<AndroidFolderDialogState>();
|
||||
state->callback = callback;
|
||||
state->userdata = userdata;
|
||||
|
||||
if (show_android_folder_select(state.get())) {
|
||||
state.release();
|
||||
return;
|
||||
}
|
||||
|
||||
callback(userdata, nullptr, "Folder selection is not supported on this platform");
|
||||
#else
|
||||
auto state = std::make_unique<SDLDialogCallbackState>();
|
||||
state->callback = callback;
|
||||
state->userdata = userdata;
|
||||
|
||||
SDL_ShowOpenFolderDialog(
|
||||
&onSDLDialogFinished, state.release(), window, default_location, false);
|
||||
#endif
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1,22 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <SDL3/SDL_dialog.h>
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
namespace dusk {
|
||||
|
||||
using FileCallback = void (*)(void* userdata, const char* path, const char* error);
|
||||
|
||||
void ShowFileSelect(FileCallback callback, void* userdata, SDL_Window* window,
|
||||
const SDL_DialogFileFilter* filters, int nfilters, const char* default_location,
|
||||
bool allow_many);
|
||||
void ShowFolderSelect(
|
||||
FileCallback callback, void* userdata, SDL_Window* window, const char* default_location);
|
||||
|
||||
std::string display_name_for_path(std::string_view path);
|
||||
|
||||
} // namespace dusk
|
||||
@@ -1,102 +0,0 @@
|
||||
#include "file_select.hpp"
|
||||
|
||||
#include <SDL3/SDL_properties.h>
|
||||
#include <SDL3/SDL_video.h>
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
namespace dusk {
|
||||
namespace {
|
||||
|
||||
struct MacOSFolderDialogState {
|
||||
FileCallback callback;
|
||||
void* userdata;
|
||||
};
|
||||
|
||||
void finish_folder_dialog(MacOSFolderDialogState* state, NSURL* url, const char* error) {
|
||||
if (state == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (error != nullptr) {
|
||||
state->callback(state->userdata, nullptr, error);
|
||||
delete state;
|
||||
return;
|
||||
}
|
||||
|
||||
if (url == nil) {
|
||||
state->callback(state->userdata, nullptr, nullptr);
|
||||
delete state;
|
||||
return;
|
||||
}
|
||||
|
||||
state->callback(state->userdata, [[url path] UTF8String], nullptr);
|
||||
delete state;
|
||||
}
|
||||
|
||||
void configure_default_location(NSOpenPanel* panel, const char* defaultLocation) {
|
||||
if (panel == nil || defaultLocation == nullptr || defaultLocation[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
NSString* path = [NSString stringWithUTF8String:defaultLocation];
|
||||
if (path == nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
BOOL isDirectory = NO;
|
||||
NSFileManager* fileManager = [NSFileManager defaultManager];
|
||||
NSURL* url = [NSURL fileURLWithPath:path];
|
||||
if ([fileManager fileExistsAtPath:path isDirectory:&isDirectory] && isDirectory) {
|
||||
[panel setDirectoryURL:url];
|
||||
} else {
|
||||
[panel setDirectoryURL:[url URLByDeletingLastPathComponent]];
|
||||
}
|
||||
}
|
||||
|
||||
NSWindow* window_for_sdl_window(SDL_Window* window) {
|
||||
if (window == nullptr) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
auto props = SDL_GetWindowProperties(window);
|
||||
return (__bridge NSWindow*)SDL_GetPointerProperty(
|
||||
props, SDL_PROP_WINDOW_COCOA_WINDOW_POINTER, nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ShowMacOSFolderSelect(
|
||||
FileCallback callback, void* userdata, SDL_Window* window, const char* defaultLocation) {
|
||||
if (callback == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* state = new MacOSFolderDialogState{
|
||||
.callback = callback,
|
||||
.userdata = userdata,
|
||||
};
|
||||
|
||||
NSOpenPanel* panel = [NSOpenPanel openPanel];
|
||||
[panel setCanChooseFiles:NO];
|
||||
[panel setCanChooseDirectories:YES];
|
||||
[panel setAllowsMultipleSelection:NO];
|
||||
[panel setCanCreateDirectories:YES];
|
||||
configure_default_location(panel, defaultLocation);
|
||||
|
||||
NSWindow* modalWindow = window_for_sdl_window(window);
|
||||
if (modalWindow != nil) {
|
||||
[panel beginSheetModalForWindow:modalWindow
|
||||
completionHandler:^(NSModalResponse result) {
|
||||
finish_folder_dialog(
|
||||
state, result == NSModalResponseOK ? [panel URL] : nil, nullptr);
|
||||
}];
|
||||
return true;
|
||||
}
|
||||
|
||||
const NSModalResponse result = [panel runModal];
|
||||
finish_folder_dialog(state, result == NSModalResponseOK ? [panel URL] : nil, nullptr);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
@@ -1,402 +0,0 @@
|
||||
#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
|
||||
@@ -1,206 +0,0 @@
|
||||
#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
|
||||
@@ -1,60 +0,0 @@
|
||||
#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
|
||||
@@ -1,24 +0,0 @@
|
||||
#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
|
||||
@@ -1,238 +0,0 @@
|
||||
#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
|
||||
@@ -1,320 +0,0 @@
|
||||
#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
|
||||
@@ -13,7 +13,7 @@
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "f_op/f_op_overlap_mng.h"
|
||||
#include "../file_select.hpp"
|
||||
#include <borealis/file_select.hpp>
|
||||
#include "aurora/lib/window.hpp"
|
||||
|
||||
#include <unordered_set>
|
||||
@@ -40,15 +40,6 @@ static constexpr auto STATES_FILENAME = "states.json";
|
||||
|
||||
static bool ValidateEncodedState(const std::string&);
|
||||
|
||||
void ImGuiStateShare::onMergeFileSelected(void* userdata, const char* path, const char* /*error*/) {
|
||||
auto* self = static_cast<ImGuiStateShare*>(userdata);
|
||||
if (path != nullptr) {
|
||||
self->m_pendingMergePath = path;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
static std::filesystem::path GetStatesFilePath() {
|
||||
return ConfigPath / STATES_FILENAME;
|
||||
}
|
||||
@@ -381,8 +372,18 @@ void ImGuiStateShare::draw(bool& open) {
|
||||
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Load Pack")) {
|
||||
static constexpr SDL_DialogFileFilter filter = {"State pack", "json"};
|
||||
ShowFileSelect(&onMergeFileSelected, this, aurora::window::get_sdl_window(), &filter, 1, nullptr, false);
|
||||
borealis::file_select::open_file(
|
||||
{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.filters = {{"State pack", "json"}},
|
||||
},
|
||||
[this](borealis::file_select::Result result) {
|
||||
if (result.status == borealis::file_select::Status::Selected &&
|
||||
!result.locations.empty())
|
||||
{
|
||||
m_pendingMergePath = std::move(result.locations.front());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!m_states.empty()) {
|
||||
|
||||
@@ -24,8 +24,6 @@ private:
|
||||
void loadStatesFile();
|
||||
void saveStatesFile();
|
||||
void mergeFromFile(const std::string& path);
|
||||
static void onMergeFileSelected(void* userdata, const char* path, const char* error);
|
||||
|
||||
std::vector<SavedStateEntry> m_states;
|
||||
std::string m_statusMsg;
|
||||
std::optional<dSv_info_c> m_pendingInfo;
|
||||
|
||||
@@ -11,24 +11,8 @@ namespace dusk {
|
||||
static bool StubLogPaused;
|
||||
static std::mutex StubLogMutex;
|
||||
|
||||
const char* LogLevelName(const AuroraLogLevel level) {
|
||||
switch (level) {
|
||||
case LOG_DEBUG:
|
||||
return "DEBUG";
|
||||
case LOG_INFO:
|
||||
return "INFO";
|
||||
case LOG_WARNING:
|
||||
return "WARNING";
|
||||
case LOG_ERROR:
|
||||
return "ERROR";
|
||||
case LOG_FATAL:
|
||||
return "FATAL";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
void SendToStubLog(AuroraLogLevel level, const char* module, const char* message) {
|
||||
void SendToStubLog(
|
||||
borealis::LogLevel level, std::string_view module, std::string_view message) {
|
||||
if (StubLogPaused) {
|
||||
return;
|
||||
}
|
||||
@@ -41,8 +25,10 @@ namespace dusk {
|
||||
}
|
||||
|
||||
LineOffsets.push_back(StubLogBuffer.size());
|
||||
const auto levelName = LogLevelName(level);
|
||||
StubLogBuffer.appendf("[%s | %s] %s\n", levelName, module, message);
|
||||
const auto levelName = borealis::to_string(level);
|
||||
StubLogBuffer.appendf("[%.*s | %.*s] %.*s\n", static_cast<int>(levelName.size()),
|
||||
levelName.data(), static_cast<int>(module.size()), module.data(),
|
||||
static_cast<int>(message.size()), message.data());
|
||||
}
|
||||
|
||||
void ImGuiMenuTools::ShowStubLog() {
|
||||
|
||||
@@ -96,12 +96,4 @@ public:
|
||||
FILE* ToInner();
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a std::filesystem::path to a std::string, UTF-8, without exploding on Windows.
|
||||
*/
|
||||
inline std::string fs_path_to_string(const std::filesystem::path& path) {
|
||||
const auto u8str = path.u8string();
|
||||
return {reinterpret_cast<const char*>(u8str.c_str())};
|
||||
}
|
||||
|
||||
} // namespace dusk::io
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include <SDL3/SDL_dialog.h>
|
||||
|
||||
struct SDL_Window;
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void (*IOSFileCallback)(void* userdata, const char* path, const char* error);
|
||||
|
||||
void Dusk_iOS_ShowFileSelect(IOSFileCallback callback, void* userdata, SDL_Window* window,
|
||||
const SDL_DialogFileFilter* filters, int nfilters,
|
||||
const char* default_location, bool allow_many);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -1,151 +0,0 @@
|
||||
#include "FileSelectDialog.h"
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#include <SDL3/SDL_error.h>
|
||||
#include <SDL3/SDL_properties.h>
|
||||
#include <SDL3/SDL_stdinc.h>
|
||||
#include <SDL3/SDL_video.h>
|
||||
|
||||
static void *g_picker_delegate_key = &g_picker_delegate_key;
|
||||
|
||||
static void RunOnMainThread(void (^block)(void))
|
||||
{
|
||||
if ([NSThread isMainThread]) {
|
||||
block();
|
||||
} else {
|
||||
dispatch_sync(dispatch_get_main_queue(), block);
|
||||
}
|
||||
}
|
||||
|
||||
static NSError *MakeError(NSString *message)
|
||||
{
|
||||
return [NSError errorWithDomain:@"dev.twilitrealm.dusk.file-select"
|
||||
code:1
|
||||
userInfo:@{NSLocalizedDescriptionKey: message}];
|
||||
}
|
||||
|
||||
static UIViewController *FindTopViewController(UIViewController *controller)
|
||||
{
|
||||
UIViewController *current = controller;
|
||||
while (current.presentedViewController != nil) {
|
||||
current = current.presentedViewController;
|
||||
}
|
||||
return current;
|
||||
}
|
||||
|
||||
static UIViewController *PresenterFromWindow(SDL_Window *window)
|
||||
{
|
||||
if (window == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const SDL_PropertiesID props = SDL_GetWindowProperties(window);
|
||||
if (props == 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
UIWindow *uiwindow = (__bridge UIWindow *)SDL_GetPointerProperty(
|
||||
props, SDL_PROP_WINDOW_UIKIT_WINDOW_POINTER, NULL);
|
||||
if (uiwindow == nil || uiwindow.rootViewController == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return FindTopViewController(uiwindow.rootViewController);
|
||||
}
|
||||
|
||||
static NSURL *InitialDirectoryURL(const char *default_location)
|
||||
{
|
||||
if (default_location == NULL || *default_location == '\0') {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSString *path = [NSString stringWithUTF8String:default_location];
|
||||
NSURL *url = [NSURL fileURLWithPath:path];
|
||||
if ([path hasSuffix:@"/"]) {
|
||||
return url;
|
||||
}
|
||||
|
||||
return [url URLByDeletingLastPathComponent];
|
||||
}
|
||||
|
||||
@interface DocumentPickerDelegate : NSObject <UIDocumentPickerDelegate>
|
||||
|
||||
@property(nonatomic, assign) IOSFileCallback callback;
|
||||
@property(nonatomic, assign) void *userdata;
|
||||
|
||||
@end
|
||||
|
||||
@implementation DocumentPickerDelegate
|
||||
|
||||
- (void)finishWithPath:(const char *)path error:(const char *)error {
|
||||
if (self.callback != NULL) {
|
||||
self.callback(self.userdata, path, error);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)documentPicker:(UIDocumentPickerViewController *)controller
|
||||
didPickDocumentsAtURLs:(NSArray<NSURL *> *)urls
|
||||
{
|
||||
NSURL *url = urls.firstObject;
|
||||
if (url == nil) {
|
||||
[self finishWithPath:NULL error:NULL];
|
||||
return;
|
||||
}
|
||||
|
||||
[self finishWithPath:url.path.UTF8String error:NULL];
|
||||
(void)controller;
|
||||
}
|
||||
|
||||
- (void)documentPickerWasCancelled:(UIDocumentPickerViewController *)controller
|
||||
{
|
||||
[self finishWithPath:NULL error:NULL];
|
||||
(void)controller;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
void Dusk_iOS_ShowFileSelect(IOSFileCallback callback, void *userdata,
|
||||
SDL_Window *window,
|
||||
const SDL_DialogFileFilter *filters, int nfilters,
|
||||
const char *default_location,
|
||||
bool allow_many)
|
||||
{
|
||||
RunOnMainThread(^{
|
||||
@autoreleasepool {
|
||||
UIViewController *presenter = PresenterFromWindow(window);
|
||||
if (presenter == nil) {
|
||||
callback(userdata, NULL, "Failed to find an iOS view controller for the file picker.");
|
||||
return;
|
||||
}
|
||||
|
||||
NSLog(@"[ShowFileSelect] presenting picker from %@", NSStringFromClass([presenter class]));
|
||||
|
||||
UIDocumentPickerViewController *picker =
|
||||
[[UIDocumentPickerViewController alloc]
|
||||
initForOpeningContentTypes:@[ UTTypeItem ]
|
||||
asCopy:YES];
|
||||
picker.allowsMultipleSelection = allow_many ? YES : NO;
|
||||
picker.shouldShowFileExtensions = YES;
|
||||
|
||||
NSURL *directory_url = InitialDirectoryURL(default_location);
|
||||
if (directory_url != nil) {
|
||||
picker.directoryURL = directory_url;
|
||||
}
|
||||
|
||||
DocumentPickerDelegate *delegate = [DocumentPickerDelegate new];
|
||||
delegate.callback = callback;
|
||||
delegate.userdata = userdata;
|
||||
picker.delegate = delegate;
|
||||
objc_setAssociatedObject(picker, g_picker_delegate_key, delegate,
|
||||
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
||||
|
||||
[presenter presentViewController:picker animated:YES completion:nil];
|
||||
(void)filters;
|
||||
(void)nfilters;
|
||||
}
|
||||
});
|
||||
}
|
||||
+65
-204
@@ -1,49 +1,15 @@
|
||||
#include "iso_validate.hpp"
|
||||
|
||||
#include <SDL3/SDL_iostream.h>
|
||||
#include <nod.h>
|
||||
#include <xxhash.h>
|
||||
#include <borealis/disc.hpp>
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string_view>
|
||||
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/settings.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint8_t hex_nibble_to_u8(char c) {
|
||||
if (c >= '0' && c <= '9')
|
||||
return c - '0';
|
||||
if (c >= 'a' && c <= 'f')
|
||||
return c - 'a' + 10;
|
||||
if (c >= 'A' && c <= 'F')
|
||||
return c - 'A' + 10;
|
||||
throw std::invalid_argument("invalid hex character");
|
||||
}
|
||||
|
||||
constexpr uint64_t parse_u64_hex(std::string_view s) {
|
||||
if (s.size() != 16)
|
||||
throw std::invalid_argument("expected 16 hex chars for uint64");
|
||||
|
||||
uint64_t value = 0;
|
||||
for (char c : s) {
|
||||
value = (value << 4) | hex_nibble_to_u8(c);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
constexpr XXH128_hash_t parse_xxh128(std::string_view hex) {
|
||||
if (hex.size() != 32)
|
||||
throw std::invalid_argument("expected 32 hex chars for XXH128");
|
||||
|
||||
return XXH128_hash_t{
|
||||
.low64 = parse_u64_hex(hex.substr(16, 16)),
|
||||
.high64 = parse_u64_hex(hex.substr(0, 16)),
|
||||
};
|
||||
}
|
||||
|
||||
const char* verification_state_name(dusk::DiscVerificationState state) noexcept {
|
||||
switch (state) {
|
||||
case dusk::DiscVerificationState::Success:
|
||||
@@ -59,195 +25,90 @@ const char* verification_state_name(dusk::DiscVerificationState state) noexcept
|
||||
} // namespace
|
||||
|
||||
namespace dusk::iso {
|
||||
namespace {
|
||||
|
||||
struct KnownDisc {
|
||||
std::string_view id;
|
||||
Platform platform;
|
||||
Region region;
|
||||
bool supported = false;
|
||||
XXH128_hash_t hash{};
|
||||
|
||||
constexpr KnownDisc(std::string_view id, Platform platform, Region region)
|
||||
: id(id), platform(platform), region(region) {}
|
||||
constexpr KnownDisc(
|
||||
std::string_view id, Platform platform, Region region, const std::string_view hash)
|
||||
: id(id), platform(platform), region(region), supported(true), hash(parse_xxh128(hash)) {}
|
||||
};
|
||||
|
||||
constexpr auto KNOWN_DISCS = std::to_array<KnownDisc>({
|
||||
{"GZ2E01", Platform::GameCube, Region::NorthAmerica, "14e886f08e548a000afde98a3195e788"},
|
||||
{"GZ2J01", Platform::GameCube, Region::Japan, "5967dc7a6a553652f4d2050aeef6f368"},
|
||||
{"GZ2P01", Platform::GameCube, Region::Europe, "9ef597588b0035ca9e91b333fa9a8a7e"},
|
||||
{"RZDE01", Platform::Wii, Region::NorthAmerica},
|
||||
{"RZDJ01", Platform::Wii, Region::Japan},
|
||||
{"RZDK01", Platform::Wii, Region::Korea},
|
||||
{"RZDP01", Platform::Wii, Region::Europe},
|
||||
constexpr auto AcceptedDiscs = std::to_array<borealis::disc::AcceptedDisc>({
|
||||
{
|
||||
.gameId = "GZ2E01",
|
||||
.expectedHash = borealis::disc::parse_xxh3_128("14e886f08e548a000afde98a3195e788"),
|
||||
},
|
||||
{
|
||||
.gameId = "GZ2J01",
|
||||
.expectedHash = borealis::disc::parse_xxh3_128("5967dc7a6a553652f4d2050aeef6f368"),
|
||||
},
|
||||
{
|
||||
.gameId = "GZ2P01",
|
||||
.expectedHash = borealis::disc::parse_xxh3_128("9ef597588b0035ca9e91b333fa9a8a7e"),
|
||||
},
|
||||
});
|
||||
|
||||
constexpr const KnownDisc* find_disc(std::string_view id) {
|
||||
for (const auto& disc : KNOWN_DISCS) {
|
||||
if (disc.id == id)
|
||||
return &disc;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
constexpr auto RecognizedGameIds =
|
||||
std::to_array<std::string_view>({"RZDE01", "RZDJ01", "RZDK01", "RZDP01"});
|
||||
|
||||
struct NodHandleWrapper {
|
||||
NodHandle* handle;
|
||||
|
||||
NodHandleWrapper() : handle(nullptr) {}
|
||||
~NodHandleWrapper() {
|
||||
if (handle != nullptr) {
|
||||
nod_free(handle);
|
||||
handle = nullptr;
|
||||
}
|
||||
}
|
||||
constexpr borealis::disc::Catalog DiscCatalog{
|
||||
.acceptedDiscs = AcceptedDiscs,
|
||||
.recognizedGameIds = RecognizedGameIds,
|
||||
};
|
||||
|
||||
static ValidationError convert_nod_error(NodResult result) {
|
||||
switch (result) {
|
||||
case NOD_RESULT_ERR_IO:
|
||||
ValidationError validation_error(borealis::disc::Status status) noexcept {
|
||||
switch (status) {
|
||||
case borealis::disc::Status::Success:
|
||||
return ValidationError::Success;
|
||||
case borealis::disc::Status::IOError:
|
||||
return ValidationError::IOError;
|
||||
case NOD_RESULT_ERR_FORMAT:
|
||||
case borealis::disc::Status::InvalidImage:
|
||||
return ValidationError::InvalidImage;
|
||||
case borealis::disc::Status::UnknownGame:
|
||||
return ValidationError::WrongGame;
|
||||
case borealis::disc::Status::UnsupportedVersion:
|
||||
return ValidationError::WrongVersion;
|
||||
case borealis::disc::Status::Canceled:
|
||||
return ValidationError::Canceled;
|
||||
case borealis::disc::Status::HashMismatch:
|
||||
return ValidationError::HashMismatch;
|
||||
case borealis::disc::Status::Failed:
|
||||
default:
|
||||
return ValidationError::Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
s64 StreamReadAt(void* user_data, u64 offset, void* out, size_t len) {
|
||||
if (len == 0) {
|
||||
return 0;
|
||||
}
|
||||
auto* io = static_cast<SDL_IOStream*>(user_data);
|
||||
const auto ret = SDL_SeekIO(io, static_cast<s64>(offset), SDL_IO_SEEK_SET);
|
||||
if (ret < 0) {
|
||||
return -1;
|
||||
}
|
||||
const auto read = SDL_ReadIO(io, out, len);
|
||||
if (read == 0) {
|
||||
if (SDL_GetIOStatus(io) == SDL_IO_STATUS_EOF) {
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return static_cast<s64>(read);
|
||||
}
|
||||
|
||||
s64 StreamLength(void* user_data) {
|
||||
return SDL_GetIOSize(static_cast<SDL_IOStream*>(user_data));
|
||||
}
|
||||
|
||||
void StreamClose(void* user_data) {
|
||||
SDL_CloseIO(static_cast<SDL_IOStream*>(user_data));
|
||||
}
|
||||
|
||||
ValidationError verify_disc(NodHandle* disc, VerificationStatus& status) {
|
||||
std::unique_ptr<XXH3_state_t, decltype(&XXH3_freeState)> hashState(
|
||||
XXH3_createState(), XXH3_freeState);
|
||||
if (!hashState) {
|
||||
return ValidationError::Unknown;
|
||||
}
|
||||
XXH3_128bits_reset(hashState.get());
|
||||
|
||||
while (true) {
|
||||
if (status.shouldCancel.load(std::memory_order_relaxed)) {
|
||||
return ValidationError::Canceled;
|
||||
}
|
||||
|
||||
size_t bytesAvail;
|
||||
const auto buf = nod_buf_read(disc, &bytesAvail);
|
||||
if (!bytesAvail)
|
||||
Region region_from_game_id(std::string_view gameId) noexcept {
|
||||
if (gameId.size() >= 4) {
|
||||
switch (gameId[3]) {
|
||||
case 'P':
|
||||
return Region::Europe;
|
||||
case 'J':
|
||||
return Region::Japan;
|
||||
case 'K':
|
||||
return Region::Korea;
|
||||
default:
|
||||
break;
|
||||
|
||||
XXH3_128bits_update(hashState.get(), buf, bytesAvail);
|
||||
|
||||
status.bytesRead.fetch_add(bytesAvail, std::memory_order_relaxed);
|
||||
nod_buf_consume(disc, bytesAvail);
|
||||
}
|
||||
}
|
||||
|
||||
const auto hash = XXH3_128bits_digest(hashState.get());
|
||||
if (!XXH128_isEqual(hash, status.knownDisc->hash)) {
|
||||
return ValidationError::HashMismatch;
|
||||
}
|
||||
return ValidationError::Success;
|
||||
return Region::NorthAmerica;
|
||||
}
|
||||
|
||||
void update_info(const borealis::disc::Result& result, DiscInfo& info) noexcept {
|
||||
if (!result.metadata.gameId.empty()) {
|
||||
info.platform = result.metadata.platform;
|
||||
info.region = region_from_game_id(result.metadata.gameId);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ValidationError validate(const char* path, VerificationStatus& status, DiscInfo& info) {
|
||||
const auto sdlStream = SDL_IOFromFile(path, "rb");
|
||||
if (sdlStream == nullptr) {
|
||||
return ValidationError::IOError;
|
||||
}
|
||||
|
||||
NodHandleWrapper disc;
|
||||
const NodDiscStream nod_stream{
|
||||
.user_data = sdlStream,
|
||||
.read_at = StreamReadAt,
|
||||
.stream_len = StreamLength,
|
||||
.close = StreamClose,
|
||||
};
|
||||
auto result = nod_disc_open_stream(&nod_stream, nullptr, &disc.handle);
|
||||
if (disc.handle == nullptr || result != NOD_RESULT_OK) {
|
||||
return convert_nod_error(result);
|
||||
}
|
||||
|
||||
status.bytesTotal.store(nod_disc_size(disc.handle), std::memory_order_relaxed);
|
||||
|
||||
NodDiscHeader header{};
|
||||
result = nod_disc_header(disc.handle, &header);
|
||||
if (result != NOD_RESULT_OK) {
|
||||
return convert_nod_error(result);
|
||||
}
|
||||
|
||||
const auto knownDisc = find_disc(std::string_view(header.game_id, 6));
|
||||
if (!knownDisc) {
|
||||
return ValidationError::WrongGame;
|
||||
}
|
||||
status.knownDisc = knownDisc;
|
||||
|
||||
info.platform = knownDisc->platform;
|
||||
info.region = knownDisc->region;
|
||||
if (!knownDisc->supported) {
|
||||
return ValidationError::WrongVersion;
|
||||
}
|
||||
return verify_disc(disc.handle, status);
|
||||
const auto result = borealis::disc::verify(
|
||||
path == nullptr ? std::string_view{} : std::string_view{path}, DiscCatalog, &status);
|
||||
update_info(result, info);
|
||||
return validation_error(result.status);
|
||||
}
|
||||
|
||||
ValidationError inspect(const char* path, DiscInfo& info) {
|
||||
const auto sdlStream = SDL_IOFromFile(path, "rb");
|
||||
if (sdlStream == nullptr) {
|
||||
return ValidationError::IOError;
|
||||
}
|
||||
|
||||
NodHandleWrapper disc;
|
||||
const NodDiscStream nod_stream{
|
||||
.user_data = sdlStream,
|
||||
.read_at = StreamReadAt,
|
||||
.stream_len = StreamLength,
|
||||
.close = StreamClose,
|
||||
};
|
||||
auto result = nod_disc_open_stream(&nod_stream, nullptr, &disc.handle);
|
||||
if (disc.handle == nullptr || result != NOD_RESULT_OK) {
|
||||
return convert_nod_error(result);
|
||||
}
|
||||
|
||||
NodDiscHeader header{};
|
||||
result = nod_disc_header(disc.handle, &header);
|
||||
if (result != NOD_RESULT_OK) {
|
||||
return convert_nod_error(result);
|
||||
}
|
||||
|
||||
const auto knownDisc = find_disc(std::string_view(header.game_id, 6));
|
||||
if (!knownDisc) {
|
||||
return ValidationError::WrongGame;
|
||||
}
|
||||
|
||||
info.platform = knownDisc->platform;
|
||||
info.region = knownDisc->region;
|
||||
if (!knownDisc->supported) {
|
||||
return ValidationError::WrongVersion;
|
||||
}
|
||||
return ValidationError::Success;
|
||||
const auto result = borealis::disc::inspect(
|
||||
path == nullptr ? std::string_view{} : std::string_view{path}, DiscCatalog);
|
||||
update_info(result, info);
|
||||
return validation_error(result.status);
|
||||
}
|
||||
|
||||
bool isPal(const char* path) {
|
||||
|
||||
+13
-15
@@ -1,12 +1,18 @@
|
||||
#ifndef DUSK_ISO_VALIDATE_HPP
|
||||
#define DUSK_ISO_VALIDATE_HPP
|
||||
|
||||
#include <atomic>
|
||||
#include <borealis/disc.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
|
||||
namespace dusk {
|
||||
enum class DiscVerificationState : uint8_t;
|
||||
}
|
||||
|
||||
namespace dusk::iso {
|
||||
struct KnownDisc;
|
||||
|
||||
enum class ValidationError : u8 {
|
||||
enum class ValidationError : uint8_t {
|
||||
Unknown = 0,
|
||||
IOError,
|
||||
InvalidImage,
|
||||
@@ -17,27 +23,19 @@ enum class ValidationError : u8 {
|
||||
Success
|
||||
};
|
||||
|
||||
enum class Platform : u8 {
|
||||
GameCube,
|
||||
Wii,
|
||||
};
|
||||
using Platform = borealis::disc::Platform;
|
||||
|
||||
enum class Region : u8 {
|
||||
enum class Region : uint8_t {
|
||||
NorthAmerica,
|
||||
Europe,
|
||||
Japan,
|
||||
Korea,
|
||||
};
|
||||
|
||||
struct VerificationStatus {
|
||||
std::atomic_size_t bytesRead = 0;
|
||||
std::atomic_size_t bytesTotal = 0;
|
||||
const KnownDisc* knownDisc = nullptr;
|
||||
std::atomic_bool shouldCancel = false;
|
||||
};
|
||||
using VerificationStatus = borealis::disc::Progress;
|
||||
|
||||
struct DiscInfo {
|
||||
Platform platform = Platform::GameCube;
|
||||
Platform platform = Platform::Unknown;
|
||||
Region region = Region::NorthAmerica;
|
||||
};
|
||||
|
||||
|
||||
+23
-364
@@ -1,35 +1,15 @@
|
||||
#include "dusk/logging.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "dusk/io.hpp"
|
||||
#include <string_view>
|
||||
|
||||
#include "tracy/Tracy.hpp"
|
||||
|
||||
#if TARGET_ANDROID
|
||||
#include "android/log.h"
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
#endif
|
||||
|
||||
bool StubLogEnabled = true;
|
||||
|
||||
using namespace std::literals::string_view_literals;
|
||||
|
||||
// MSVC is broken and seemingly miscompiles std::string_view::npos without this.
|
||||
// I wish I was joking.
|
||||
constexpr size_t npos = std::string_view::npos;
|
||||
|
||||
static constexpr std::string_view StubFragments[] = {
|
||||
namespace {
|
||||
constexpr std::string_view kStubFragments[] = {
|
||||
"is a stub"sv,
|
||||
"Unimplemented: BP register"sv,
|
||||
"Unhandled BP register"sv,
|
||||
@@ -37,354 +17,33 @@ static constexpr std::string_view StubFragments[] = {
|
||||
"but selective updates are not implemented"sv,
|
||||
};
|
||||
|
||||
#if _WIN32
|
||||
#define DUSK_FILENO _fileno
|
||||
#else
|
||||
#define DUSK_FILENO fileno
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
// On macOS, std::mutex becomes poisoned when its dtor is run.
|
||||
// We use this to check if the LogState is destroyed before attempting to acquire it.
|
||||
std::atomic g_logStateAlive(true);
|
||||
std::atomic<int> g_logFd(-1);
|
||||
constexpr size_t MaxRetainedLogCount = 10;
|
||||
constexpr size_t MaxRetainedOldLogCount = MaxRetainedLogCount - 1;
|
||||
constexpr uintmax_t MaxRetainedOldLogBytes = 100ull * 1024ull * 1024ull;
|
||||
|
||||
struct LogState {
|
||||
std::mutex mutex;
|
||||
FILE* file = nullptr;
|
||||
std::u8string filePath;
|
||||
|
||||
~LogState() {
|
||||
CloseFile();
|
||||
g_logStateAlive.store(false, std::memory_order_release);
|
||||
}
|
||||
|
||||
void CloseFile() {
|
||||
if (!g_logStateAlive.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard lock(mutex);
|
||||
if (file != nullptr) {
|
||||
g_logFd.store(-1, std::memory_order_release);
|
||||
std::fflush(file);
|
||||
std::fclose(file);
|
||||
file = nullptr;
|
||||
}
|
||||
}
|
||||
};
|
||||
LogState g_logState;
|
||||
|
||||
const char* LogLevelString(AuroraLogLevel level) {
|
||||
switch (level) {
|
||||
case LOG_DEBUG:
|
||||
return "DEBUG";
|
||||
case LOG_INFO:
|
||||
return "INFO";
|
||||
case LOG_WARNING:
|
||||
return "WARNING";
|
||||
case LOG_ERROR:
|
||||
return "ERROR";
|
||||
case LOG_FATAL:
|
||||
return "FATAL";
|
||||
}
|
||||
|
||||
return "??";
|
||||
}
|
||||
|
||||
FILE* LogStreamForLevel(AuroraLogLevel level) {
|
||||
return level >= LOG_ERROR ? stderr : stdout;
|
||||
}
|
||||
|
||||
struct LogFileCandidate {
|
||||
std::filesystem::path path;
|
||||
std::string filename;
|
||||
uintmax_t size;
|
||||
};
|
||||
|
||||
void warn_log_cleanup_failure(
|
||||
const char* action, const std::filesystem::path& path, const std::error_code& ec) {
|
||||
std::fprintf(stderr, "[WARNING | dusk] Failed to %s '%s': %s\n", action,
|
||||
dusk::io::fs_path_to_string(path).c_str(), ec.message().c_str());
|
||||
}
|
||||
|
||||
bool is_digit_at(const std::string_view value, size_t index) {
|
||||
return std::isdigit(static_cast<unsigned char>(value[index])) != 0;
|
||||
}
|
||||
|
||||
bool is_generated_log_file_name(const std::filesystem::path& path) {
|
||||
const std::string filename = path.filename().string();
|
||||
constexpr std::string_view currentPrefix = "dusklight-"sv;
|
||||
constexpr std::string_view legacyPrefix = "dusk-"sv;
|
||||
constexpr std::string_view suffix = ".log"sv;
|
||||
size_t timestampOffset = 0;
|
||||
|
||||
if (filename.starts_with(currentPrefix)) {
|
||||
timestampOffset = currentPrefix.size();
|
||||
} else if (filename.starts_with(legacyPrefix)) {
|
||||
timestampOffset = legacyPrefix.size();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (filename.size() != timestampOffset + 19 || !filename.ends_with(suffix) ||
|
||||
filename[timestampOffset + 8] != '-') {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = timestampOffset; i < timestampOffset + 8; ++i) {
|
||||
if (!is_digit_at(filename, i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (size_t i = timestampOffset + 9; i < timestampOffset + 15; ++i) {
|
||||
if (!is_digit_at(filename, i)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void delete_log_file(const std::filesystem::path& path) {
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(path, ec);
|
||||
if (ec) {
|
||||
warn_log_cleanup_failure("remove old log file", path, ec);
|
||||
}
|
||||
}
|
||||
|
||||
void prune_old_log_files(const std::filesystem::path& logsDir) {
|
||||
std::error_code ec;
|
||||
std::filesystem::directory_iterator entries{logsDir, ec};
|
||||
if (ec) {
|
||||
warn_log_cleanup_failure("inspect log directory", logsDir, ec);
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<LogFileCandidate> candidates;
|
||||
for (const auto& entry : entries) {
|
||||
const std::filesystem::path path = entry.path();
|
||||
if (!is_generated_log_file_name(path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ec.clear();
|
||||
const auto status = entry.symlink_status(ec);
|
||||
if (ec) {
|
||||
warn_log_cleanup_failure("inspect log file", path, ec);
|
||||
continue;
|
||||
}
|
||||
if (!std::filesystem::is_regular_file(status)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ec.clear();
|
||||
const uintmax_t size = entry.file_size(ec);
|
||||
if (ec) {
|
||||
warn_log_cleanup_failure("inspect size of log file", path, ec);
|
||||
continue;
|
||||
}
|
||||
|
||||
candidates.push_back({path, path.filename().string(), size});
|
||||
}
|
||||
|
||||
std::sort(candidates.begin(), candidates.end(),
|
||||
[](const LogFileCandidate& a, const LogFileCandidate& b) {
|
||||
return a.filename > b.filename;
|
||||
});
|
||||
|
||||
const size_t retainedCount = std::min(candidates.size(), MaxRetainedOldLogCount);
|
||||
uintmax_t retainedBytes = 0;
|
||||
for (size_t i = 0; i < retainedCount; ++i) {
|
||||
retainedBytes += candidates[i].size;
|
||||
}
|
||||
|
||||
size_t retainedAfterSizeLimit = retainedCount;
|
||||
while (retainedAfterSizeLimit > 0 && retainedBytes > MaxRetainedOldLogBytes) {
|
||||
--retainedAfterSizeLimit;
|
||||
retainedBytes -= candidates[retainedAfterSizeLimit].size;
|
||||
}
|
||||
|
||||
for (size_t i = retainedAfterSizeLimit; i < candidates.size(); ++i) {
|
||||
delete_log_file(candidates[i].path);
|
||||
}
|
||||
}
|
||||
|
||||
std::string MakeTimestampedLogName() {
|
||||
const auto now = std::chrono::system_clock::now();
|
||||
const std::time_t nowTime = std::chrono::system_clock::to_time_t(now);
|
||||
|
||||
std::tm localTime{};
|
||||
#if _WIN32
|
||||
localtime_s(&localTime, &nowTime);
|
||||
#else
|
||||
localtime_r(&nowTime, &localTime);
|
||||
#endif
|
||||
|
||||
std::array<char, 32> buffer{};
|
||||
std::strftime(buffer.data(), buffer.size(), "dusklight-%Y%m%d-%H%M%S.log", &localTime);
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
void WriteLogLine(FILE* out, const char* levelStr, const char* module, const char* message, unsigned int len) {
|
||||
if (out == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::fprintf(out, "[%s | %s] ", levelStr, module);
|
||||
std::fwrite(message, 1, len, out);
|
||||
std::fputc('\n', out);
|
||||
std::fflush(out);
|
||||
}
|
||||
|
||||
void WriteLogLineToFile(
|
||||
const char* levelStr, const char* module, const char* message, unsigned int len) {
|
||||
if (g_logStateAlive.load(std::memory_order_acquire)) {
|
||||
std::lock_guard lock(g_logState.mutex);
|
||||
if (g_logState.file != nullptr) {
|
||||
WriteLogLine(g_logState.file, levelStr, module, message, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
static bool IsForStubLog(const char* message) {
|
||||
std::string_view msg_view(message);
|
||||
|
||||
for (auto& fragment : StubFragments) {
|
||||
if (msg_view.find(fragment) != ""sv.npos) {
|
||||
bool is_for_stub_log(const std::string_view message) {
|
||||
for (const auto& fragment : kStubFragments) {
|
||||
if (message.find(fragment) != std::string_view::npos) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#if TARGET_ANDROID
|
||||
void aurora_log_callback(AuroraLogLevel level, const char* module, const char* message,
|
||||
unsigned int len) {
|
||||
bool divert_stub_messages(const borealis::log::Message& message) {
|
||||
ZoneScoped;
|
||||
if (StubLogEnabled && level != LOG_FATAL && IsForStubLog(message)) {
|
||||
dusk::SendToStubLog(level, module, message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (module == nullptr) {
|
||||
module = "";
|
||||
}
|
||||
|
||||
const char* levelStr = LogLevelString(level);
|
||||
int android_log_level = 0;
|
||||
switch (level) {
|
||||
case LOG_DEBUG:
|
||||
android_log_level = ANDROID_LOG_DEBUG;
|
||||
break;
|
||||
case LOG_INFO:
|
||||
android_log_level = ANDROID_LOG_INFO;
|
||||
break;
|
||||
case LOG_WARNING:
|
||||
android_log_level = ANDROID_LOG_WARN;
|
||||
break;
|
||||
case LOG_ERROR:
|
||||
android_log_level = ANDROID_LOG_ERROR;
|
||||
break;
|
||||
case LOG_FATAL:
|
||||
android_log_level = ANDROID_LOG_FATAL;
|
||||
break;
|
||||
}
|
||||
|
||||
std::stringstream msgStream(std::string(message, len));
|
||||
std::string segment;
|
||||
while(std::getline(msgStream, segment)) {
|
||||
__android_log_print(android_log_level, module, "%s\n", segment.c_str());
|
||||
}
|
||||
|
||||
WriteLogLineToFile(levelStr, module, message, len);
|
||||
|
||||
if (level == LOG_FATAL) {
|
||||
abort();
|
||||
if (!StubLogEnabled || !is_for_stub_log(message.text)) {
|
||||
return false;
|
||||
}
|
||||
dusk::SendToStubLog(message.level, message.module, message.text);
|
||||
return true;
|
||||
}
|
||||
#else
|
||||
void aurora_log_callback(AuroraLogLevel level, const char* module, const char* message,
|
||||
unsigned int len) {
|
||||
ZoneScoped;
|
||||
if (StubLogEnabled && level != LOG_FATAL && IsForStubLog(message)) {
|
||||
dusk::SendToStubLog(level, module, message);
|
||||
return;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
if (module == nullptr) {
|
||||
module = "";
|
||||
}
|
||||
|
||||
const char* levelStr = LogLevelString(level);
|
||||
FILE* out = LogStreamForLevel(level);
|
||||
WriteLogLine(out, levelStr, module, message, len);
|
||||
WriteLogLineToFile(levelStr, module, message, len);
|
||||
|
||||
if (level == LOG_FATAL) {
|
||||
abort();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
aurora::Module DuskLog("dusk");
|
||||
|
||||
void dusk::InitializeFileLogging(const std::filesystem::path& configDir, AuroraLogLevel logLevel) {
|
||||
if (!g_logStateAlive.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
std::lock_guard lock(g_logState.mutex);
|
||||
if (g_logState.file != nullptr || configDir.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
const std::filesystem::path logsDir = configDir / "logs";
|
||||
std::filesystem::create_directories(logsDir, ec);
|
||||
if (ec) {
|
||||
std::fprintf(stderr, "[WARNING | dusk] Failed to create log directory '%s': %s\n",
|
||||
io::fs_path_to_string(logsDir).c_str(), ec.message().c_str());
|
||||
return;
|
||||
}
|
||||
prune_old_log_files(logsDir);
|
||||
|
||||
const std::filesystem::path logPath = logsDir / MakeTimestampedLogName();
|
||||
g_logState.file = io::FileStream::Create(logPath).ToInner();
|
||||
if (g_logState.file == nullptr) {
|
||||
std::fprintf(stderr, "[WARNING | dusk] Failed to open log file '%s'\n",
|
||||
io::fs_path_to_string(logPath).c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
g_logState.filePath = logPath.u8string();
|
||||
g_logFd.store(DUSK_FILENO(g_logState.file), std::memory_order_release);
|
||||
aurora::g_config.logCallback = &aurora_log_callback;
|
||||
aurora::g_config.logLevel = logLevel;
|
||||
WriteLogLine(g_logState.file, "INFO", "dusk", "File logging initialized", 24);
|
||||
}
|
||||
|
||||
void dusk::ShutdownFileLogging() {
|
||||
if (!g_logStateAlive.load(std::memory_order_acquire)) {
|
||||
return;
|
||||
}
|
||||
g_logState.CloseFile();
|
||||
}
|
||||
|
||||
const char* dusk::GetLogFilePath() {
|
||||
if (!g_logStateAlive.load(std::memory_order_acquire)) {
|
||||
return nullptr;
|
||||
}
|
||||
std::lock_guard lock(g_logState.mutex);
|
||||
return reinterpret_cast<const char*>(
|
||||
g_logState.filePath.empty() ? nullptr : g_logState.filePath.c_str());
|
||||
}
|
||||
|
||||
int dusk::GetLogFileDescriptor() {
|
||||
return g_logFd.load(std::memory_order_acquire);
|
||||
void dusk::InitializeLogging(
|
||||
const std::filesystem::path& cacheDir, const borealis::cli::StandardOptions& standard) {
|
||||
borealis::log::Options options{};
|
||||
options.level = borealis::LogLevel::Debug;
|
||||
options.fileDirectory = cacheDir.empty() ? std::filesystem::path{} : cacheDir / "logs";
|
||||
options.filePrefix = "dusklight";
|
||||
options.legacyFilePrefixes = {"dusk"};
|
||||
options.divert = &divert_stub_messages;
|
||||
standard.apply_to(options);
|
||||
borealis::log::init(options);
|
||||
}
|
||||
|
||||
+10
-13
@@ -1,23 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <aurora/aurora.h>
|
||||
#include <aurora/lib/logging.hpp>
|
||||
#include <borealis/cli.hpp>
|
||||
#include <borealis/log.hpp>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
void aurora_log_callback(AuroraLogLevel level, const char* module, const char* message, unsigned int len);
|
||||
#include <string_view>
|
||||
|
||||
namespace dusk {
|
||||
void InitializeFileLogging(const std::filesystem::path& configDir, AuroraLogLevel logLevel);
|
||||
void ShutdownFileLogging();
|
||||
const char* GetLogFilePath();
|
||||
int GetLogFileDescriptor();
|
||||
void SendToStubLog(AuroraLogLevel level, const char* module, const char* message);
|
||||
}
|
||||
void InitializeLogging(
|
||||
const std::filesystem::path& cacheDir, const borealis::cli::StandardOptions& standard);
|
||||
void SendToStubLog(borealis::LogLevel level, std::string_view module, std::string_view message);
|
||||
} // namespace dusk
|
||||
|
||||
extern bool StubLogEnabled;
|
||||
|
||||
extern aurora::Module DuskLog;
|
||||
inline constexpr borealis::Log DuskLog{"dusk"};
|
||||
|
||||
#ifndef NDEBUG
|
||||
#define STUB_LOG() DuskLog.debug("{} is a stub", __FUNCTION__)
|
||||
@@ -26,8 +23,8 @@ extern aurora::Module DuskLog;
|
||||
#endif
|
||||
|
||||
#if TARGET_PC
|
||||
#define STUB_RET(...) \
|
||||
STUB_LOG(); \
|
||||
#define STUB_RET(...) \
|
||||
STUB_LOG(); \
|
||||
return __VA_ARGS__;
|
||||
|
||||
#else
|
||||
|
||||
+2
-1
@@ -8,6 +8,7 @@
|
||||
#include "d/actor/d_a_movie_player.h"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/io.hpp"
|
||||
#include <borealis/io.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -93,7 +94,7 @@ bool RestartProcess(int argc, char* argv[]) {
|
||||
|
||||
std::vector<std::string> args;
|
||||
args.reserve(static_cast<size_t>(std::max(argc, 1)));
|
||||
args.push_back(dusk::io::fs_path_to_string(executablePath));
|
||||
args.push_back(borealis::io::fs_path_to_string(executablePath));
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
args.emplace_back(argv[i] != nullptr ? argv[i] : "");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <utility>
|
||||
|
||||
#include "dusk/io.hpp"
|
||||
#include <borealis/io.hpp>
|
||||
#include "loader.hpp"
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
@@ -32,7 +33,7 @@ std::vector<std::string> ModBundleDisk::getFileNames() {
|
||||
|
||||
const auto& path = it->path();
|
||||
const auto relPath = fs::relative(path, root_path);
|
||||
auto string = io::fs_path_to_string(relPath);
|
||||
auto string = borealis::io::fs_path_to_string(relPath);
|
||||
if constexpr (fs::path::preferred_separator != '/') {
|
||||
// Convert \ to / on Windows
|
||||
for (auto& chr : string) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace dusk::mods::loader {
|
||||
namespace {
|
||||
aurora::Module Log{"dusk::mods::loader"};
|
||||
constexpr borealis::Log Log{"dusk::mods::loader"};
|
||||
|
||||
struct Edge {
|
||||
size_t provider;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/mod_loader.hpp"
|
||||
|
||||
#include <borealis/io.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
@@ -85,7 +87,7 @@ static constexpr std::string_view k_nativeLibName = ""sv;
|
||||
|
||||
namespace dusk::mods {
|
||||
namespace {
|
||||
aurora::Module Log{"dusk::mods::loader"};
|
||||
constexpr borealis::Log Log{"dusk::mods::loader"};
|
||||
ModLoader g_modLoader;
|
||||
constexpr std::string_view k_nativeLibDir = "lib/"sv;
|
||||
|
||||
@@ -282,7 +284,7 @@ static ModMetadata load_metadata(const std::filesystem::path& modPath, ModBundle
|
||||
validate_mod_id(metaId);
|
||||
|
||||
if (metaName.empty()) {
|
||||
metaName = io::fs_path_to_string(modPath.stem());
|
||||
metaName = borealis::io::fs_path_to_string(modPath.stem());
|
||||
}
|
||||
if (metaVersion.empty()) {
|
||||
metaVersion = "?"s;
|
||||
@@ -516,7 +518,7 @@ std::filesystem::path ModLoader::external_native_lib_path(const LoadedMod& mod)
|
||||
return {};
|
||||
}
|
||||
fs::path path = libDir / fs::path(mod.metadata.id +
|
||||
io::fs_path_to_string(fs::path(k_nativeLibName).extension()));
|
||||
borealis::io::fs_path_to_string(fs::path(k_nativeLibName).extension()));
|
||||
std::error_code ec;
|
||||
if (!fs::is_regular_file(path, ec)) {
|
||||
return {};
|
||||
@@ -544,7 +546,7 @@ void ModLoader::load_native(
|
||||
return;
|
||||
}
|
||||
mod.dir = fs::absolute(scratchDir);
|
||||
mod.dirUtf8 = io::fs_path_to_string(mod.dir);
|
||||
mod.dirUtf8 = borealis::io::fs_path_to_string(mod.dir);
|
||||
|
||||
fs::path libPath;
|
||||
fs::path runtimeDir;
|
||||
@@ -666,7 +668,7 @@ void ModLoader::load_native(
|
||||
|
||||
mod.nativePath = fs::absolute(libPath);
|
||||
mod.nativeDir = fs::absolute(runtimeDir);
|
||||
mod.nativeDirUtf8 = io::fs_path_to_string(mod.nativeDir);
|
||||
mod.nativeDirUtf8 = borealis::io::fs_path_to_string(mod.nativeDir);
|
||||
mod.native = std::move(nativeMod);
|
||||
mod.nativeStatus = NativeModStatus::Loaded;
|
||||
runtimeDirRollback.release();
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
|
||||
#if DUSK_HAS_PREPATCH
|
||||
#include <mach-o/dyld.h>
|
||||
@@ -19,7 +19,7 @@
|
||||
namespace dusk::mods::prepatch {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::prepatch");
|
||||
constexpr borealis::Log Log{"dusk::mods::prepatch"};
|
||||
|
||||
constexpr std::string_view kSiteMagic = "PS01";
|
||||
constexpr size_t kSiteHeaderSize = 12;
|
||||
|
||||
@@ -55,26 +55,26 @@ void emit(Source source, const std::string& modId, LogLevel level, const std::st
|
||||
slot.message.assign(message);
|
||||
}
|
||||
|
||||
AuroraLogLevel auroraLevel = LOG_INFO;
|
||||
auto logLevel = borealis::LogLevel::Info;
|
||||
switch (level) {
|
||||
case LOG_LEVEL_TRACE:
|
||||
logLevel = borealis::LogLevel::Trace;
|
||||
break;
|
||||
case LOG_LEVEL_DEBUG:
|
||||
auroraLevel = LOG_DEBUG;
|
||||
logLevel = borealis::LogLevel::Debug;
|
||||
break;
|
||||
case LOG_LEVEL_INFO:
|
||||
auroraLevel = LOG_INFO;
|
||||
logLevel = borealis::LogLevel::Info;
|
||||
break;
|
||||
case LOG_LEVEL_WARN:
|
||||
auroraLevel = LOG_WARNING;
|
||||
logLevel = borealis::LogLevel::Warning;
|
||||
break;
|
||||
case LOG_LEVEL_ERROR:
|
||||
auroraLevel = LOG_ERROR;
|
||||
logLevel = borealis::LogLevel::Error;
|
||||
break;
|
||||
}
|
||||
if (aurora::g_config.logLevel <= auroraLevel) {
|
||||
aurora::log_internal(auroraLevel, modId.c_str(), message.c_str(),
|
||||
static_cast<unsigned int>(message.length()));
|
||||
}
|
||||
const borealis::Log modLog{modId.c_str()};
|
||||
modLog.report(logLevel, "{}", message);
|
||||
}
|
||||
|
||||
Range copy_since(uint64_t sinceSeq, std::vector<Line>& out) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
#include <zstd.h>
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
@@ -28,7 +28,7 @@
|
||||
namespace dusk::mods::manifest {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::manifest");
|
||||
constexpr borealis::Log Log{"dusk::mods::manifest"};
|
||||
|
||||
constexpr char kMagic[8] = {'S', 'Y', 'M', 'G', 'E', 'N', '\0', '\0'};
|
||||
constexpr uint32_t kVersion = 2;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/config.h"
|
||||
@@ -21,7 +21,7 @@
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::config");
|
||||
constexpr borealis::Log Log{"dusk::mods::config"};
|
||||
|
||||
enum class ConfigSlotKind : uint8_t {
|
||||
Var,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "slot_map.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/gfx.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/gfx.h"
|
||||
@@ -25,7 +25,7 @@
|
||||
namespace dusk::mods {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::gfx");
|
||||
constexpr borealis::Log Log{"dusk::mods::gfx"};
|
||||
|
||||
enum class GfxSlotKind : uint8_t {
|
||||
DrawType,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <version.h>
|
||||
#include <borealis/version.h>
|
||||
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
@@ -135,7 +135,7 @@ void host_mod_detached(LoadedMod& mod) {
|
||||
|
||||
constinit HostService s_hostService{
|
||||
.header = SERVICE_HEADER(HostService, HOST_SERVICE_MAJOR, HOST_SERVICE_MINOR),
|
||||
.version = DUSK_VERSION_STRING,
|
||||
.version = BOREALIS_APP_VERSION,
|
||||
.build_id = nullptr,
|
||||
.build_id_len = 0,
|
||||
.get_service = host_get_service,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "aurora/dvd.h"
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/overlay.h"
|
||||
|
||||
@@ -19,7 +19,7 @@ using namespace std::string_literals;
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::overlay");
|
||||
constexpr borealis::Log Log{"dusk::mods::overlay"};
|
||||
|
||||
struct OverlayFileData {
|
||||
std::string bundlePath;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/resource.h"
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::resource");
|
||||
constexpr borealis::Log Log{"dusk::mods::resource"};
|
||||
|
||||
// Allocations by owning mod, so buffers still live when a mod detaches can be freed.
|
||||
std::unordered_map<void*, const LoadedMod*> s_buffers;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "registry.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "mods/svc/texture.h"
|
||||
|
||||
@@ -29,7 +29,7 @@ struct TextureRawData {
|
||||
uint32_t gxFormat = 0;
|
||||
};
|
||||
|
||||
aurora::Module Log("dusk::mods::textures");
|
||||
constexpr borealis::Log Log{"dusk::mods::textures"};
|
||||
|
||||
// Referenced by Aurora's lazy virtual-file reads (from arbitrary threads, under Aurora's registry
|
||||
// lock) and by raw-entry spans. Immutable after construction; freed only after the corresponding
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/mod_loader.hpp"
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
#include "dusk/ui/menu_bar.hpp"
|
||||
@@ -32,7 +32,7 @@
|
||||
namespace dusk::mods::svc::ui_impl {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::ui");
|
||||
constexpr borealis::Log Log{"dusk::mods::ui"};
|
||||
|
||||
enum class UiSlotKind : u8 {
|
||||
Window,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include "registry.hpp"
|
||||
#include "slot_map.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "dusk/mods/loader/loader.hpp"
|
||||
|
||||
#include <SDL3/SDL_error.h>
|
||||
@@ -19,7 +19,7 @@
|
||||
namespace dusk::mods::svc {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log("dusk::mods::window");
|
||||
constexpr borealis::Log Log{"dusk::mods::window"};
|
||||
|
||||
struct WindowSlot {
|
||||
SDL_Window* window = nullptr;
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#include "dusk/presentation.hpp"
|
||||
|
||||
#include "dusk/settings.h"
|
||||
|
||||
#include <borealis/presentation.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
namespace dusk::presentation {
|
||||
namespace {
|
||||
|
||||
float preferred_frame_rate() {
|
||||
switch (getSettings().game.enableFrameInterpolation.getValue()) {
|
||||
case FrameInterpMode::Off:
|
||||
return 30.0f;
|
||||
case FrameInterpMode::Capped:
|
||||
return static_cast<float>(std::max(getSettings().video.maxFrameRate.getValue(), 1));
|
||||
case FrameInterpMode::Unlimited:
|
||||
default:
|
||||
return 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void update_frame_rate_preference() {
|
||||
borealis::presentation::set_preferred_frame_rate(preferred_frame_rate());
|
||||
}
|
||||
|
||||
} // namespace dusk::presentation
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk::presentation {
|
||||
|
||||
void update_frame_rate_preference();
|
||||
|
||||
} // namespace dusk::presentation
|
||||
@@ -16,7 +16,7 @@ std::string mod_image_source(const mods::LoadedMod& mod, std::string_view bundle
|
||||
|
||||
#include <SDL3/SDL_iostream.h>
|
||||
#include <SDL3/SDL_surface.h>
|
||||
#include <aurora/lib/logging.hpp>
|
||||
#include <borealis/log.hpp>
|
||||
#include <aurora/rmlui.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
@@ -34,7 +34,7 @@ std::string mod_image_source(const mods::LoadedMod& mod, std::string_view bundle
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
aurora::Module Log{"dusk::ui::modTexture"};
|
||||
constexpr borealis::Log Log{"dusk::ui::modTexture"};
|
||||
|
||||
constexpr std::string_view kScheme = "mod";
|
||||
constexpr std::string_view kSourcePrefix = "mod://";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "overlay.hpp"
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include <borealis/log.hpp>
|
||||
#include "controller_config.hpp"
|
||||
#include "dusk/achievements.h"
|
||||
#include "dusk/action_bindings.h"
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
aurora::Module Log{"dusk::ui::overlay"};
|
||||
constexpr borealis::Log Log{"dusk::ui::overlay"};
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
|
||||
+60
-50
@@ -1,23 +1,23 @@
|
||||
#include "prelaunch.hpp"
|
||||
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/data.hpp"
|
||||
#include "dusk/file_select.hpp"
|
||||
#include "dusk/iso_validate.hpp"
|
||||
#include "dusk/main.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "dusk/update_check.hpp"
|
||||
#include "modal.hpp"
|
||||
#include "mods_window.hpp"
|
||||
#include "preset.hpp"
|
||||
#include "settings.hpp"
|
||||
#include "version.h"
|
||||
|
||||
#include <SDL3/SDL_dialog.h>
|
||||
#include <SDL3/SDL_error.h>
|
||||
#include <SDL3/SDL_misc.h>
|
||||
#include <aurora/lib/logging.hpp>
|
||||
#include <aurora/lib/window.hpp>
|
||||
#include <borealis/file_select.hpp>
|
||||
#include <borealis/log.hpp>
|
||||
#include <borealis/update.hpp>
|
||||
#include <borealis/version.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
namespace dusk::ui {
|
||||
namespace {
|
||||
aurora::Module PrelaunchLog{"dusk::ui::prelaunch"};
|
||||
constexpr borealis::Log PrelaunchLog{"dusk::ui::prelaunch"};
|
||||
|
||||
const Rml::String kDocumentSource = R"RML(
|
||||
<rml>
|
||||
@@ -72,10 +72,10 @@ const Rml::String kDocumentSource = R"RML(
|
||||
</rml>
|
||||
)RML";
|
||||
|
||||
constexpr std::array<SDL_DialogFileFilter, 2> kDiscFileFilters{{
|
||||
const std::vector<borealis::file_select::Filter> kDiscFileFilters{
|
||||
{"Game Disc Images", "iso;gcm;ciso;gcz;nfs;rvz;wbfs;wia;tgc"},
|
||||
{"All Files", "*"},
|
||||
}};
|
||||
};
|
||||
|
||||
struct DiscVerificationResult {
|
||||
std::string path;
|
||||
@@ -102,7 +102,7 @@ struct DiscVerificationTask {
|
||||
}
|
||||
|
||||
~DiscVerificationTask() {
|
||||
status.shouldCancel.store(true, std::memory_order_relaxed);
|
||||
status.cancelRequested.store(true, std::memory_order_relaxed);
|
||||
join();
|
||||
}
|
||||
|
||||
@@ -129,15 +129,15 @@ struct UpdateCheckTask {
|
||||
UpdateCheckTask() {
|
||||
worker = std::thread([this] {
|
||||
try {
|
||||
result = update_check::check_latest_github_release("TwilitRealm", "dusklight");
|
||||
result = borealis::update::check_latest_github_release(AppInfo);
|
||||
} catch (const std::exception& e) {
|
||||
result = {
|
||||
.status = update_check::Status::Failed,
|
||||
.status = borealis::update::Status::Failed,
|
||||
.message = fmt::format("Update check failed with exception: {}", e.what()),
|
||||
};
|
||||
} catch (...) {
|
||||
result = {
|
||||
.status = update_check::Status::Failed,
|
||||
.status = borealis::update::Status::Failed,
|
||||
.message = "Update check failed with an unknown exception",
|
||||
};
|
||||
}
|
||||
@@ -155,13 +155,13 @@ struct UpdateCheckTask {
|
||||
|
||||
[[nodiscard]] bool finished() const { return done.load(std::memory_order_acquire); }
|
||||
|
||||
update_check::Result result;
|
||||
borealis::update::Result result;
|
||||
std::atomic_bool done = false;
|
||||
std::thread worker;
|
||||
};
|
||||
|
||||
std::unique_ptr<UpdateCheckTask> sUpdateCheckTask;
|
||||
std::optional<update_check::Result> sUpdateCheckResult;
|
||||
std::optional<borealis::update::Result> sUpdateCheckResult;
|
||||
|
||||
bool verification_state_allows_launch(iso::ValidationError validation) noexcept {
|
||||
return validation == iso::ValidationError::Unknown ||
|
||||
@@ -212,7 +212,7 @@ void begin_disc_verification(std::string path) noexcept {
|
||||
return;
|
||||
}
|
||||
if (sDiscVerificationTask != nullptr) {
|
||||
sDiscVerificationTask->status.shouldCancel.store(true, std::memory_order_relaxed);
|
||||
sDiscVerificationTask->status.cancelRequested.store(true, std::memory_order_relaxed);
|
||||
sDiscVerificationTask.reset();
|
||||
}
|
||||
sDiscVerificationTask = std::make_unique<DiscVerificationTask>(std::move(path));
|
||||
@@ -244,7 +244,7 @@ void begin_update_check() {
|
||||
sUpdateCheckTask = std::make_unique<UpdateCheckTask>();
|
||||
}
|
||||
|
||||
std::optional<update_check::Result> take_finished_update_check() {
|
||||
std::optional<borealis::update::Result> take_finished_update_check() {
|
||||
if (sUpdateCheckTask == nullptr || !sUpdateCheckTask->finished()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -255,7 +255,7 @@ std::optional<update_check::Result> take_finished_update_check() {
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string update_release_label(const update_check::Release& release) {
|
||||
std::string update_release_label(const borealis::update::Release& release) {
|
||||
std::string_view tagName = release.tagName;
|
||||
if (!tagName.empty() && tagName.front() == 'v') {
|
||||
tagName.remove_prefix(1);
|
||||
@@ -265,7 +265,7 @@ std::string update_release_label(const update_check::Release& release) {
|
||||
|
||||
void open_update_release() {
|
||||
if (!sUpdateCheckResult.has_value() ||
|
||||
sUpdateCheckResult->status != update_check::Status::UpdateAvailable)
|
||||
sUpdateCheckResult->status != borealis::update::Status::UpdateAvailable)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -438,7 +438,7 @@ private:
|
||||
}
|
||||
|
||||
mCancelRequested = true;
|
||||
sDiscVerificationTask->status.shouldCancel.store(true, std::memory_order_relaxed);
|
||||
sDiscVerificationTask->status.cancelRequested.store(true, std::memory_order_relaxed);
|
||||
if (mCancelButton != nullptr) {
|
||||
mCancelButton->set_text("Cancelling...");
|
||||
mCancelButton->set_disabled(true);
|
||||
@@ -455,7 +455,7 @@ private:
|
||||
}
|
||||
|
||||
if (mFileName != nullptr) {
|
||||
std::string fileName = display_name_for_path(sDiscVerificationTask->path);
|
||||
std::string fileName = borealis::file_select::display_name(sDiscVerificationTask->path);
|
||||
if (fileName.empty()) {
|
||||
fileName = sDiscVerificationTask->path;
|
||||
}
|
||||
@@ -496,12 +496,15 @@ private:
|
||||
bool mFinished = false;
|
||||
};
|
||||
|
||||
void file_dialog_callback(void*, const char* path, const char* error) {
|
||||
if (path == nullptr || error != nullptr) {
|
||||
void file_dialog_callback(borealis::file_select::Result result) {
|
||||
if (result.status != borealis::file_select::Status::Selected || result.locations.empty()) {
|
||||
if (result.status == borealis::file_select::Status::Failed) {
|
||||
PrelaunchLog.warn("File selection failed: {}", result.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
begin_disc_verification(path);
|
||||
begin_disc_verification(result.locations.front());
|
||||
}
|
||||
|
||||
PrelaunchState sPrelaunchState;
|
||||
@@ -649,8 +652,12 @@ void ensure_initialized() noexcept {
|
||||
|
||||
void open_iso_picker() noexcept {
|
||||
ensure_initialized();
|
||||
ShowFileSelect(&file_dialog_callback, nullptr, aurora::window::get_sdl_window(),
|
||||
kDiscFileFilters.data(), kDiscFileFilters.size(), nullptr, false);
|
||||
borealis::file_select::open_file(
|
||||
{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.filters = kDiscFileFilters,
|
||||
},
|
||||
&file_dialog_callback);
|
||||
}
|
||||
|
||||
bool is_restart_pending() noexcept {
|
||||
@@ -888,32 +895,35 @@ void Prelaunch::update() {
|
||||
Rml::String innerRML = "";
|
||||
|
||||
switch (state.activeDiscInfo.platform) {
|
||||
case iso::Platform::GameCube:
|
||||
innerRML += "GameCube";
|
||||
break;
|
||||
case iso::Platform::Wii:
|
||||
innerRML += "Wii";
|
||||
break;
|
||||
case iso::Platform::Unknown:
|
||||
innerRML += "Unknown";
|
||||
break;
|
||||
case iso::Platform::GameCube:
|
||||
innerRML += "GameCube";
|
||||
break;
|
||||
case iso::Platform::Wii:
|
||||
innerRML += "Wii";
|
||||
break;
|
||||
}
|
||||
|
||||
innerRML += " • ";
|
||||
|
||||
switch (state.activeDiscInfo.region) {
|
||||
case iso::Region::Japan:
|
||||
innerRML += "JPN";
|
||||
break;
|
||||
case iso::Region::Europe:
|
||||
innerRML += "EUR";
|
||||
break;
|
||||
case iso::Region::NorthAmerica:
|
||||
innerRML += "USA";
|
||||
break;
|
||||
case iso::Region::Korea:
|
||||
innerRML += "KOR";
|
||||
break;
|
||||
default:
|
||||
innerRML += "Unknown";
|
||||
break;
|
||||
case iso::Region::Japan:
|
||||
innerRML += "JPN";
|
||||
break;
|
||||
case iso::Region::Europe:
|
||||
innerRML += "EUR";
|
||||
break;
|
||||
case iso::Region::NorthAmerica:
|
||||
innerRML += "USA";
|
||||
break;
|
||||
case iso::Region::Korea:
|
||||
innerRML += "KOR";
|
||||
break;
|
||||
default:
|
||||
innerRML += "Unknown";
|
||||
break;
|
||||
}
|
||||
mDiscDetail->SetInnerRML(innerRML);
|
||||
} else {
|
||||
@@ -921,7 +931,7 @@ void Prelaunch::update() {
|
||||
}
|
||||
}
|
||||
if (mVersion != nullptr) {
|
||||
std::string_view versionStr(DUSK_WC_DESCRIBE);
|
||||
std::string_view versionStr(BOREALIS_APP_DESCRIBE);
|
||||
if (versionStr[0] == 'v') {
|
||||
versionStr = versionStr.substr(1);
|
||||
}
|
||||
@@ -929,7 +939,7 @@ void Prelaunch::update() {
|
||||
}
|
||||
if (mUpdateStatus != nullptr && mUpdateMessage != nullptr) {
|
||||
if (auto result = take_finished_update_check()) {
|
||||
if (result->status == update_check::Status::Failed) {
|
||||
if (result->status == borealis::update::Status::Failed) {
|
||||
PrelaunchLog.error("Failed to check for updates: {}", result->message);
|
||||
}
|
||||
sUpdateCheckResult = std::move(*result);
|
||||
@@ -939,11 +949,11 @@ void Prelaunch::update() {
|
||||
mUpdateStatus->SetAttribute("state", "checking");
|
||||
mUpdateMessage->SetInnerRML("Checking for updates...");
|
||||
} else if (!sUpdateCheckResult.has_value() ||
|
||||
sUpdateCheckResult->status == update_check::Status::UpToDate)
|
||||
sUpdateCheckResult->status == borealis::update::Status::UpToDate)
|
||||
{
|
||||
mUpdateStatus->RemoveAttribute("state");
|
||||
mUpdateMessage->SetInnerRML("");
|
||||
} else if (sUpdateCheckResult->status == update_check::Status::UpdateAvailable) {
|
||||
} else if (sUpdateCheckResult->status == borealis::update::Status::UpdateAvailable) {
|
||||
mUpdateStatus->SetAttribute("state", "available");
|
||||
mUpdateMessage->SetInnerRML("Update available!");
|
||||
if (mUpdateDownloadLabel != nullptr) {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
#if BOREALIS_HAS_SENTRY
|
||||
|
||||
#include "reporting.hpp"
|
||||
|
||||
#include "button.hpp"
|
||||
#include "dusk/crash_reporting.h"
|
||||
#include "ui.hpp"
|
||||
|
||||
#include <borealis/sentry.hpp>
|
||||
#include <dolphin/gx/GXAurora.h>
|
||||
|
||||
namespace dusk::ui {
|
||||
@@ -45,11 +45,11 @@ CrashReportWindow::CrashReportWindow() : WindowSmall("modal", "modal-dialog") {
|
||||
{"Enable",
|
||||
"Send crash reports to Dusklight developers. Reports will include the information described "
|
||||
"above.",
|
||||
[] { crash_reporting::set_consent(true); }},
|
||||
[] { borealis::sentry::set_consent(true); }},
|
||||
{"Disable",
|
||||
"Do not send crash reports. This may make it more difficult to resolve issues you "
|
||||
"encounter.",
|
||||
[] { crash_reporting::set_consent(false); }},
|
||||
[] { borealis::sentry::set_consent(false); }},
|
||||
};
|
||||
|
||||
for (const auto& option : kOptions) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
#if BOREALIS_HAS_SENTRY
|
||||
|
||||
#include "component.hpp"
|
||||
#include "window.hpp"
|
||||
|
||||
+65
-56
@@ -6,13 +6,14 @@
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/audio/DuskAudioSystem.h"
|
||||
#include "dusk/audio/DuskDsp.hpp"
|
||||
#include "dusk/android_frame_rate.hpp"
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/hotkeys.h"
|
||||
#include "dusk/data.hpp"
|
||||
#include "dusk/file_select.hpp"
|
||||
#include "dusk/imgui/ImGuiEngine.hpp"
|
||||
#include "dusk/io.hpp"
|
||||
#include "dusk/presentation.hpp"
|
||||
#include <borealis/io.hpp>
|
||||
#include <borealis/file_select.hpp>
|
||||
#include "dusk/livesplit.h"
|
||||
#include "dusk/discord_presence.hpp"
|
||||
#include "dusk/speedrun.h"
|
||||
@@ -31,8 +32,8 @@
|
||||
#include <SDL3/SDL_filesystem.h>
|
||||
#include <fmt/format.h>
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
#include "dusk/crash_reporting.h"
|
||||
#if BOREALIS_HAS_SENTRY
|
||||
#include <borealis/sentry.hpp>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
@@ -225,7 +226,7 @@ Rml::String configured_data_path_display_name() {
|
||||
return "(none)";
|
||||
}
|
||||
|
||||
auto display = display_name_for_path(path);
|
||||
auto display = borealis::file_select::display_name(path);
|
||||
if (display.empty()) {
|
||||
return path;
|
||||
}
|
||||
@@ -274,17 +275,17 @@ void show_data_folder_error_modal(std::string_view message) {
|
||||
}
|
||||
}
|
||||
|
||||
void data_folder_dialog_callback(void*, const char* path, const char* error) {
|
||||
if (error != nullptr) {
|
||||
show_data_folder_error_modal(error);
|
||||
void data_folder_dialog_callback(borealis::file_select::Result result) {
|
||||
if (result.status == borealis::file_select::Status::Canceled) {
|
||||
return;
|
||||
}
|
||||
if (path == nullptr) {
|
||||
if (result.status != borealis::file_select::Status::Selected || result.locations.empty()) {
|
||||
show_data_folder_error_modal("Dusklight could not open the folder picker.");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string dataPathError;
|
||||
if (data::set_custom_data_path(path, &dataPathError)) {
|
||||
if (data::set_custom_data_path(result.locations.front(), &dataPathError)) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
return;
|
||||
}
|
||||
@@ -487,7 +488,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
if (path.empty()) {
|
||||
display = "(none)";
|
||||
} else {
|
||||
display = display_name_for_path(path);
|
||||
display = borealis::file_select::display_name(path);
|
||||
if (display.empty()) {
|
||||
display = path;
|
||||
}
|
||||
@@ -506,49 +507,56 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
pane.add_rml("Set the disc image that Dusklight uses to launch the game.<br/><br/>"
|
||||
"Changes require a restart.");
|
||||
});
|
||||
#if DUSK_CAN_CHANGE_DATA_FOLDER
|
||||
leftPane.register_control(
|
||||
leftPane.add_select_button({
|
||||
.key = "Data Folder",
|
||||
.getValue = [] { return configured_data_path_display_name(); },
|
||||
.isModified = [] { return data::is_data_path_restart_pending(); },
|
||||
}),
|
||||
rightPane, [](Pane& pane) {
|
||||
pane.add_text("The data folder is where Dusklight stores settings, saves, "
|
||||
"logs, texture replacements, and other app data.");
|
||||
pane.add_child<DataFolderPathText>();
|
||||
if (data::manager().capabilities().canChangeLocation &&
|
||||
borealis::file_select::capabilities().canOpenFolder)
|
||||
{
|
||||
leftPane.register_control(
|
||||
leftPane.add_select_button({
|
||||
.key = "Data Folder",
|
||||
.getValue = [] { return configured_data_path_display_name(); },
|
||||
.isModified = [] { return data::is_data_path_restart_pending(); },
|
||||
}),
|
||||
rightPane, [](Pane& pane) {
|
||||
pane.add_text("The data folder is where Dusklight stores settings, saves, "
|
||||
"logs, texture replacements, and other app data.");
|
||||
pane.add_child<DataFolderPathText>();
|
||||
#if DUSK_CAN_OPEN_DATA_FOLDER
|
||||
pane.add_button("Open Data Folder").on_pressed([] {
|
||||
if (data::open_data_path()) {
|
||||
mDoAud_seStartMenu(kSoundClick);
|
||||
}
|
||||
});
|
||||
pane.add_button("Open Data Folder").on_pressed([] {
|
||||
if (data::open_data_path()) {
|
||||
mDoAud_seStartMenu(kSoundClick);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
pane.add_button("Change Data Folder").on_pressed([] {
|
||||
const auto defaultLocation =
|
||||
io::fs_path_to_string(data::configured_data_path());
|
||||
ShowFolderSelect(&data_folder_dialog_callback, nullptr,
|
||||
aurora::window::get_sdl_window(),
|
||||
defaultLocation.empty() ? nullptr : defaultLocation.c_str());
|
||||
});
|
||||
pane.add_button("Change Data Folder").on_pressed([] {
|
||||
const auto defaultLocation =
|
||||
borealis::io::fs_path_to_string(data::configured_data_path());
|
||||
borealis::file_select::open_folder(
|
||||
{
|
||||
.parentWindow = aurora::window::get_sdl_window(),
|
||||
.defaultLocation = defaultLocation,
|
||||
},
|
||||
&data_folder_dialog_callback);
|
||||
});
|
||||
#if defined(_WIN32)
|
||||
pane.add_button("Portable Mode").on_pressed([] {
|
||||
if (data::set_portable_data_path()) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
}
|
||||
});
|
||||
pane.add_button("Portable Mode").on_pressed([] {
|
||||
if (data::set_portable_data_path()) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
pane.add_button({
|
||||
.text = "Reset to Default",
|
||||
.isDisabled = [] { return data::is_default_data_path(); },
|
||||
}).on_pressed([] {
|
||||
if (data::reset_data_path()) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
}
|
||||
pane.add_button(
|
||||
{
|
||||
.text = "Reset to Default",
|
||||
.isDisabled = [] { return data::is_default_data_path(); },
|
||||
})
|
||||
.on_pressed([] {
|
||||
if (data::reset_data_path()) {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
}
|
||||
});
|
||||
pane.add_rml("Data will be migrated automatically on restart.");
|
||||
});
|
||||
pane.add_rml("Data will be migrated automatically on restart.");
|
||||
});
|
||||
#endif
|
||||
}
|
||||
leftPane.register_control(
|
||||
leftPane.add_select_button({
|
||||
.key = "Language",
|
||||
@@ -857,7 +865,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.on_pressed([i] {
|
||||
mDoAud_seStartMenu(kSoundItemChange);
|
||||
getSettings().game.enableFrameInterpolation.setValue(static_cast<FrameInterpMode>(i));
|
||||
android::update_surface_frame_rate();
|
||||
presentation::update_frame_rate_preference();
|
||||
config::save();
|
||||
});
|
||||
}
|
||||
@@ -866,7 +874,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
config_int_select(leftPane, rightPane, getSettings().video.maxFrameRate,
|
||||
"Framerate Cap", "Limit the framerate to the specified value.", 30, 540, 1,
|
||||
[] { return getSettings().game.enableFrameInterpolation.getValue() != FrameInterpMode::Capped; },
|
||||
[](int) { android::update_surface_frame_rate(); });
|
||||
[](int) { presentation::update_frame_rate_preference(); });
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.enableMapBackground,
|
||||
{
|
||||
.key = "Enable Mini-Map Shadows",
|
||||
@@ -1415,15 +1423,16 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
});
|
||||
pane.add_rml("<br/>Choose which notifications can be displayed.");
|
||||
});
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
#if BOREALIS_HAS_SENTRY
|
||||
auto& crashReporting = leftPane.add_child<BoolButton>(BoolButton::Props{
|
||||
.key = "Crash Reporting",
|
||||
.getValue =
|
||||
[] { return crash_reporting::get_consent() == crash_reporting::Consent::Given; },
|
||||
.setValue = [](bool enabled) { crash_reporting::set_consent(enabled); },
|
||||
[] { return borealis::sentry::get_consent() == borealis::sentry::Consent::Given; },
|
||||
.setValue = [](bool enabled) { borealis::sentry::set_consent(enabled); },
|
||||
.isDisabled =
|
||||
[] {
|
||||
return crash_reporting::get_consent() == crash_reporting::Consent::Unavailable;
|
||||
return borealis::sentry::get_consent() ==
|
||||
borealis::sentry::Consent::Unavailable;
|
||||
},
|
||||
.isModified = [] { return false; },
|
||||
});
|
||||
@@ -1447,7 +1456,7 @@ SettingsWindow::SettingsWindow(bool prelaunch) : mPrelaunch(prelaunch) {
|
||||
.helpText = "Checks GitHub releases for a new Dusklight version on startup.<br/><br/>"
|
||||
"No personal information is transmitted or collected.",
|
||||
});
|
||||
#ifdef DUSK_DISCORD
|
||||
#if BOREALIS_HAS_DISCORD
|
||||
config_bool_select(leftPane, rightPane, getSettings().game.enableDiscordPresence,
|
||||
{
|
||||
.key = "Enable Discord Rich Presence",
|
||||
|
||||
+2
-1
@@ -17,6 +17,7 @@
|
||||
#include "aurora/lib/window.hpp"
|
||||
#include "dusk/config.hpp"
|
||||
#include "dusk/io.hpp"
|
||||
#include <borealis/io.hpp>
|
||||
#include "icon_provider.hpp"
|
||||
#include "input.hpp"
|
||||
#include "mod_texture_provider.hpp"
|
||||
@@ -27,7 +28,7 @@ namespace dusk::ui {
|
||||
namespace {
|
||||
|
||||
void load_font(const char* filename, bool fallback = false) {
|
||||
Rml::LoadFontFace(io::fs_path_to_string(resource_path(filename)), fallback);
|
||||
Rml::LoadFontFace(borealis::io::fs_path_to_string(resource_path(filename)), fallback);
|
||||
}
|
||||
|
||||
bool sInitialized = false;
|
||||
|
||||
@@ -1,351 +0,0 @@
|
||||
#include "update_check.hpp"
|
||||
|
||||
#include "dusk/http/http.hpp"
|
||||
#include "fmt/format.h"
|
||||
#include "nlohmann/json.hpp"
|
||||
#include "version.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <optional>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::update_check {
|
||||
namespace {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
constexpr std::string_view GitHubApiVersion = "2026-03-10";
|
||||
|
||||
struct Version {
|
||||
int major = 0;
|
||||
int minor = 0;
|
||||
int patch = 0;
|
||||
std::vector<std::string_view> prerelease;
|
||||
};
|
||||
|
||||
std::string json_string(const json& value, const char* key) {
|
||||
const auto iter = value.find(key);
|
||||
if (iter == value.end() || !iter->is_string()) {
|
||||
return {};
|
||||
}
|
||||
return iter->get<std::string>();
|
||||
}
|
||||
|
||||
std::optional<int> parse_component(std::string_view& value) {
|
||||
if (value.empty() || value.front() < '0' || value.front() > '9') {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
int parsed = 0;
|
||||
const char* begin = value.data();
|
||||
const char* end = value.data() + value.size();
|
||||
const auto [ptr, ec] = std::from_chars(begin, end, parsed);
|
||||
if (ec != std::errc()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
value.remove_prefix(static_cast<size_t>(ptr - begin));
|
||||
return parsed;
|
||||
}
|
||||
|
||||
bool consume(std::string_view& value, char expected) {
|
||||
if (value.empty() || value.front() != expected) {
|
||||
return false;
|
||||
}
|
||||
value.remove_prefix(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool is_digit(char value) {
|
||||
return value >= '0' && value <= '9';
|
||||
}
|
||||
|
||||
bool is_identifier_char(char value) {
|
||||
return is_digit(value) || (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z') || value == '-';
|
||||
}
|
||||
|
||||
bool is_numeric_identifier(std::string_view value) {
|
||||
if (value.empty()) {
|
||||
return false;
|
||||
}
|
||||
for (const char c : value) {
|
||||
if (!is_digit(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool is_identifier_list(std::string_view value) {
|
||||
if (value.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool expectingIdentifier = true;
|
||||
for (const char c : value) {
|
||||
if (c == '.') {
|
||||
if (expectingIdentifier) {
|
||||
return false;
|
||||
}
|
||||
expectingIdentifier = true;
|
||||
continue;
|
||||
}
|
||||
if (!is_identifier_char(c)) {
|
||||
return false;
|
||||
}
|
||||
expectingIdentifier = false;
|
||||
}
|
||||
|
||||
return !expectingIdentifier;
|
||||
}
|
||||
|
||||
std::string_view trim_git_describe_suffix(std::string_view value) {
|
||||
if (value.ends_with("-dirty")) {
|
||||
value.remove_suffix(6);
|
||||
}
|
||||
if (is_numeric_identifier(value)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const size_t suffixStart = value.rfind('-');
|
||||
if (suffixStart != std::string_view::npos && value.substr(0, suffixStart).find('.') != std::string_view::npos
|
||||
&& is_numeric_identifier(value.substr(suffixStart + 1))) {
|
||||
value.remove_suffix(value.size() - suffixStart);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void split_identifiers(std::string_view value, std::vector<std::string_view>& identifiers) {
|
||||
while (!value.empty()) {
|
||||
const size_t separator = value.find('.');
|
||||
if (separator == std::string_view::npos) {
|
||||
identifiers.push_back(value);
|
||||
return;
|
||||
}
|
||||
identifiers.push_back(value.substr(0, separator));
|
||||
value.remove_prefix(separator + 1);
|
||||
}
|
||||
}
|
||||
|
||||
std::string_view trim_leading_zeroes(std::string_view value) {
|
||||
while (value.size() > 1 && value.front() == '0') {
|
||||
value.remove_prefix(1);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
int compare_identifier(std::string_view lhs, std::string_view rhs) {
|
||||
const bool lhsNumeric = is_numeric_identifier(lhs);
|
||||
const bool rhsNumeric = is_numeric_identifier(rhs);
|
||||
if (lhsNumeric && rhsNumeric) {
|
||||
lhs = trim_leading_zeroes(lhs);
|
||||
rhs = trim_leading_zeroes(rhs);
|
||||
if (lhs.size() != rhs.size()) {
|
||||
return lhs.size() < rhs.size() ? -1 : 1;
|
||||
}
|
||||
} else if (lhsNumeric != rhsNumeric) {
|
||||
return lhsNumeric ? -1 : 1;
|
||||
}
|
||||
|
||||
const int result = lhs.compare(rhs);
|
||||
if (result < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (result > 0) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int compare_version(const Version& lhs, const Version& rhs) {
|
||||
if (lhs.major != rhs.major) {
|
||||
return lhs.major < rhs.major ? -1 : 1;
|
||||
}
|
||||
if (lhs.minor != rhs.minor) {
|
||||
return lhs.minor < rhs.minor ? -1 : 1;
|
||||
}
|
||||
if (lhs.patch != rhs.patch) {
|
||||
return lhs.patch < rhs.patch ? -1 : 1;
|
||||
}
|
||||
if (lhs.prerelease.empty() != rhs.prerelease.empty()) {
|
||||
return lhs.prerelease.empty() ? 1 : -1;
|
||||
}
|
||||
|
||||
const size_t commonSize = std::min(lhs.prerelease.size(), rhs.prerelease.size());
|
||||
for (size_t i = 0; i < commonSize; ++i) {
|
||||
const int result = compare_identifier(lhs.prerelease[i], rhs.prerelease[i]);
|
||||
if (result != 0) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (lhs.prerelease.size() != rhs.prerelease.size()) {
|
||||
return lhs.prerelease.size() < rhs.prerelease.size() ? -1 : 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::optional<Version> parse_version(std::string_view value) {
|
||||
if (!value.empty() && value.front() == 'v') {
|
||||
value.remove_prefix(1);
|
||||
}
|
||||
|
||||
Version version;
|
||||
auto major = parse_component(value);
|
||||
if (!major || !consume(value, '.')) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto minor = parse_component(value);
|
||||
if (!minor || !consume(value, '.')) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto patch = parse_component(value);
|
||||
if (!patch) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
version.major = *major;
|
||||
version.minor = *minor;
|
||||
version.patch = *patch;
|
||||
|
||||
if (value.empty()) {
|
||||
return version;
|
||||
}
|
||||
if (value.front() == '+') {
|
||||
value.remove_prefix(1);
|
||||
if (!is_identifier_list(value)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return version;
|
||||
}
|
||||
if (!consume(value, '-')) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const size_t buildStart = value.find('+');
|
||||
std::string_view prerelease = value.substr(0, buildStart);
|
||||
if (!is_identifier_list(prerelease)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
if (buildStart != std::string_view::npos && !is_identifier_list(value.substr(buildStart + 1))) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
prerelease = trim_git_describe_suffix(prerelease);
|
||||
if (!prerelease.empty()) {
|
||||
split_identifiers(prerelease, version.prerelease);
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
Release parse_release(const json& value) {
|
||||
Release release{
|
||||
.tagName = json_string(value, "tag_name"),
|
||||
.name = json_string(value, "name"),
|
||||
.htmlUrl = json_string(value, "html_url"),
|
||||
.body = json_string(value, "body"),
|
||||
};
|
||||
|
||||
const auto assets = value.find("assets");
|
||||
if (assets != value.end() && assets->is_array()) {
|
||||
for (const auto& asset : *assets) {
|
||||
if (!asset.is_object()) {
|
||||
continue;
|
||||
}
|
||||
release.assets.push_back({
|
||||
.name = json_string(asset, "name"),
|
||||
.browserDownloadUrl = json_string(asset, "browser_download_url"),
|
||||
.digest = json_string(asset, "digest"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return release;
|
||||
}
|
||||
|
||||
std::string release_url(std::string_view owner, std::string_view repo) {
|
||||
return fmt::format("https://api.github.com/repos/{}/{}/releases/latest", owner, repo);
|
||||
}
|
||||
|
||||
std::string user_agent() {
|
||||
return fmt::format("Dusk/{}", DUSK_WC_DESCRIBE);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result check_latest_github_release(std::string_view owner, std::string_view repo) {
|
||||
if (!http::available()) {
|
||||
return {
|
||||
.status = Status::Disabled,
|
||||
.message = "No HTTP backend is available",
|
||||
};
|
||||
}
|
||||
if (owner.empty() || repo.empty()) {
|
||||
return {
|
||||
.status = Status::Failed,
|
||||
.message = "GitHub owner and repo are required",
|
||||
};
|
||||
}
|
||||
|
||||
http::Request request{
|
||||
.url = release_url(owner, repo),
|
||||
.headers =
|
||||
{
|
||||
{.name = "User-Agent", .value = user_agent()},
|
||||
{.name = "Accept", .value = "application/vnd.github+json"},
|
||||
{.name = "X-GitHub-Api-Version", .value = std::string(GitHubApiVersion)},
|
||||
},
|
||||
};
|
||||
|
||||
http::Result result = http::get(request);
|
||||
if (result.error != http::Error::None) {
|
||||
return {
|
||||
.status = Status::Failed,
|
||||
.message = result.message,
|
||||
};
|
||||
}
|
||||
if (result.response.statusCode != 200) {
|
||||
return {
|
||||
.status = Status::Failed,
|
||||
.message = fmt::format("GitHub returned HTTP {}", result.response.statusCode),
|
||||
};
|
||||
}
|
||||
|
||||
Release latest;
|
||||
try {
|
||||
latest = parse_release(json::parse(result.response.body));
|
||||
} catch (const std::exception& e) {
|
||||
return {
|
||||
.status = Status::Failed,
|
||||
.message = fmt::format("Failed to parse GitHub release JSON: {}", e.what()),
|
||||
};
|
||||
}
|
||||
|
||||
const std::optional<Version> latestVersion = parse_version(latest.tagName);
|
||||
const std::optional<Version> currentVersion = parse_version(DUSK_WC_DESCRIBE);
|
||||
if (!latestVersion) {
|
||||
return {
|
||||
.status = Status::Failed,
|
||||
.message = fmt::format("Failed to parse release tag '{}'", latest.tagName),
|
||||
.latest = std::move(latest),
|
||||
};
|
||||
}
|
||||
if (!currentVersion) {
|
||||
return {
|
||||
.status = Status::Failed,
|
||||
.message = fmt::format("Failed to parse Dusklight version '{}'", DUSK_WC_DESCRIBE),
|
||||
.latest = std::move(latest),
|
||||
};
|
||||
}
|
||||
|
||||
const bool updateAvailable = compare_version(*latestVersion, *currentVersion) > 0;
|
||||
return {
|
||||
.status = updateAvailable ? Status::UpdateAvailable : Status::UpToDate,
|
||||
.message = updateAvailable ? "Update available" : "Dusklight is up to date",
|
||||
.latest = std::move(latest),
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace dusk::update_check
|
||||
@@ -1,41 +0,0 @@
|
||||
#ifndef DUSK_UPDATE_CHECK_HPP
|
||||
#define DUSK_UPDATE_CHECK_HPP
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace dusk::update_check {
|
||||
|
||||
enum class Status {
|
||||
Disabled,
|
||||
UpToDate,
|
||||
UpdateAvailable,
|
||||
Failed,
|
||||
};
|
||||
|
||||
struct Asset {
|
||||
std::string name;
|
||||
std::string browserDownloadUrl;
|
||||
std::string digest;
|
||||
};
|
||||
|
||||
struct Release {
|
||||
std::string tagName;
|
||||
std::string name;
|
||||
std::string htmlUrl;
|
||||
std::string body;
|
||||
std::vector<Asset> assets;
|
||||
};
|
||||
|
||||
struct Result {
|
||||
Status status = Status::Failed;
|
||||
std::string message;
|
||||
Release latest;
|
||||
};
|
||||
|
||||
Result check_latest_github_release(std::string_view owner, std::string_view repo);
|
||||
|
||||
} // namespace dusk::update_check
|
||||
|
||||
#endif // DUSK_UPDATE_CHECK_HPP
|
||||
+46
-35
@@ -44,14 +44,17 @@
|
||||
#include <cstring>
|
||||
#include <sstream>
|
||||
|
||||
#include <borealis/aurora_log.h>
|
||||
#include <borealis/cli.hpp>
|
||||
#include <borealis/crash.hpp>
|
||||
#include <borealis/io.hpp>
|
||||
#include <borealis/sentry.hpp>
|
||||
#include <borealis/version.h>
|
||||
#include <filesystem>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
#include "SSystem/SComponent/c_API.h"
|
||||
#include "dusk/android_frame_rate.hpp"
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/crash_handler.h"
|
||||
#include "dusk/crash_reporting.h"
|
||||
#include "dusk/data.hpp"
|
||||
#include "dusk/dusk.h"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
@@ -66,13 +69,13 @@
|
||||
#include "dusk/mods/svc/window.hpp"
|
||||
#include "dusk/mouse.h"
|
||||
#include "dusk/os.h"
|
||||
#include "dusk/presentation.hpp"
|
||||
#include "dusk/ui/menu_bar.hpp"
|
||||
#include "dusk/ui/overlay.hpp"
|
||||
#include "dusk/ui/prelaunch.hpp"
|
||||
#include "dusk/ui/preset.hpp"
|
||||
#include "dusk/ui/touch_controls.hpp"
|
||||
#include "dusk/ui/ui.hpp"
|
||||
#include "version.h"
|
||||
|
||||
#include <aurora/aurora.h>
|
||||
#include <aurora/event.h>
|
||||
@@ -102,7 +105,7 @@
|
||||
#include <TargetConditionals.h>
|
||||
#endif
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
#if BOREALIS_HAS_SENTRY
|
||||
#include "dusk/ui/reporting.hpp"
|
||||
#endif
|
||||
|
||||
@@ -332,7 +335,7 @@ void main01(void) {
|
||||
|
||||
FrameMark;
|
||||
|
||||
#ifdef DUSK_DISCORD
|
||||
#if BOREALIS_HAS_DISCORD
|
||||
dusk::discord::run_callbacks();
|
||||
dusk::discord::update_presence();
|
||||
#endif
|
||||
@@ -480,8 +483,8 @@ static void LanguageInit() {
|
||||
}
|
||||
|
||||
static void log_build_info() {
|
||||
DuskLog.info("Build: {} (rev {}, built {}, type {})", DUSK_WC_DESCRIBE, DUSK_WC_REVISION, DUSK_WC_DATE, DUSK_BUILD_TYPE);
|
||||
DuskLog.info("Platform: {}", DUSK_PLATFORM_NAME);
|
||||
DuskLog.info("Build: {} (rev {}, built {}, type {})", BOREALIS_APP_DESCRIBE, BOREALIS_APP_REVISION, BOREALIS_APP_DATE, BOREALIS_BUILD_TYPE);
|
||||
DuskLog.info("Platform: {}", BOREALIS_PLATFORM_NAME);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -496,14 +499,14 @@ int game_main(int argc, char* argv[]) {
|
||||
mainCalled = true;
|
||||
|
||||
cxxopts::ParseResult parsed_arg_options;
|
||||
borealis::cli::StandardOptions standardOptions;
|
||||
|
||||
try {
|
||||
cxxopts::Options arg_options("Dusklight", "PC Port of a classic adventure game");
|
||||
|
||||
borealis::cli::add_standard_options(arg_options);
|
||||
arg_options.add_options()
|
||||
("l,log-level", "Log level from " + std::to_string(AuroraLogLevel::LOG_DEBUG) + " to " + std::to_string(AuroraLogLevel::LOG_FATAL), cxxopts::value<uint8_t>()->default_value("0"))
|
||||
("h,help", "Print usage")
|
||||
("console", "Show the Windows console window for logs", cxxopts::value<bool>()->default_value("false")->implicit_value("true"))
|
||||
("dvd", "Path to DVD image file", cxxopts::value<std::string>())
|
||||
("mods", "Path to mods directory", cxxopts::value<std::string>())
|
||||
("backend", "Graphics API backend to use (auto, d3d12, d3d11, metal, vulkan, null)", cxxopts::value<std::string>())
|
||||
@@ -517,6 +520,7 @@ int game_main(int argc, char* argv[]) {
|
||||
arg_options.allow_unrecognised_options();
|
||||
|
||||
parsed_arg_options = arg_options.parse(argc, argv);
|
||||
standardOptions = borealis::cli::parse(parsed_arg_options);
|
||||
|
||||
if (parsed_arg_options.count("help"))
|
||||
{
|
||||
@@ -570,12 +574,11 @@ int game_main(int argc, char* argv[]) {
|
||||
|
||||
dusk::registerSettings();
|
||||
|
||||
const auto startupLogLevel =
|
||||
static_cast<AuroraLogLevel>(parsed_arg_options["log-level"].as<uint8_t>());
|
||||
const auto dataPaths = dusk::data::initialize_data();
|
||||
const auto dataPaths = dusk::data::initialize_data(standardOptions.userDir);
|
||||
dusk::ConfigPath = dataPaths.userPath;
|
||||
dusk::CachePath = dataPaths.cachePath;
|
||||
dusk::InitializeFileLogging(dusk::CachePath, startupLogLevel);
|
||||
dusk::InitializeLogging(dusk::CachePath, standardOptions);
|
||||
const auto startupLogLevel = borealis::log::level();
|
||||
|
||||
// Development Mode
|
||||
if (parsed_arg_options.count("develop")) {
|
||||
@@ -587,9 +590,15 @@ int game_main(int argc, char* argv[]) {
|
||||
|
||||
dusk::config::load_from_user_preferences();
|
||||
ApplyCVarOverrides(parsed_arg_options["cvar"]);
|
||||
dusk::android::update_surface_frame_rate();
|
||||
dusk::crash_reporting::initialize();
|
||||
dusk::crash_handler::install();
|
||||
borealis::sentry::Options sentryOptions{
|
||||
.release = fmt::format("{}@{}", dusk::AppInfo.appName, BOREALIS_APP_DESCRIBE),
|
||||
.databaseDirectory = dusk::CachePath / "sentry",
|
||||
};
|
||||
if (const char* logPath = borealis::log::file_path()) {
|
||||
sentryOptions.attachments.emplace_back(logPath);
|
||||
}
|
||||
borealis::sentry::initialize(sentryOptions);
|
||||
borealis::crash::install();
|
||||
// TODO: How to handle this?
|
||||
// PADSetDefaultMapping(&defaultPadMapping, PAD_TYPE_STANDARD);
|
||||
|
||||
@@ -597,19 +606,19 @@ int game_main(int argc, char* argv[]) {
|
||||
const auto mappingsPath = dusk::ConfigPath / "gamecontrollerdb.txt";
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(mappingsPath, ec)) {
|
||||
const auto mappingsPathString = dusk::io::fs_path_to_string(mappingsPath);
|
||||
const auto mappingsPathString = borealis::io::fs_path_to_string(mappingsPath);
|
||||
if (SDL_AddGamepadMappingsFromFile(mappingsPathString.c_str()) < 0) {
|
||||
DuskLog.warn("Failed to load gamecontrollerdb.txt from '{}': {}",
|
||||
mappingsPathString, SDL_GetError());
|
||||
}
|
||||
} else if (ec) {
|
||||
DuskLog.warn("Failed to inspect gamecontrollerdb.txt in data folder '{}': {}",
|
||||
dusk::io::fs_path_to_string(mappingsPath), ec.message());
|
||||
borealis::io::fs_path_to_string(mappingsPath), ec.message());
|
||||
}
|
||||
}
|
||||
|
||||
// Set SDL metadata for audio mixers and macOS "About" menu
|
||||
SDL_SetAppMetadata("Dusklight", DUSK_VERSION_STRING, "dev.twilitrealm.dusk");
|
||||
SDL_SetAppMetadata("Dusklight", BOREALIS_APP_VERSION, "dev.twilitrealm.dusk");
|
||||
|
||||
{
|
||||
const auto userPathString = dusk::ConfigPath.u8string();
|
||||
@@ -638,8 +647,8 @@ int game_main(int argc, char* argv[]) {
|
||||
}
|
||||
|
||||
config.desiredBackend = ResolveDesiredBackend(parsed_arg_options);
|
||||
config.logCallback = &aurora_log_callback;
|
||||
config.logLevel = startupLogLevel;
|
||||
config.logCallback = borealis::log::aurora_callback();
|
||||
config.logLevel = borealis::log::to_aurora_level(startupLogLevel);
|
||||
config.mem1Size = 256 * 1024 * 1024;
|
||||
config.mem2Size = 24 * 1024 * 1024;
|
||||
config.allowJoystickBackgroundEvents = dusk::getSettings().game.allowBackgroundInput;
|
||||
@@ -649,20 +658,22 @@ int game_main(int argc, char* argv[]) {
|
||||
auroraInfo = aurora_initialize(argc, argv, &config);
|
||||
}
|
||||
|
||||
dusk::presentation::update_frame_rate_preference();
|
||||
|
||||
// Apply after aurora_initialize: speedrun mode mutates cvars whose change callbacks push
|
||||
// values into aurora.
|
||||
if (dusk::getSettings().game.speedrunMode) {
|
||||
dusk::resetForSpeedrunMode();
|
||||
}
|
||||
|
||||
#ifdef DUSK_DISCORD
|
||||
#if BOREALIS_HAS_DISCORD
|
||||
if (dusk::getSettings().game.enableDiscordPresence) {
|
||||
dusk::discord::initialize();
|
||||
}
|
||||
#endif
|
||||
|
||||
VISetWindowTitle(
|
||||
fmt::format("Dusklight {} [{}]", DUSK_WC_DESCRIBE, dusk::backend_name(auroraInfo.backend))
|
||||
fmt::format("Dusklight {} [{}]", BOREALIS_APP_DESCRIBE, dusk::backend_name(auroraInfo.backend))
|
||||
.c_str());
|
||||
|
||||
if (dusk::getSettings().video.lockAspectRatio) {
|
||||
@@ -688,11 +699,11 @@ int game_main(int argc, char* argv[]) {
|
||||
// Run ImGui UI loop if Aurora couldn't initialize a backend
|
||||
if (auroraInfo.backend == BACKEND_NULL) {
|
||||
launchUILoop();
|
||||
dusk::crash_reporting::shutdown();
|
||||
dusk::ShutdownFileLogging();
|
||||
borealis::sentry::shutdown();
|
||||
borealis::log::shutdown();
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
#ifdef DUSK_DISCORD
|
||||
#if BOREALIS_HAS_DISCORD
|
||||
dusk::discord::shutdown();
|
||||
#endif
|
||||
dusk::ui::shutdown();
|
||||
@@ -784,11 +795,11 @@ int game_main(int argc, char* argv[]) {
|
||||
|
||||
// pre game launch ui main loop
|
||||
if (!launchUILoop()) {
|
||||
dusk::crash_reporting::shutdown();
|
||||
dusk::ShutdownFileLogging();
|
||||
borealis::sentry::shutdown();
|
||||
borealis::log::shutdown();
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
#ifdef DUSK_DISCORD
|
||||
#if BOREALIS_HAS_DISCORD
|
||||
dusk::discord::shutdown();
|
||||
#endif
|
||||
dusk::ui::shutdown();
|
||||
@@ -814,8 +825,8 @@ int game_main(int argc, char* argv[]) {
|
||||
dusk::IsGameLaunched = true;
|
||||
}
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
if (dusk::crash_reporting::get_consent() == dusk::crash_reporting::Consent::Unknown) {
|
||||
#if BOREALIS_HAS_SENTRY
|
||||
if (borealis::sentry::get_consent() == borealis::sentry::Consent::Unknown) {
|
||||
dusk::ui::push_document(std::make_unique<dusk::ui::CrashReportWindow>());
|
||||
}
|
||||
#endif
|
||||
@@ -905,8 +916,8 @@ int game_main(int argc, char* argv[]) {
|
||||
daMP_c::m_myObj->daMP_c_Finish();
|
||||
}
|
||||
|
||||
dusk::crash_reporting::shutdown();
|
||||
dusk::ShutdownFileLogging();
|
||||
borealis::sentry::shutdown();
|
||||
borealis::log::shutdown();
|
||||
fflush(stdout);
|
||||
fflush(stderr);
|
||||
|
||||
@@ -915,7 +926,7 @@ int game_main(int argc, char* argv[]) {
|
||||
// Notifies all CVs and causes threads to exit
|
||||
OSResetSystem(OS_RESET_SHUTDOWN, 0, 0);
|
||||
|
||||
#ifdef DUSK_DISCORD
|
||||
#if BOREALIS_HAS_DISCORD
|
||||
dusk::discord::shutdown();
|
||||
#endif
|
||||
dusk::ui::shutdown();
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#ifndef VERSION_H
|
||||
#define VERSION_H
|
||||
|
||||
#define DUSK_WC_DESCRIBE "@DUSK_WC_DESCRIBE@"
|
||||
#define DUSK_VERSION_STRING "@DUSK_VERSION_STRING@"
|
||||
|
||||
#define DUSK_WC_BRANCH "@DUSK_WC_BRANCH@"
|
||||
#define DUSK_WC_REVISION "@DUSK_WC_REVISION@"
|
||||
#define DUSK_WC_DATE "@DUSK_WC_DATE@"
|
||||
#define DUSK_BUILD_TYPE "@CMAKE_BUILD_TYPE@"
|
||||
|
||||
#if defined(__x86_64__) || defined(_M_AMD64)
|
||||
#define DUSK_ARCH "x86_64"
|
||||
#elif defined(__i386__) || defined(_M_IX86)
|
||||
#define DUSK_ARCH "x86"
|
||||
#elif defined(__aarch64__) || defined(_M_ARM64)
|
||||
#define DUSK_ARCH "arm64"
|
||||
#endif
|
||||
|
||||
#define DUSK_PLATFORM_NAME "@PLATFORM_NAME@"
|
||||
#define DUSK_DLPACKAGE "dusklight-@DUSK_WC_DESCRIBE@-" DUSK_PLATFORM_NAME "-" DUSK_ARCH
|
||||
|
||||
#define DUSK_SENTRY_DSN "@DUSK_SENTRY_DSN@"
|
||||
#define DUSK_SENTRY_ENVIRONMENT "@DUSK_SENTRY_ENVIRONMENT@"
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user