This commit is contained in:
TakaRikka
2026-08-05 16:32:21 -07:00
94 changed files with 654 additions and 14091 deletions
+4 -4
View File
@@ -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
+3
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+1 -1
View File
@@ -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
-121
View File
@@ -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()
+1 -1
Vendored Submodule
+1
Submodule extern/borealis added at eec74b6dec
+2 -18
View File
@@ -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
)
+1 -1
View File
@@ -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
View File
@@ -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.
+22 -153
View File
@@ -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
View File
@@ -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"
+2 -2
View File
@@ -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"
-5
View File
@@ -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")
+2 -2
View File
@@ -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;
-74
View File
@@ -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
-7
View File
@@ -1,7 +0,0 @@
#pragma once
namespace dusk::android {
void update_surface_frame_rate();
} // namespace dusk::android
+11 -11
View File
@@ -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
View File
@@ -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);
-965
View File
@@ -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
-7
View File
@@ -1,7 +0,0 @@
#pragma once
namespace dusk::crash_handler {
void install();
} // namespace dusk::crash_handler
-188
View File
@@ -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
-17
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+5 -11
View File
@@ -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();
-867
View File
@@ -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
-41
View File
@@ -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
+28 -22
View File
@@ -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
+2 -2
View File
@@ -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
-314
View File
@@ -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
-22
View File
@@ -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
-102
View File
@@ -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
-402
View File
@@ -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
-206
View File
@@ -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
-60
View File
@@ -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
-24
View File
@@ -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
-238
View File
@@ -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
-320
View File
@@ -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 -12
View File
@@ -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()) {
-2
View File
@@ -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;
+6 -20
View File
@@ -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() {
-8
View File
@@ -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
-21
View File
@@ -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
-151
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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] : "");
}
+2 -1
View File
@@ -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) {
+1 -1
View File
@@ -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;
+7 -5
View File
@@ -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();
+2 -2
View File
@@ -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;
+9 -9
View File
@@ -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) {
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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 -2
View File
@@ -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,
+2 -2
View File
@@ -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 -2
View File
@@ -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;
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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,
+2 -2
View File
@@ -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;
+30
View File
@@ -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
+7
View File
@@ -0,0 +1,7 @@
#pragma once
namespace dusk::presentation {
void update_frame_rate_preference();
} // namespace dusk::presentation
+2 -2
View File
@@ -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://";
+2 -2
View File
@@ -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
View File
@@ -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) {
+4 -4
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
#pragma once
#if DUSK_ENABLE_SENTRY_NATIVE
#if BOREALIS_HAS_SENTRY
#include "component.hpp"
#include "window.hpp"
+65 -56
View File
@@ -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
View File
@@ -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;
-351
View File
@@ -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
-41
View File
@@ -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
View File
@@ -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();
-26
View File
@@ -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