mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-20 05:16:24 -04:00
Merge with origin/main
This commit is contained in:
@@ -76,7 +76,7 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dusklight-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}}
|
||||
name: dusklight-${{env.APP_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}}
|
||||
path: |
|
||||
build/install/Dusklight-*.AppImage
|
||||
build/install/debug.tar.*
|
||||
@@ -145,7 +145,7 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dusklight-${{env.DUSK_VERSION}}-${{matrix.artifact_name}}
|
||||
name: dusklight-${{env.APP_VERSION}}-${{matrix.artifact_name}}
|
||||
path: |
|
||||
build/install/Dusklight.app
|
||||
build/install/debug.tar.*
|
||||
@@ -208,9 +208,6 @@ jobs:
|
||||
- name: Build bundled mods
|
||||
run: cmake --build --preset ${{matrix.preset}} --target dusklight_mods
|
||||
|
||||
- name: Stage stripped JNI library
|
||||
run: ANDROID_STAGE_ABIS="${{matrix.abi}}" platforms/android/scripts/stage-jni-libs.sh
|
||||
|
||||
- name: Build APK
|
||||
working-directory: platforms/android
|
||||
run: ./gradlew :app:assembleRelease --rerun-tasks
|
||||
@@ -224,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:
|
||||
@@ -286,7 +283,7 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dusklight-${{env.DUSK_VERSION}}-win32-msvc-${{matrix.artifact_arch}}
|
||||
name: dusklight-${{env.APP_VERSION}}-win32-msvc-${{matrix.artifact_arch}}
|
||||
path: |
|
||||
build/install/*.exe
|
||||
build/install/*.dll
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
[submodule "extern/aurora"]
|
||||
path = extern/aurora
|
||||
url = https://github.com/encounter/aurora.git
|
||||
[submodule "extern/borealis"]
|
||||
path = extern/borealis
|
||||
url = https://github.com/encounter/borealis.git
|
||||
|
||||
+33
-140
@@ -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)
|
||||
@@ -150,6 +151,7 @@ if (DUSK_MOVIE_SUPPORT)
|
||||
-DENABLE_SHARED=OFF
|
||||
-DWITH_TURBOJPEG=ON
|
||||
-DWITH_JAVA=OFF
|
||||
-DCMAKE_INSTALL_LIBDIR=lib
|
||||
)
|
||||
if (CMAKE_TOOLCHAIN_FILE)
|
||||
get_filename_component(_jpeg_toolchain_file "${CMAKE_TOOLCHAIN_FILE}" ABSOLUTE BASE_DIR "${CMAKE_SOURCE_DIR}")
|
||||
@@ -193,24 +195,21 @@ if (DUSK_MOVIE_SUPPORT)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
|
||||
if (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "GNU")
|
||||
# -Wno-multichar: Multi-character constants ('ABCD') are implementation-defined but all compilers
|
||||
# (CW, GCC, Clang, MSVC) encode them identically in big-endian order.
|
||||
# For >4-char literals (which GCC/Clang truncate to int), use the MULTI_CHAR() macro.
|
||||
# -Wdeprecated-declarations: JSystem uses std::iterator, deprecated in C++17
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-multichar")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-multichar -Wno-trigraphs -Wno-deprecated-declarations")
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN")
|
||||
set(CMAKE_BUILD_RPATH "$ORIGIN")
|
||||
elseif (APPLE)
|
||||
add_compile_options(-Wno-declaration-after-statement -Wno-non-pod-varargs)
|
||||
set(CMAKE_INSTALL_RPATH "@loader_path")
|
||||
set(CMAKE_BUILD_RPATH "@loader_path")
|
||||
elseif (MSVC)
|
||||
add_compile_options(
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:/bigobj>
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:/MP>
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:/FS>
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-multichar>
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-Wno-trigraphs>
|
||||
$<$<COMPILE_LANGUAGE:CXX>:-Wno-deprecated-declarations>
|
||||
)
|
||||
elseif (CMAKE_CXX_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
|
||||
add_compile_options(
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:/bigobj>
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:/MP>
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:/FS>
|
||||
)
|
||||
|
||||
if (NOT DUSK_BUILD_WARNINGS)
|
||||
@@ -225,21 +224,18 @@ elseif (MSVC)
|
||||
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:/utf-8>)
|
||||
endif ()
|
||||
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL Linux)
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN")
|
||||
set(CMAKE_BUILD_RPATH "$ORIGIN")
|
||||
elseif (APPLE)
|
||||
add_compile_options(-Wno-declaration-after-statement -Wno-non-pod-varargs)
|
||||
set(CMAKE_INSTALL_RPATH "@loader_path")
|
||||
set(CMAKE_BUILD_RPATH "@loader_path")
|
||||
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
|
||||
@@ -247,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
|
||||
@@ -282,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
|
||||
@@ -328,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)
|
||||
@@ -398,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 ()
|
||||
@@ -471,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 ()
|
||||
@@ -552,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)
|
||||
@@ -610,6 +517,7 @@ if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR
|
||||
add_subdirectory(mods/template_mod)
|
||||
add_subdirectory(mods/ao_mod)
|
||||
add_subdirectory(mods/shadow_mod)
|
||||
add_subdirectory(mods/window_demo)
|
||||
endif ()
|
||||
|
||||
if (APPLE)
|
||||
@@ -643,8 +551,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"
|
||||
@@ -677,21 +585,6 @@ if (APPLE)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
find_library(APPKIT_FRAMEWORK AppKit REQUIRED)
|
||||
target_sources(dusklight PRIVATE src/dusk/file_select_macos.mm)
|
||||
set_source_files_properties(src/dusk/file_select_macos.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
|
||||
target_link_libraries(dusklight PRIVATE ${APPKIT_FRAMEWORK})
|
||||
endif ()
|
||||
|
||||
if (IOS)
|
||||
find_library(UIKIT_FRAMEWORK UIKit REQUIRED)
|
||||
find_library(UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK UniformTypeIdentifiers REQUIRED)
|
||||
target_sources(dusklight PRIVATE src/dusk/ios/FileSelectDialog.m)
|
||||
set_source_files_properties(src/dusk/ios/FileSelectDialog.m PROPERTIES COMPILE_FLAGS -fobjc-arc)
|
||||
target_link_libraries(dusklight PRIVATE ${UIKIT_FRAMEWORK} ${UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK})
|
||||
endif ()
|
||||
|
||||
include(extern/aurora/cmake/AuroraCopyRuntimeDLLs.cmake)
|
||||
aurora_copy_runtime_dlls(dusklight)
|
||||
|
||||
|
||||
+4
-4
@@ -28,12 +28,12 @@
|
||||
"cacheVariables": {
|
||||
"CMAKE_C_COMPILER_LAUNCHER": "sccache",
|
||||
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache",
|
||||
"DUSK_ENABLE_SENTRY_NATIVE": {
|
||||
"BOREALIS_ENABLE_SENTRY": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
},
|
||||
"DUSK_SENTRY_DSN": "$env{SENTRY_DSN}",
|
||||
"DUSK_SENTRY_ENVIRONMENT": "production",
|
||||
"BOREALIS_SENTRY_DSN": "$env{SENTRY_DSN}",
|
||||
"BOREALIS_SENTRY_ENVIRONMENT": "production",
|
||||
"Rust_RUSTUP_INSTALL_MISSING_TARGET": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
@@ -448,7 +448,7 @@
|
||||
"ci"
|
||||
],
|
||||
"cacheVariables": {
|
||||
"DUSK_ENABLE_SENTRY_NATIVE": {
|
||||
"BOREALIS_ENABLE_SENTRY": {
|
||||
"type": "BOOL",
|
||||
"value": false
|
||||
}
|
||||
|
||||
@@ -23,5 +23,5 @@ cp -r platforms/freedesktop/{16x16,32x32,48x48,64x64,128x128,256x256,512x512,102
|
||||
cp platforms/freedesktop/dev.twilitrealm.dusk.desktop build/appdir/usr/share/applications
|
||||
|
||||
cd build/install
|
||||
VERSION="$DUSK_VERSION" NO_STRIP=1 "$linuxdeploy" \
|
||||
VERSION="$APP_VERSION" NO_STRIP=1 "$linuxdeploy" \
|
||||
-l "$lib_dir/libusb-1.0.so" --appdir "$build_dir/appdir" --output appimage
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
# Version detection shared by the main build and the mod SDK (sdk/CMakeLists.txt)
|
||||
include_guard(GLOBAL)
|
||||
|
||||
get_filename_component(_DUSK_VERSION_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
||||
|
||||
set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN")
|
||||
set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment")
|
||||
|
||||
set(DUSK_VERSION_OVERRIDE "" CACHE STRING "Override version string (skips git detection and format validation)")
|
||||
|
||||
macro(detect_version)
|
||||
if (DUSK_VERSION_OVERRIDE)
|
||||
set(DUSK_WC_DESCRIBE "${DUSK_VERSION_OVERRIDE}")
|
||||
set(DUSK_VERSION_STRING "0.0.0.0")
|
||||
set(DUSK_SHORT_VERSION_STRING "0.0.0")
|
||||
set(DUSK_VERSION_CODE "1")
|
||||
set(DUSK_WC_REVISION "")
|
||||
set(DUSK_WC_BRANCH "")
|
||||
set(DUSK_WC_DATE "")
|
||||
message(STATUS "Dusklight version overridden to ${DUSK_WC_DESCRIBE}")
|
||||
else ()
|
||||
# obtain revision info from git
|
||||
find_package(Git)
|
||||
if (GIT_FOUND)
|
||||
# make sure version information gets re-run when the current Git HEAD changes
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD
|
||||
OUTPUT_VARIABLE dusk_git_head_filename
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
get_filename_component(dusk_git_head_filename "${dusk_git_head_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_filename}")
|
||||
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --symbolic-full-name HEAD
|
||||
OUTPUT_VARIABLE dusk_git_head_symbolic
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT}
|
||||
COMMAND ${GIT_EXECUTABLE} rev-parse --git-path ${dusk_git_head_symbolic}
|
||||
OUTPUT_VARIABLE dusk_git_head_symbolic_filename
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
get_filename_component(dusk_git_head_symbolic_filename "${dusk_git_head_symbolic_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_symbolic_filename}")
|
||||
|
||||
# defines DUSK_WC_REVISION
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
|
||||
OUTPUT_VARIABLE DUSK_WC_REVISION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
# defines DUSK_WC_DESCRIBE
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} describe --tags --long --dirty --match "v*"
|
||||
OUTPUT_VARIABLE DUSK_WC_DESCRIBE
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
|
||||
# remove the git hash, then collapse a clean "-0" suffix only
|
||||
string(REGEX REPLACE "-[^-]+(-dirty|)$" "\\1" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
|
||||
string(REGEX REPLACE "-0$" "" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
|
||||
|
||||
# defines DUSK_WC_BRANCH
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
|
||||
OUTPUT_VARIABLE DUSK_WC_BRANCH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
# defines DUSK_WC_DATE
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} log -1 --format=%ad
|
||||
OUTPUT_VARIABLE DUSK_WC_DATE
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
else ()
|
||||
message(STATUS "Unable to find git, commit information will not be available")
|
||||
endif ()
|
||||
|
||||
if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$")
|
||||
set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
|
||||
set(_ver_major ${CMAKE_MATCH_1})
|
||||
set(_ver_minor ${CMAKE_MATCH_2})
|
||||
set(_ver_patch ${CMAKE_MATCH_3})
|
||||
set(DUSK_VERSION_TWEAK "0")
|
||||
if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$")
|
||||
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
|
||||
elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$")
|
||||
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
|
||||
endif ()
|
||||
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}")
|
||||
if (DUSK_VERSION_TWEAK GREATER 999)
|
||||
set(_tweak 999)
|
||||
else ()
|
||||
set(_tweak ${DUSK_VERSION_TWEAK})
|
||||
endif ()
|
||||
# encoding: major*1e7 + minor*1e5 + patch*1e3 + tweak; collision-free for major<210, minor<100, patch<100, tweak<=999
|
||||
math(EXPR DUSK_VERSION_CODE
|
||||
"${_ver_major} * 10000000 + ${_ver_minor} * 100000 + ${_ver_patch} * 1000 + ${_tweak}")
|
||||
else ()
|
||||
set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION")
|
||||
set(DUSK_VERSION_STRING "0.0.0.0")
|
||||
set(DUSK_SHORT_VERSION_STRING "0.0.0")
|
||||
set(DUSK_VERSION_CODE "1")
|
||||
endif ()
|
||||
|
||||
endif ()
|
||||
|
||||
# Add version information to CI environment variables
|
||||
if (DEFINED ENV{GITHUB_ENV})
|
||||
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION=${DUSK_WC_DESCRIBE}\n")
|
||||
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION_CODE=${DUSK_VERSION_CODE}\n")
|
||||
endif ()
|
||||
message(STATUS "Dusklight version set to ${DUSK_WC_DESCRIBE}")
|
||||
endmacro()
|
||||
|
||||
# Sets PLATFORM_NAME and configures version.h into the caller's binary dir.
|
||||
macro(configure_version_header)
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL Windows)
|
||||
set(PLATFORM_NAME win32)
|
||||
elseif (CMAKE_SYSTEM_NAME STREQUAL Darwin)
|
||||
if (IOS)
|
||||
set(PLATFORM_NAME ios)
|
||||
elseif (TVOS)
|
||||
set(PLATFORM_NAME tvos)
|
||||
else ()
|
||||
set(PLATFORM_NAME macos)
|
||||
endif ()
|
||||
else ()
|
||||
string(TOLOWER CMAKE_SYSTEM_NAME PLATFORM_NAME)
|
||||
endif ()
|
||||
|
||||
configure_file(${_DUSK_VERSION_ROOT}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
|
||||
endmacro()
|
||||
@@ -62,3 +62,7 @@ target_sources(dusklight_mod_feature_game INTERFACE
|
||||
add_library(dusklight_mod_feature_webgpu INTERFACE)
|
||||
target_link_libraries(dusklight_mod_feature_webgpu INTERFACE dusklight_mod_api)
|
||||
target_compile_definitions(dusklight_mod_feature_webgpu INTERFACE DUSK_MOD_FEATURE_WEBGPU=1)
|
||||
|
||||
add_library(dusklight_mod_feature_fmt INTERFACE)
|
||||
target_link_libraries(dusklight_mod_feature_fmt INTERFACE dusklight_mod_api)
|
||||
target_compile_definitions(dusklight_mod_feature_fmt INTERFACE DUSK_MOD_FEATURE_FMT=1)
|
||||
|
||||
+27
-1
@@ -113,6 +113,29 @@ function(_mod_add_webgpu_headers target_name)
|
||||
endif ()
|
||||
endfunction()
|
||||
|
||||
function(_mod_add_fmt target_name)
|
||||
if (NOT TARGET fmt::fmt-header-only)
|
||||
find_package(fmt 11 CONFIG QUIET GLOBAL)
|
||||
endif ()
|
||||
|
||||
if (NOT TARGET fmt::fmt-header-only)
|
||||
include(FetchContent)
|
||||
message(STATUS "Mod SDK: fetching fmt")
|
||||
# Keep the fallback version in sync with extern/aurora/extern/CMakeLists.txt.
|
||||
FetchContent_Declare(fmt
|
||||
URL https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz
|
||||
URL_HASH SHA256=ea7de4299689e12b6dddd392f9896f08fb0777ac7168897a244a6d6085043fea
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP FALSE
|
||||
EXCLUDE_FROM_ALL)
|
||||
FetchContent_MakeAvailable(fmt)
|
||||
endif ()
|
||||
|
||||
if (NOT TARGET fmt::fmt-header-only)
|
||||
message(FATAL_ERROR "add_mod: FEATURES fmt could not provide fmt::fmt-header-only")
|
||||
endif ()
|
||||
target_link_libraries(${target_name} PRIVATE fmt::fmt-header-only)
|
||||
endfunction()
|
||||
|
||||
function(add_mod target_name)
|
||||
cmake_parse_arguments(ARG "BUNDLE" "MOD_JSON;RES_DIR;OVERLAY_DIR;TEXTURES_DIR;OUTPUT_DIR"
|
||||
"SOURCES;RUNTIME_LIBRARIES;FEATURES" ${ARGN})
|
||||
@@ -127,7 +150,7 @@ function(add_mod target_name)
|
||||
message(FATAL_ERROR "add_mod: MOD_JSON does not exist: ${_mod_json}")
|
||||
endif ()
|
||||
|
||||
set(_supported_features game webgpu)
|
||||
set(_supported_features fmt game webgpu)
|
||||
set(_features "")
|
||||
foreach (_feature IN LISTS ARG_FEATURES)
|
||||
list(FIND _supported_features "${_feature}" _feature_index)
|
||||
@@ -167,6 +190,9 @@ function(add_mod target_name)
|
||||
if (_feature STREQUAL "webgpu")
|
||||
_mod_add_webgpu_headers(${target_name})
|
||||
endif ()
|
||||
if (_feature STREQUAL "fmt")
|
||||
_mod_add_fmt(${target_name})
|
||||
endif ()
|
||||
if (_feature STREQUAL "game" OR _feature STREQUAL "webgpu")
|
||||
set(_needs_host_link TRUE)
|
||||
endif ()
|
||||
|
||||
@@ -2,40 +2,50 @@ include_guard(GLOBAL)
|
||||
|
||||
get_filename_component(_SYMBOL_MANIFEST_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
|
||||
|
||||
set(_SYMGEN_VERSION "1.3.1")
|
||||
set(_SYMGEN_VERSION "1.3.2")
|
||||
set(_SYMGEN_RELEASE_BASE_URL "https://github.com/encounter/symgen/releases/download/v${_SYMGEN_VERSION}")
|
||||
set(SYMGEN_PATH "" CACHE FILEPATH "Path to a symgen executable; empty downloads the pinned release")
|
||||
mark_as_advanced(SYMGEN_PATH)
|
||||
|
||||
function(symgen_host_asset out_name)
|
||||
function(symgen_host_asset out_name out_hash)
|
||||
string(TOLOWER "${CMAKE_HOST_SYSTEM_PROCESSOR}" _host_processor)
|
||||
set(_asset "")
|
||||
set(_asset_hash "")
|
||||
|
||||
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin")
|
||||
if (_host_processor MATCHES "^(arm64|aarch64)$")
|
||||
set(_asset "symgen-macos-arm64")
|
||||
set(_asset_hash "SHA256=0344838d1674df09c17c3eeddcf26eb89c333fdb85dfd78f68adc436070eccbe")
|
||||
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
|
||||
set(_asset "symgen-macos-x86_64")
|
||||
set(_asset_hash "SHA256=ae0674f4a1e9d0dedfa02d35939ac28cdd229276b331c1f507df5df809cbec7e")
|
||||
endif ()
|
||||
elseif (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
|
||||
if (_host_processor MATCHES "^(aarch64|arm64)$")
|
||||
set(_asset "symgen-linux-aarch64")
|
||||
set(_asset_hash "SHA256=af766de2bfaeb0a06f6d7bc17bb2510b4a9c40f44a56e49bc4a4b798a6223042")
|
||||
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
|
||||
set(_asset "symgen-linux-x86_64")
|
||||
set(_asset_hash "SHA256=ebd62fb9623acc942b6295609e2306f85a73043b0a8a117f2072b761dd08e68f")
|
||||
elseif (_host_processor MATCHES "^(i[3-6]86|x86)$")
|
||||
set(_asset "symgen-linux-i686")
|
||||
set(_asset_hash "SHA256=07780a4513fd29726578efc4ff2d88736b22f407ed821b32a68e35ff1e9af5f4")
|
||||
endif ()
|
||||
elseif (CMAKE_HOST_WIN32)
|
||||
if (_host_processor MATCHES "^(arm64|aarch64)$")
|
||||
set(_asset "symgen-windows-arm64.exe")
|
||||
set(_asset_hash "SHA256=5bb22b4a4a9b5ad45646af411bfb09b8321a732ff3a077eb4c9de1feaef27d2b")
|
||||
elseif (_host_processor MATCHES "^(x86_64|amd64)$")
|
||||
set(_asset "symgen-windows-x86_64.exe")
|
||||
set(_asset_hash "SHA256=1d1ac087f991a96932d108969e15998becfec643e88f3e7d182ca97ebfc6a46f")
|
||||
elseif (_host_processor MATCHES "^(i[3-6]86|x86)$")
|
||||
set(_asset "symgen-windows-x86.exe")
|
||||
set(_asset_hash "SHA256=c113f4cd05f813efe2b1878dbfcf44d6302aee3974271736e0036430b0cced78")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
set(${out_name} "${_asset}" PARENT_SCOPE)
|
||||
set(${out_hash} "${_asset_hash}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
function(ensure_symgen required)
|
||||
@@ -54,7 +64,7 @@ function(ensure_symgen required)
|
||||
return()
|
||||
endif ()
|
||||
else ()
|
||||
symgen_host_asset(_asset)
|
||||
symgen_host_asset(_asset _asset_hash)
|
||||
if (_asset STREQUAL "")
|
||||
if (required)
|
||||
message(FATAL_ERROR "symgen: no prebuilt binary for host "
|
||||
@@ -75,7 +85,8 @@ function(ensure_symgen required)
|
||||
file(DOWNLOAD "${_url}" "${_symgen}"
|
||||
TLS_VERIFY ON
|
||||
STATUS _download_status
|
||||
SHOW_PROGRESS)
|
||||
SHOW_PROGRESS
|
||||
EXPECTED_HASH "${_asset_hash}")
|
||||
list(GET _download_status 0 _download_code)
|
||||
if (NOT _download_code EQUAL 0)
|
||||
list(GET _download_status 1 _download_message)
|
||||
@@ -106,6 +117,16 @@ function(setup_symbol_manifest target)
|
||||
endif ()
|
||||
add_dependencies(${target} symgen)
|
||||
|
||||
# Reserve an ELF program-header entry when the linker supports it (mold).
|
||||
# symgen can replace the PT_NULL entry without relocating the table, keeping later post-link tools safe.
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
include(CheckLinkerFlag)
|
||||
check_linker_flag(CXX "LINKER:--spare-program-headers=1" _linker_supports_spare_program_headers)
|
||||
if (_linker_supports_spare_program_headers)
|
||||
target_link_options(${target} PRIVATE "LINKER:--spare-program-headers=1")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (WIN32)
|
||||
set(_input --pdb "$<TARGET_PDB_FILE:${target}>")
|
||||
else ()
|
||||
|
||||
+184
-19
@@ -54,7 +54,7 @@ include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/FetchDusklight.cmake")
|
||||
add_subdirectory("${DUSKLIGHT_DIR}/sdk" dusklight-sdk EXCLUDE_FROM_ALL)
|
||||
|
||||
add_mod(my_mod
|
||||
FEATURES game # remove for service/asset-only mods; add webgpu for GfxService
|
||||
FEATURES game fmt # remove game for service-only mods; add webgpu for GfxService
|
||||
SOURCES src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
RES_DIR res # mod resources, including icon.png and banner.png
|
||||
@@ -64,6 +64,8 @@ add_mod(my_mod
|
||||
```
|
||||
|
||||
Available features:
|
||||
|
||||
- `fmt`: Provides the header-only `{fmt}` library and the formatted logging helpers in `mods/svc/log.hpp`.
|
||||
- `game`: Allows calling into and hooking game code. Mods that **only** use services may omit it, providing a wider
|
||||
range of compatibility with Dusklight versions and a slightly faster build process.
|
||||
- `webgpu`: Allows importing the WebGPU API (`webgpu/webgpu.h`). Must be enabled when using
|
||||
@@ -143,6 +145,9 @@ IMPORT_SERVICE_VERSION(LogService, svc_log, 0); // required, minimum minor ver
|
||||
IMPORT_OPTIONAL_SERVICE(SomeService, svc_maybe); // may be null
|
||||
```
|
||||
|
||||
A service must be imported in only **one** file (usually your `mod.cpp`). Other files may simply use `svc_log` or
|
||||
`mods::log::` after including the appropriate header.
|
||||
|
||||
Each service is individually versioned, and there may be multiple major versions of a service provided at once,
|
||||
allowing backwards compatibility with older mods while still changing services fundamentally if necessary. A **major**
|
||||
bump is a breaking change, treated as a different service entirely. For **additive** changes, a service appends new
|
||||
@@ -179,7 +184,15 @@ svc_log->write(mod_ctx, LOG_LEVEL_DEBUG, "verbose details");
|
||||
```
|
||||
|
||||
Messages appear in the console prefixed with your mod ID. Messages are plain UTF-8 strings and are copied before the
|
||||
call returns; use `snprintf` or `fmt::format` for formatting.
|
||||
call returns. C++ mods can enable `add_mod(... FEATURES fmt)` and use the formatted logging helpers in
|
||||
`mods/svc/log.hpp`:
|
||||
|
||||
```cpp
|
||||
#include <mods/svc/log.hpp>
|
||||
|
||||
mods::log::info("spawned actor {} at ({}, {})", actorName, x, y);
|
||||
mods::log::warn("health is down to {:.1f}%", healthPercent);
|
||||
```
|
||||
|
||||
### ResourceService (`mods/svc/resource.h`)
|
||||
|
||||
@@ -196,8 +209,8 @@ if (svc_resource->load(mod_ctx, "config.txt", &buf) == MOD_OK) {
|
||||
}
|
||||
```
|
||||
|
||||
Missing files return `MOD_UNAVAILABLE`. Always `free` what you `load`. Note that the bundle is read-only; for writable
|
||||
storage, use the directory from `svc_host->mod_dir(mod_ctx)`.
|
||||
Missing files return `MOD_UNAVAILABLE`. Always `free` what you `load`. The bundle is read-only; use
|
||||
`HostService::data_dir` for persistent storage.
|
||||
|
||||
### HostService (`mods/svc/host.h`)
|
||||
|
||||
@@ -206,9 +219,17 @@ Mod metadata and runtime interaction with the loader:
|
||||
```cpp
|
||||
IMPORT_SERVICE(HostService, svc_host);
|
||||
|
||||
const char* id = svc_host->mod_id(mod_ctx);
|
||||
const char* dir = svc_host->mod_dir(mod_ctx); // writable per-mod directory
|
||||
svc_host->fail(mod_ctx, MOD_ERROR, "something unrecoverable happened"); // disables the mod
|
||||
// Temporary mod data directory, wiped on startup
|
||||
const char* cacheDir = svc_host->mod_dir(mod_ctx);
|
||||
|
||||
// Persistent mod data directory
|
||||
const char* dataDir = nullptr;
|
||||
if (svc_host->data_dir(mod_ctx, &dataDir) == MOD_OK) {
|
||||
// ...
|
||||
}
|
||||
|
||||
// Report an error and disable the mod
|
||||
svc_host->fail(mod_ctx, MOD_ERROR, "something unrecoverable happened");
|
||||
```
|
||||
|
||||
`get_service`/`publish_service` provide dynamic service lookup; see [Exporting Services](#exporting-services).
|
||||
@@ -236,7 +257,7 @@ every service dropped its state. For your own mod's teardown, use `mod_shutdown`
|
||||
### HookService (`mods/svc/hook.h`)
|
||||
|
||||
Installs hooks on game functions and resolves symbols by name. You'll rarely call it directly; use the typed helpers in
|
||||
`mods/hook.hpp` described in [Hooking Game Functions](#hooking-game-functions).
|
||||
`mods/svc/hook.hpp` described in [Hooking Game Functions](#hooking-game-functions).
|
||||
|
||||
### OverlayService (`mods/svc/overlay.h`)
|
||||
|
||||
@@ -327,6 +348,88 @@ Change callbacks fire on the game thread whenever the value changes at runtime (
|
||||
Writes that store the same value are silent. Values applied from `config.json` or `--cvar` at registration do
|
||||
**not** fire callbacks; read the value after `register_var` for the starting state.
|
||||
|
||||
### SaveService (`mods/svc/save.h`)
|
||||
|
||||
Stores named binary blobs for each save slot. Blob names are scoped to the calling mod, and each mod may store up to
|
||||
`SAVE_BLOB_BUDGET_BYTES` per slot. The service copies data passed to `set_blob`.
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(SaveService, svc_save);
|
||||
|
||||
struct MySaveData {
|
||||
uint32_t version;
|
||||
uint32_t counter;
|
||||
};
|
||||
|
||||
MySaveData state{1, 42};
|
||||
svc_save->set_blob(mod_ctx, "state", &state, sizeof(state));
|
||||
|
||||
MySaveData loaded{};
|
||||
size_t loadedSize = sizeof(loaded);
|
||||
if (svc_save->get_blob(mod_ctx, "state", &loaded, &loadedSize) == MOD_OK &&
|
||||
loadedSize == sizeof(loaded)) {
|
||||
apply_state(loaded);
|
||||
}
|
||||
```
|
||||
|
||||
`set_blob`, `get_blob`, and `delete_blob` operate on the current slot, which is available after creating or loading a
|
||||
save and unavailable at file select. Blob changes are written with the next game save. File-select copy and erase
|
||||
operations update the blob data as well. Use `peek_blob` to read the calling mod's data from any slot; it uses the same
|
||||
buffer contract as `get_blob`. Pass a `NULL` buffer to either read function to query the blob size.
|
||||
|
||||
`observe_saves` registers callbacks for new, loaded, and written saves. New-save callbacks run after the slot's blobs
|
||||
are cleared. Observers are removed automatically when the mod is detached, so the output handle is only needed for
|
||||
manual unregistration. Save callbacks run on the game thread.
|
||||
|
||||
### StageService (`mods/svc/stage.h`)
|
||||
|
||||
Allows making changes to a stage's "stage info" (contents of .dzs/.dzr files).
|
||||
(Currently only supports editing actor nodes.)
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(StageService, svc_stage);
|
||||
|
||||
stage_actor_data_class record = {
|
||||
"carry00",
|
||||
0xFF000000,
|
||||
cXyz(0.0f, 0.0f, 0.0f),
|
||||
csXyz(0, 0, 0),
|
||||
0,
|
||||
};
|
||||
|
||||
StageActorHandle handle{};
|
||||
svc_stage->patch_actor(mod_ctx, "F_SP102", 0, -1, record_crc, &record, sizeof(record), &handle);
|
||||
```
|
||||
|
||||
```
|
||||
StageActorHandle handle{};
|
||||
svc_stage->delete_actor(mod_ctx, "F_SP102", 0, -1, record_crc, &handle);
|
||||
```
|
||||
|
||||
Patch or remove actors from the original actor list as the room loads.
|
||||
Given records must be of either `stage_actor_data_class` or `stage_tgsc_data_class` types.
|
||||
`record_crc` is the CRC-32 of the unmodified original record used to identify the record to replace or remove.
|
||||
|
||||
```
|
||||
stage_actor_data_class record = {
|
||||
"carry00",
|
||||
0xFF000000,
|
||||
cXyz(0.0f, 0.0f, 0.0f),
|
||||
csXyz(0, 0, 0),
|
||||
0,
|
||||
};
|
||||
|
||||
StageActorHandle handle{};
|
||||
svc_stage->add_actor(mod_ctx, "F_SP102", 0, -1, &record, sizeof(record), &handle);
|
||||
```
|
||||
|
||||
Add a new actor to the actor list as the room loads.
|
||||
Given records must be of either `stage_actor_data_class` or `stage_tgsc_data_class` types.
|
||||
|
||||
Stage names may contain up to 8 characters. For patches and deletions, room `0xff` and layer `-1` match any room or
|
||||
layer; additions require a specific room. Edits are removed when the mod is detached. If multiple mods edit the same
|
||||
record, the later-loaded mod wins.
|
||||
|
||||
### UiService (`mods/svc/ui.h`)
|
||||
|
||||
Integrate seamlessly with Dusklight's UI system: add controls and buttons to your mod's detail pane in the Mods window,
|
||||
@@ -409,6 +512,22 @@ sets `keep_open`. A `keep_open` action can close it later (or immediately) with
|
||||
`on_dismiss` if present and always closes. `dialog_set_body`, `dialog_set_icon`, and `dialog_add_action` mutate a live
|
||||
dialog.
|
||||
|
||||
**Toasts:** `push_toast` enqueues a notification. Titles and bodies accept RML. The optional `type` is applied as an
|
||||
RCSS class; `warning` uses the built-in warning appearance, and mods can define their own types. A duration of 0 uses
|
||||
the default of 5 seconds.
|
||||
|
||||
Toasts have a `mod-id` attribute, so `UI_SCOPE_OVERLAY` styles can use selectors such as
|
||||
`toast[mod-id="com.example.randomizer"].success`.
|
||||
|
||||
```cpp
|
||||
UiToastDesc toast = UI_TOAST_DESC_INIT;
|
||||
toast.type = "success";
|
||||
toast.title_rml = "Randomizer";
|
||||
toast.body_rml = "<span>Seed loaded successfully.</span>";
|
||||
toast.duration_ms = 3000;
|
||||
svc_ui->push_toast(mod_ctx, &toast);
|
||||
```
|
||||
|
||||
**Menu bar tabs:** `register_menu_tab` adds a tab to the in-game menu bar. `on_selected` fires when the user activates
|
||||
the tab: typically you'd push a window from it. The tab is removed by `unregister_menu_tab`, or automatically when the
|
||||
mod is disabled.
|
||||
@@ -420,6 +539,26 @@ existing documents restyle immediately, and future ones pick it up when created.
|
||||
host styles and may override them. Scope selectors tightly (use `[mod-id="..."]`!), especially for `UI_SCOPE_WINDOW`,
|
||||
unless changing host UI is intentional.
|
||||
|
||||
### WindowService (`mods/svc/window.h`)
|
||||
|
||||
Allows creating new windows that can be rendered to via `GfxService`.
|
||||
|
||||
```cpp
|
||||
IMPORT_SERVICE(WindowService, svc_window);
|
||||
|
||||
WindowDesc desc = WINDOW_DESC_INIT;
|
||||
desc.title = "My auxiliary view";
|
||||
desc.on_event = on_window_event;
|
||||
WindowHandle window = 0;
|
||||
svc_window->create_window(mod_ctx, &desc, &window);
|
||||
```
|
||||
|
||||
Window callbacks run on the game thread. A close event is only a request; call `destroy_window` when the mod is ready to
|
||||
close it. A window attached to a GfxService present target cannot be destroyed until that target is unregistered. Only
|
||||
one present target may be attached to a WindowService window at a time.
|
||||
|
||||
New windows are hidden by default so a mod can finish attaching graphics before calling `show_window`.
|
||||
|
||||
### GfxService (`mods/svc/gfx.h`)
|
||||
|
||||
**Requires `add_mod(... FEATURES webgpu)`**
|
||||
@@ -451,6 +590,30 @@ registered with `register_compute_type` follow the same worker-thread rule and r
|
||||
All WGPU handles from the service are borrowed. Resolved target views are valid for the current frame only. GPU objects
|
||||
created by a mod are owned by that mod and should be released in `mod_shutdown`.
|
||||
|
||||
#### External presentation
|
||||
|
||||
GfxService supports external presentation ("present targets") backed by either a WindowService window (via
|
||||
`register_window_present_target`) or a plain `WGPUSurface` (via `register_present_target`).
|
||||
|
||||
```cpp
|
||||
GfxPresentTargetDesc target_desc = GFX_PRESENT_TARGET_DESC_INIT;
|
||||
target_desc.render = render_auxiliary_view;
|
||||
GfxPresentTargetHandle target = 0;
|
||||
svc_gfx->register_window_present_target(mod_ctx, window, &target_desc, &target);
|
||||
|
||||
// From a stage callback:
|
||||
svc_gfx->push_present(mod_ctx, target, &payload, sizeof(payload));
|
||||
```
|
||||
|
||||
For WindowService windows, the surface is automatically reconfigured on window size changes.
|
||||
For plain `WBPUSurface`s, `resize_present_target` must be used to resize.
|
||||
|
||||
To create a `WGPUSurface` manually, `GfxDeviceInfo` holds the `WGPUInstance` and `WGPUAdapter` which can be used with
|
||||
`wgpuInstanceCreateSurface` and a chained `WGPUSurfaceSource*` struct.
|
||||
|
||||
`push_present` must be called every frame from a GfxService stage callback. If surface was lost, `push_present` returns
|
||||
`MOD_ERROR`. Unregister and re-register the target before trying again.
|
||||
|
||||
### CameraService (`mods/svc/camera.h`)
|
||||
|
||||
Converts a game view provided by a render callback into WebGPU-convention camera data. Matrix fields are column-major
|
||||
@@ -470,6 +633,9 @@ if (svc_camera->get_camera(mod_ctx, game_view, &camera) == MOD_OK) {
|
||||
first in-game frame. Projection matrices match the renderer's WebGPU clip convention and renderer depth convention
|
||||
(reversed-Z by default).
|
||||
|
||||
Camera operators allow overriding the main camera. When an operator callback returns true, its values replace the camera
|
||||
state for the current frame. Register and unregister using `register_camera_operator` / `unregister_camera_operator`.
|
||||
|
||||
### GamemodeService (`mods/svc/gamemode.h`)
|
||||
|
||||
Allows a mod to register a gamemode that allows the game to designate one form of gameplay (named a gamemode). This
|
||||
@@ -532,11 +698,10 @@ svc_gamemode->register_gamemode(mod_ctx, &gamemodeDesc);
|
||||
**Requires `add_mod(... FEATURES game)`**
|
||||
|
||||
Mods may hook the vast majority of game functions, including file-local static, private and virtual functions.
|
||||
`mods/hook.hpp` provides typed helpers over the hook service:
|
||||
`mods/svc/hook.hpp` provides typed helpers over the hook service:
|
||||
|
||||
```cpp
|
||||
#include "mods/hook.hpp"
|
||||
#include "mods/svc/hook.h"
|
||||
#include "mods/svc/hook.hpp"
|
||||
|
||||
IMPORT_SERVICE(HookService, svc_hook);
|
||||
|
||||
@@ -560,7 +725,7 @@ HookAction on_pos_move_pre(ModContext*, void* args, void* retval, void* userdata
|
||||
return HOOK_CONTINUE;
|
||||
}
|
||||
|
||||
mods::hook_add_pre<LinkPosMove>(svc_hook, on_pos_move_pre);
|
||||
mods::hook::add_pre<LinkPosMove>(on_pos_move_pre);
|
||||
```
|
||||
|
||||
### Post-hooks
|
||||
@@ -571,7 +736,7 @@ if any.
|
||||
```cpp
|
||||
void on_pos_move_post(ModContext*, void* args, void* retval, void* userdata) { ... }
|
||||
|
||||
mods::hook_add_post<LinkPosMove>(svc_hook, on_pos_move_post);
|
||||
mods::hook::add_post<LinkPosMove>(on_pos_move_post);
|
||||
```
|
||||
|
||||
### Replace-hooks
|
||||
@@ -586,7 +751,7 @@ void on_execute_replace(ModContext*, void* args, void* retval, void*) {
|
||||
}
|
||||
}
|
||||
|
||||
mods::hook_replace<LinkExecute>(svc_hook, on_execute_replace);
|
||||
mods::hook::replace<LinkExecute>(on_execute_replace);
|
||||
```
|
||||
|
||||
By default a second replace-hook on the same function is a conflict; `HookOptions` (`replace_policy`, `priority`,
|
||||
@@ -602,7 +767,7 @@ symbol name instead. You must supply the signature along with the name.
|
||||
DEFINE_HOOK_SYMBOL("daAlink_hookshotAtHitCallBack",
|
||||
void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*), HookshotHit);
|
||||
|
||||
mods::hook_add_pre<HookshotHit>(svc_hook, on_hookshot_hit_pre);
|
||||
mods::hook::add_pre<HookshotHit>(on_hookshot_hit_pre);
|
||||
...
|
||||
HookshotHit::g_orig(link, atObjInf, target, tgObjInf); // call through to the original
|
||||
```
|
||||
@@ -642,7 +807,7 @@ HookAction on_create_item_pre(ModContext*, void* args, void*, void*) {
|
||||
return HOOK_CONTINUE;
|
||||
}
|
||||
|
||||
mods::hook_add_pre<CreateItem>(svc_hook, on_create_item_pre);
|
||||
mods::hook::add_pre<CreateItem>(on_create_item_pre);
|
||||
```
|
||||
|
||||
For reference parameters (e.g. `const cXyz& pos`), `arg_ref<cXyz>` yields a direct reference.
|
||||
@@ -828,6 +993,6 @@ const char* nativeDir = svc_host->native_dir(mod_ctx); // read-only
|
||||
```
|
||||
|
||||
Libraries loaded explicitly by the mod remain its responsibility: stop their threads and unload them during
|
||||
`mod_shutdown`. Do not write into `native_dir`; use `mod_dir` for writable state. Native library namespaces are
|
||||
process-wide on some platforms, so two mods cannot safely assume that incompatible libraries with the same filename
|
||||
will remain isolated.
|
||||
`mod_shutdown`. Do not write into `native_dir`; use `data_dir` for persistent storage or `mod_dir` for temporary
|
||||
(session) storage. Native library namespaces are process-wide on some platforms, so two mods cannot safely assume that
|
||||
incompatible libraries with the same filename will remain isolated.
|
||||
|
||||
Vendored
+1
-1
Submodule extern/aurora updated: 81f12f31d2...8005d336ab
+1
Submodule extern/borealis added at 6fd955e7e6
+9
-18
@@ -1421,31 +1421,22 @@ 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/gamemode.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
|
||||
@@ -1502,7 +1493,15 @@ set(DUSK_FILES
|
||||
src/dusk/mods/svc/ui.cpp
|
||||
src/dusk/mods/svc/ui.hpp
|
||||
src/dusk/mods/svc/gamemode.cpp
|
||||
src/dusk/mods/svc/window.cpp
|
||||
src/dusk/mods/svc/window.hpp
|
||||
src/dusk/mods/svc/save.cpp
|
||||
src/dusk/mods/svc/save.hpp
|
||||
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
|
||||
@@ -1578,18 +1577,10 @@ 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
|
||||
src/helpers/endian.cpp
|
||||
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
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
description = "Dusklight — native PC port of the Twilight Princess decompilation";
|
||||
|
||||
inputs.nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
inputs.self.submodules = true;
|
||||
|
||||
outputs =
|
||||
{ self, nixpkgs }:
|
||||
@@ -58,21 +59,6 @@
|
||||
hasNodPrebuilt = nodPrebuiltInfo ? ${system};
|
||||
|
||||
aurora = builtins.pathExists "${self}/extern/aurora/CMakeLists.txt";
|
||||
needSubmodules = ''
|
||||
dusklight: The aurora submodule is not vendored. Add submodules=1 to build.
|
||||
|
||||
As a flake input:
|
||||
|
||||
dusklight.url = "git+https://github.com/TwilitRealm/dusklight?ref=main&submodules=1";
|
||||
|
||||
nix command:
|
||||
|
||||
nix run 'git+https://github.com/TwilitRealm/dusklight?submodules=1'
|
||||
|
||||
Local checkout:
|
||||
|
||||
nix run '.?submodules=1#dusklight'
|
||||
'';
|
||||
|
||||
dawn = pkgs.fetchzip {
|
||||
url = "https://github.com/encounter/dawn/releases/download/${dawnVersion}/dawn-${dawnInfo.${system}.triple}.tar.gz";
|
||||
@@ -140,6 +126,14 @@
|
||||
JSON = pkgs.nlohmann_json.src;
|
||||
XXHASH = pkgs.xxhash.src;
|
||||
ZSTD = pkgs.zstd.src;
|
||||
|
||||
|
||||
MINIZ = pkgs.fetchzip {
|
||||
url = "https://github.com/richgel999/miniz/releases/download/3.0.2/miniz-3.0.2.zip";
|
||||
hash = "sha256-DXysXkQEmoDAMMg1F8KexkwpXNyiHNzLJqXR9SMEkxk=";
|
||||
stripRoot = false;
|
||||
};
|
||||
|
||||
FMT = pkgs.fetchzip {
|
||||
url = "https://github.com/fmtlib/fmt/archive/refs/tags/12.1.0.tar.gz";
|
||||
hash = "sha256-ZmI1Dv0ZabPlxa02OpERI47jp7zFfjpeWCy1WyuPYZ0=";
|
||||
@@ -165,19 +159,16 @@
|
||||
};
|
||||
|
||||
dusklight =
|
||||
if !aurora then
|
||||
throw needSubmodules
|
||||
else
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "dusklight";
|
||||
version = versionSuffix;
|
||||
src = ./.;
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "dusklight";
|
||||
version = versionSuffix;
|
||||
src = ./.;
|
||||
|
||||
postUnpack = ''
|
||||
chmod -R u+w "$sourceRoot"
|
||||
substituteInPlace "$sourceRoot/extern/aurora/CMakeLists.txt" \
|
||||
--replace-warn "add_subdirectory(tests)" ""
|
||||
'';
|
||||
postUnpack = ''
|
||||
chmod -R u+w "$sourceRoot"
|
||||
substituteInPlace "$sourceRoot/extern/aurora/CMakeLists.txt" \
|
||||
--replace-warn "add_subdirectory(tests)" ""
|
||||
'';
|
||||
|
||||
nativeBuildInputs = [
|
||||
pkgs.cmake
|
||||
@@ -237,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"
|
||||
|
||||
@@ -130,8 +130,8 @@ private:
|
||||
}; // Size = 0x28
|
||||
|
||||
struct mDoDvdThdStack {
|
||||
u8 stack[4096];
|
||||
} ATTRIBUTE_ALIGN(16);
|
||||
ATTRIBUTE_ALIGN(16) u8 stack[4096];
|
||||
};
|
||||
|
||||
struct mDoDvdThd {
|
||||
static s32 main(void*);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define JASCALC_H
|
||||
|
||||
#include <types.h>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
/**
|
||||
@@ -10,16 +11,31 @@
|
||||
*/
|
||||
struct JASCalc {
|
||||
static void imixcopy(const s16*, const s16*, s16*, u32);
|
||||
static void bcopyfast(const void* src, void* dest, u32 size);
|
||||
#if TARGET_PC
|
||||
static void bcopyfast(const void* src, void* dest, u32 size) {
|
||||
std::memcpy(dest, src, size);
|
||||
}
|
||||
#if TARGET_ANDROID
|
||||
static void _bcopy(const void* src, void* dest, u32 size);
|
||||
static void _bcopy(const void* src, void* dest, u32 size) {
|
||||
#else
|
||||
static void bcopy(const void* src, void* dest, u32 size);
|
||||
static void bcopy(const void* src, void* dest, u32 size) {
|
||||
#endif
|
||||
static void bzerofast(void* dest, u32 size);
|
||||
std::memcpy(dest, src, size);
|
||||
}
|
||||
static void bzerofast(void* dest, u32 size) {
|
||||
std::memset(dest, 0, size);
|
||||
}
|
||||
#if TARGET_ANDROID
|
||||
static void _bzero(void* dest, u32 size);
|
||||
static void _bzero(void* dest, u32 size) {
|
||||
#else
|
||||
static void bzero(void* dest, u32 size) {
|
||||
#endif
|
||||
std::memset(dest, 0, size);
|
||||
}
|
||||
#else
|
||||
static void bcopyfast(const void* src, void* dest, u32 size);
|
||||
static void bcopy(const void* src, void* dest, u32 size);
|
||||
static void bzerofast(void* dest, u32 size);
|
||||
static void bzero(void* dest, u32 size);
|
||||
#endif
|
||||
static f32 pow2(f32);
|
||||
|
||||
@@ -23,9 +23,9 @@ DUSK_GAME_DATA Vec J3DSys::mParentS;
|
||||
DUSK_GAME_DATA J3DTexCoordScaleInfo J3DSys::sTexCoordScaleTable[8];
|
||||
|
||||
#if TARGET_PC // Original game bug, array is too small.
|
||||
static u8 NullTexData[0x20] ATTRIBUTE_ALIGN(32) = {0};
|
||||
ATTRIBUTE_ALIGN(32) static u8 NullTexData[0x20] = {0};
|
||||
#else
|
||||
static u8 NullTexData[0x10] ATTRIBUTE_ALIGN(32) = {0};
|
||||
ATTRIBUTE_ALIGN(32) static u8 NullTexData[0x10] = {0};
|
||||
#endif
|
||||
|
||||
static Mtx j3dIdentityMtx = {
|
||||
|
||||
@@ -11,6 +11,7 @@ void JASCalc::imixcopy(const s16* s1, const s16* s2, s16* dst, u32 n) {
|
||||
}
|
||||
}
|
||||
|
||||
#if !TARGET_PC
|
||||
void JASCalc::bcopyfast(const void* src, void* dest, u32 size) {
|
||||
JUT_ASSERT(226, (reinterpret_cast<uintptr_t>(src) & 0x03) == 0);
|
||||
JUT_ASSERT(227, (reinterpret_cast<uintptr_t>(dest) & 0x03) == 0);
|
||||
@@ -33,11 +34,7 @@ void JASCalc::bcopyfast(const void* src, void* dest, u32 size) {
|
||||
}
|
||||
}
|
||||
|
||||
#if TARGET_ANDROID
|
||||
void JASCalc::_bcopy(const void* src, void* dest, u32 size) {
|
||||
#else
|
||||
void JASCalc::bcopy(const void* src, void* dest, u32 size) {
|
||||
#endif
|
||||
u32* usrc;
|
||||
u32* udest;
|
||||
|
||||
@@ -94,11 +91,7 @@ void JASCalc::bzerofast(void* dest, u32 size) {
|
||||
}
|
||||
}
|
||||
|
||||
#if TARGET_ANDROID
|
||||
void JASCalc::_bzero(void* dest, u32 size) {
|
||||
#else
|
||||
void JASCalc::bzero(void* dest, u32 size) {
|
||||
#endif
|
||||
u32* udest;
|
||||
u8* bdest = (u8*)dest;
|
||||
if ((size & 0x1f) == 0 && (reinterpret_cast<uintptr_t>(dest) & 0x1f) == 0) {
|
||||
@@ -139,6 +132,7 @@ void JASCalc::bzero(void* dest, u32 size) {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if AVOID_UB
|
||||
DUSK_GAME_DATA s16 const JASCalc::CUTOFF_TO_IIR_TABLE[129][4] = {
|
||||
|
||||
@@ -99,14 +99,14 @@ void JASDsp::invalChannelAll() {
|
||||
DCInvalidateRange(CH_BUF, sizeof(TChannel) * DSP_CHANNELS);
|
||||
}
|
||||
|
||||
DUSK_GAME_DATA u8 const ATTRIBUTE_ALIGN(32) JASDsp::DSPADPCM_FILTER[64] = {
|
||||
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA u8 const JASDsp::DSPADPCM_FILTER[64] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, 0x04, 0x00,
|
||||
0x10, 0x00, 0xF8, 0x00, 0x0E, 0x00, 0xFA, 0x00, 0x0C, 0x00, 0xFC, 0x00, 0x12, 0x00, 0xF6, 0x00,
|
||||
0x10, 0x68, 0xF7, 0x38, 0x12, 0xC0, 0xF7, 0x04, 0x14, 0x00, 0xF4, 0x00, 0x08, 0x00, 0xF8, 0x00,
|
||||
0x04, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0x04, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
DUSK_GAME_DATA u32 const ATTRIBUTE_ALIGN(32) JASDsp::DSPRES_FILTER[320] = {
|
||||
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA u32 const JASDsp::DSPRES_FILTER[320] = {
|
||||
0x0C3966AD,
|
||||
0x0D46FFDF,
|
||||
0x0B396696,
|
||||
|
||||
@@ -25,7 +25,7 @@ void DspHandShake(void*) {
|
||||
Dsp_Running_Start();
|
||||
}
|
||||
|
||||
static u8 jdsp[7936] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 jdsp[7936] = {
|
||||
0x02, 0x9F, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFF, 0x00, 0x00, 0x02, 0xFF, 0x00, 0x00,
|
||||
0x02, 0xFF, 0x00, 0x00, 0x02, 0xFF, 0x00, 0x00, 0x02, 0xFF, 0x00, 0x00, 0x02, 0x9F, 0x06, 0xA5,
|
||||
0x02, 0x9F, 0x00, 0x4E, 0x12, 0x05, 0x02, 0xBF, 0x00, 0x57, 0x81, 0x00, 0x00, 0x9F, 0x10, 0x00,
|
||||
@@ -524,9 +524,9 @@ static u8 jdsp[7936] ATTRIBUTE_ALIGN(32) = {
|
||||
0x80, 0x01, 0x02, 0xBF, 0x00, 0xF4, 0x02, 0xDF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static DSPTaskInfo audio_task ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static DSPTaskInfo audio_task;
|
||||
|
||||
static u8 AUDIO_YIELD_BUFFER[8192] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 AUDIO_YIELD_BUFFER[8192];
|
||||
|
||||
void DspBoot(void (*requestCallback)(void*)) {
|
||||
DspInitWork();
|
||||
|
||||
@@ -452,13 +452,13 @@ static void dummy() {
|
||||
JUTXfb::getManager()->setDisplayingXfbIndex(0);
|
||||
}
|
||||
|
||||
static Mtx e_mtx ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static Mtx e_mtx = {
|
||||
{1.0f, 0.0f, 0.0f, 0.0f},
|
||||
{0.0f, 1.0f, 0.0f, 0.0f},
|
||||
{0.0f, 0.0f, 1.0f, 0.0f},
|
||||
};
|
||||
|
||||
static u8 clear_z_TX[64] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 clear_z_TX[64] = {
|
||||
0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF,
|
||||
0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
|
||||
@@ -16,10 +16,10 @@ inline f64 getConst2() {
|
||||
return 9.765625E-4;
|
||||
}
|
||||
|
||||
DUSK_GAME_DATA TSinCosTable<13, f32> sincosTable_ ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA TSinCosTable<13, f32> sincosTable_;
|
||||
|
||||
DUSK_GAME_DATA TAtanTable<1024, f32> atanTable_ ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA TAtanTable<1024, f32> atanTable_;
|
||||
|
||||
DUSK_GAME_DATA TAsinAcosTable<1024, f32> asinAcosTable_ ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA TAsinAcosTable<1024, f32> asinAcosTable_;
|
||||
|
||||
} // namespace JMath
|
||||
|
||||
@@ -46,7 +46,7 @@ JPAResource::JPAResource() {
|
||||
mUsrIdx = fldNum = keyNum = texNum = mpCalcEmitterFuncListNum = mpDrawEmitterFuncListNum = mpDrawEmitterChildFuncListNum = mpCalcParticleFuncListNum = mpDrawParticleFuncListNum = mpCalcParticleChildFuncListNum = mpDrawParticleChildFuncListNum = 0;
|
||||
}
|
||||
|
||||
static u8 jpa_pos[324] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 jpa_pos[324] = {
|
||||
0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x32, 0xCE, 0x00, 0x00, 0xCE, 0x00, 0xE7, 0x00, 0x00, 0x19,
|
||||
0x00, 0x00, 0x19, 0xCE, 0x00, 0xE7, 0xCE, 0x00, 0xCE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCE,
|
||||
0x00, 0xCE, 0xCE, 0x00, 0x00, 0x19, 0x00, 0x32, 0x19, 0x00, 0x32, 0xE7, 0x00, 0x00, 0xE7, 0x00,
|
||||
@@ -70,7 +70,7 @@ static u8 jpa_pos[324] ATTRIBUTE_ALIGN(32) = {
|
||||
0x00, 0x00, 0x00, 0xCE,
|
||||
};
|
||||
|
||||
static u8 jpa_crd[32] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 jpa_crd[32] = {
|
||||
0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x02, 0x01, 0x00, 0x01,
|
||||
0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x02, 0x02, 0x00, 0x02,
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#endif
|
||||
#include "global.h"
|
||||
|
||||
DUSK_GAME_DATA u8 const JUTResFONT_Ascfont_fix12[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA u8 const JUTResFONT_Ascfont_fix12[] = {
|
||||
0x46, 0x4F, 0x4E, 0x54, 0x62, 0x66, 0x6E, 0x31, 0x00, 0x00, 0x41, 0x60, 0x00, 0x00, 0x00, 0x04,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x49, 0x4E, 0x46, 0x31, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C,
|
||||
|
||||
@@ -136,7 +136,7 @@ void TRK__read_aram(__REGISTER int c, __REGISTER u32 p2, void* p3) {
|
||||
}
|
||||
|
||||
void TRK__write_aram(__REGISTER int c, __REGISTER u32 p2, void* p3) {
|
||||
u8 buff[32] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 buff[32];
|
||||
u32 err;
|
||||
__REGISTER int count = c;
|
||||
__REGISTER u32 bf;
|
||||
|
||||
@@ -85,7 +85,7 @@ DSError TRKDoSupportMask(TRKBuffer*) {
|
||||
}
|
||||
|
||||
DSError TRKDoReadMemory(TRKBuffer* buffer) {
|
||||
u8 buf[0x820] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 buf[0x820];
|
||||
size_t tempLength;
|
||||
int result;
|
||||
int replyErr;
|
||||
@@ -158,7 +158,7 @@ DSError TRKDoReadMemory(TRKBuffer* buffer) {
|
||||
}
|
||||
|
||||
DSError TRKDoWriteMemory(TRKBuffer* b) {
|
||||
u8 buf[0x820] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 buf[0x820];
|
||||
size_t tempLength;
|
||||
int options;
|
||||
int result;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
#include "__ax.h"
|
||||
|
||||
static s32 __AXBufferAuxA[3][480] ATTRIBUTE_ALIGN(32);
|
||||
static s32 __AXBufferAuxB[3][480] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static s32 __AXBufferAuxA[3][480];
|
||||
ATTRIBUTE_ALIGN(32) static s32 __AXBufferAuxB[3][480];
|
||||
|
||||
static void (* __AXCallbackAuxA)(void*, void*);
|
||||
static void (* __AXCallbackAuxB)(void*, void*);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
#include "__ax.h"
|
||||
|
||||
static AXSPB __AXStudio ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static AXSPB __AXStudio;
|
||||
|
||||
static s32 __AXSpbAL;
|
||||
static s32 __AXSpbAR;
|
||||
|
||||
@@ -35,9 +35,9 @@ static u32 __AXAuxMixCycles[32] = {
|
||||
0x000009E2, 0x00000E97
|
||||
};
|
||||
|
||||
static AXPB __AXPB[AX_MAX_VOICES] ATTRIBUTE_ALIGN(32);
|
||||
static AXPBITDBUFFER __AXITD[AX_MAX_VOICES] ATTRIBUTE_ALIGN(32);
|
||||
static AXPBU __AXUpdates[AX_MAX_VOICES] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static AXPB __AXPB[AX_MAX_VOICES];
|
||||
ATTRIBUTE_ALIGN(32) static AXPBITDBUFFER __AXITD[AX_MAX_VOICES];
|
||||
ATTRIBUTE_ALIGN(32) static AXPBU __AXUpdates[AX_MAX_VOICES];
|
||||
static AXVPB __AXVPB[AX_MAX_VOICES];
|
||||
|
||||
static u32 __AXMaxDspCycles;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
u16 axDspSlaveLength = (AX_DSP_SLAVE_LENGTH * 2);
|
||||
|
||||
u16 axDspSlave[AX_DSP_SLAVE_LENGTH] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) u16 axDspSlave[AX_DSP_SLAVE_LENGTH] = {
|
||||
0x0000, 0x0000, 0x029F, 0x0E88, 0x029F, 0x0E97,
|
||||
0x029F, 0x0EB3, 0x029F, 0x0ED3, 0x029F, 0x0ED9,
|
||||
0x029F, 0x0F0B, 0x029F, 0x0F11, 0x1302, 0x1303,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#include "__card.h"
|
||||
|
||||
static u8 CardData[352] ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) = {
|
||||
ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) static u8 CardData[352] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x02, 0xFF, 0x00, 0x21, 0x13, 0x06, 0x12, 0x03, 0x12, 0x04,
|
||||
0x13, 0x05, 0x00, 0x92, 0x00, 0xFF, 0x00, 0x88, 0xFF, 0xFF, 0x00, 0x89, 0xFF, 0xFF, 0x00, 0x8A, 0xFF, 0xFF, 0x00,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <dolphin/demo.h>
|
||||
#include "sdk_math.h"
|
||||
|
||||
static s16 __AVX_internal_buffer[3200] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static s16 __AVX_internal_buffer[3200];
|
||||
|
||||
static void (*__AVX_save_isr)(void);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include <dolphin/dolphin.h>
|
||||
#include <dolphin/demo.h>
|
||||
|
||||
u32 DEMOFontBitmap[768] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) u32 DEMOFontBitmap[768] = {
|
||||
0x00000000,
|
||||
0x00000000,
|
||||
0x00000000,
|
||||
|
||||
@@ -43,7 +43,7 @@ void* __piReg;
|
||||
GXBool __GXinBegin;
|
||||
#endif
|
||||
|
||||
static u16 DefaultTexData[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u16 DefaultTexData[] = {
|
||||
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
|
||||
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
|
||||
};
|
||||
|
||||
@@ -7,11 +7,11 @@ const char* __MCCVersion = "<< Dolphin SDK - MCC\tdebug build: Apr 5 2004 03:57
|
||||
const char* __MCCVersion = "<< Dolphin SDK - MCC\trelease build: Apr 5 2004 04:15:49 (0x2301) >>";
|
||||
#endif
|
||||
|
||||
static MCC_ChannelInfo gChannelInfo[16] ATTRIBUTE_ALIGN(32);
|
||||
static char gStreamWork[32] ATTRIBUTE_ALIGN(32);
|
||||
static char m_szAdapterMode[32] ATTRIBUTE_ALIGN(32);
|
||||
static char m_szInitCode[32] ATTRIBUTE_ALIGN(32);
|
||||
static MCC_Info channelInfo[16] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static MCC_ChannelInfo gChannelInfo[16];
|
||||
ATTRIBUTE_ALIGN(32) static char gStreamWork[32];
|
||||
ATTRIBUTE_ALIGN(32) static char m_szAdapterMode[32];
|
||||
ATTRIBUTE_ALIGN(32) static char m_szInitCode[32];
|
||||
ATTRIBUTE_ALIGN(32) static MCC_Info channelInfo[16];
|
||||
|
||||
volatile static BOOL gIsChannelinfoDirty = TRUE;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
#include "__os.h"
|
||||
|
||||
static SramControl Scb ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT);
|
||||
ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) static SramControl Scb;
|
||||
|
||||
// prototypes
|
||||
static int GetRTC(u32* rtc);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
#include "__card.h"
|
||||
|
||||
static u8 CardData[352] ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) = {
|
||||
ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) static u8 CardData[352] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x02, 0xFF, 0x00, 0x21, 0x13, 0x06, 0x12, 0x03, 0x12, 0x04,
|
||||
0x13, 0x05, 0x00, 0x92, 0x00, 0xFF, 0x00, 0x88, 0xFF, 0xFF, 0x00, 0x89, 0xFF, 0xFF, 0x00, 0x8A, 0xFF, 0xFF, 0x00,
|
||||
|
||||
@@ -44,7 +44,7 @@ static u32 LastError;
|
||||
static BOOL ResetRequired;
|
||||
static u32 MotorState;
|
||||
static volatile OSTime LastResetEnd;
|
||||
static u32 __DVDNumTmdBytes ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u32 __DVDNumTmdBytes;
|
||||
static DVDGameTOC* GameToc;
|
||||
static DVDPartitionInfo* PartInfo;
|
||||
static DVDPartitionInfo* BootGameInfo;
|
||||
@@ -80,10 +80,10 @@ static DVDCommandBlock DummyCommandBlock;
|
||||
static OSAlarm ResetAlarm;
|
||||
static OSAlarm CoverAlarm;
|
||||
|
||||
static u8 __DVDGameTocBuffer[OSRoundUp32B(sizeof(DVDGameTOC) * 4)] ATTRIBUTE_ALIGN(32);
|
||||
static u8 __DVDPartInfoBuffer[OSRoundUp32B(sizeof(DVDPartitionInfo) * 4)] ATTRIBUTE_ALIGN(32);
|
||||
static u8 __DVDTmdBuffer[OSRoundUp32B(sizeof(ESTitleMeta))] ATTRIBUTE_ALIGN(32);
|
||||
static u8 __DVDTicketViewBuffer[OSRoundUp32B(sizeof(ESTicketView))] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 __DVDGameTocBuffer[OSRoundUp32B(sizeof(DVDGameTOC) * 4)];
|
||||
ATTRIBUTE_ALIGN(32) static u8 __DVDPartInfoBuffer[OSRoundUp32B(sizeof(DVDPartitionInfo) * 4)];
|
||||
ATTRIBUTE_ALIGN(32) static u8 __DVDTmdBuffer[OSRoundUp32B(sizeof(ESTitleMeta))];
|
||||
ATTRIBUTE_ALIGN(32) static u8 __DVDTicketViewBuffer[OSRoundUp32B(sizeof(ESTicketView))];
|
||||
|
||||
static OSAlarm FatalAlarm;
|
||||
DVDCommandBlock __DVDStopMotorCommandBlock;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "__os.h"
|
||||
#include "__dvd.h"
|
||||
|
||||
static u8 CheckBuffer[32] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 CheckBuffer[32];
|
||||
|
||||
static volatile BOOL lowDone = TRUE;
|
||||
static volatile u32 lowIntType = 0;
|
||||
|
||||
@@ -8,15 +8,15 @@ static volatile u8 requestInProgress = FALSE;
|
||||
static u8 breakRequested;
|
||||
static u8 callbackInProgress;
|
||||
|
||||
static u32 registerBuf[8] ATTRIBUTE_ALIGN(32);
|
||||
static u32 statusRegister[8] ATTRIBUTE_ALIGN(32);
|
||||
static u32 controlRegister[8] ATTRIBUTE_ALIGN(32);
|
||||
static s32 lastTicketError[8] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u32 registerBuf[8];
|
||||
ATTRIBUTE_ALIGN(32) static u32 statusRegister[8];
|
||||
ATTRIBUTE_ALIGN(32) static u32 controlRegister[8];
|
||||
ATTRIBUTE_ALIGN(32) static s32 lastTicketError[8];
|
||||
|
||||
static u32 readLength;
|
||||
static u32 spinUpValue;
|
||||
|
||||
static diRegVals_t diRegValCache ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static diRegVals_t diRegValCache;
|
||||
|
||||
static u8 DVDLowInitCalled = FALSE;
|
||||
|
||||
@@ -42,8 +42,8 @@ typedef struct dvdContext {
|
||||
|
||||
static int freeDvdContext = 0;
|
||||
static u8 dvdContextsInited = FALSE;
|
||||
static dvdContext_t dvdContexts[4] ATTRIBUTE_ALIGN(32);
|
||||
static IOSIoVector ioVec[10] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static dvdContext_t dvdContexts[4];
|
||||
ATTRIBUTE_ALIGN(32) static IOSIoVector ioVec[10];
|
||||
|
||||
static void* ddrAllocAligned32(const int size) {
|
||||
void* low, *high;
|
||||
|
||||
@@ -11,8 +11,8 @@ static NANDCommandBlock NandCb;
|
||||
static NANDFileInfo NandInfo;
|
||||
static DVDCBCallback Callback;
|
||||
static u32 NextOffset;
|
||||
DVDErrorInfo __ErrorInfo ATTRIBUTE_ALIGN(32);
|
||||
DVDErrorInfo __FirstErrorInfo ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) DVDErrorInfo __ErrorInfo;
|
||||
ATTRIBUTE_ALIGN(32) DVDErrorInfo __FirstErrorInfo;
|
||||
|
||||
void cbForNandClose(s32 result, NANDCommandBlock* block) {
|
||||
if (Callback) {
|
||||
|
||||
@@ -33,7 +33,7 @@ s32 ESP_CloseLib(void) {
|
||||
|
||||
s32 ESP_LaunchTitle(u64 titleID, ESTicketView* pTicketView) {
|
||||
s32 ret = 0;
|
||||
u8 buf[256] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 buf[256];
|
||||
IOSIoVector* vec = (IOSIoVector*)(buf + 208);
|
||||
u64* id = (u64*)buf;
|
||||
|
||||
@@ -63,7 +63,7 @@ out:
|
||||
|
||||
s32 ESP_GetTicketViews(ESTitleId titleId, ESTicketView* ticketViewList, u32* ticketViewCnt) {
|
||||
s32 rv = 0;
|
||||
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
|
||||
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
|
||||
ESTitleId* p1 = (ESTitleId*)__esBuf;
|
||||
u32* p2 = (u32*)(__esBuf + 32);
|
||||
@@ -111,7 +111,7 @@ out:
|
||||
|
||||
s32 ESP_DiGetTicketView(const void* ticket, ESTicketView* ticketView) {
|
||||
s32 rv = 0;
|
||||
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
|
||||
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
|
||||
|
||||
if (__esFd < 0 || ticketView == NULL) {
|
||||
@@ -142,7 +142,7 @@ out:
|
||||
|
||||
s32 ESP_DiGetTmd(ESTitleMeta* tmd, u32* tmdSize) {
|
||||
s32 rv = 0;
|
||||
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
|
||||
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
|
||||
u32* p1 = (u32*)__esBuf;
|
||||
|
||||
@@ -184,7 +184,7 @@ out:
|
||||
|
||||
s32 ESP_GetTmdView(ESTitleId titleId, ESTmdView* tmdView, u32* size) {
|
||||
s32 rv = 0;
|
||||
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
|
||||
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
|
||||
ESTitleId* p1 = (ESTitleId*)__esBuf;
|
||||
u32* p2 = (u32*)(__esBuf + 32);
|
||||
@@ -233,7 +233,7 @@ out:
|
||||
|
||||
s32 ESP_GetDataDir(ESTitleId titleId, char* dataDir) {
|
||||
s32 rv = 0;
|
||||
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
|
||||
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
|
||||
ESTitleId* p1 = (ESTitleId*)__esBuf;
|
||||
|
||||
@@ -260,7 +260,7 @@ out:
|
||||
|
||||
s32 ESP_GetTitleId(ESTitleId* titleId) {
|
||||
s32 rv = 0;
|
||||
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
|
||||
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
|
||||
|
||||
if (__esFd < 0 || titleId == NULL) {
|
||||
@@ -283,7 +283,7 @@ out:
|
||||
|
||||
s32 ESP_GetConsumption(ESTicketId ticketId, ESLpEntry* entries, u32* nEntries) {
|
||||
s32 rv = 0;
|
||||
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
|
||||
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
|
||||
ESTicketId* p1 = (ESTicketId*)__esBuf;
|
||||
u32* p2 = (u32*)(__esBuf + 32);
|
||||
|
||||
@@ -25,7 +25,7 @@ typedef struct isfs_GetUsage {
|
||||
} isfs_GetUsage;
|
||||
|
||||
typedef struct __isfsCtxt {
|
||||
u8 ioBuf[ROUNDUP(256)] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 ioBuf[ROUNDUP(256)];
|
||||
ISFSCallback cb;
|
||||
void* ctxt;
|
||||
u32 func;
|
||||
|
||||
@@ -49,7 +49,7 @@ volatile void* __piReg;
|
||||
GXBool __GXinBegin;
|
||||
#endif
|
||||
|
||||
static u16 DefaultTexData[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u16 DefaultTexData[] = {
|
||||
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
|
||||
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace nw4hbm {
|
||||
|
||||
private:
|
||||
/* 0x00 (base) */
|
||||
/* 0xD4 */ ut::Color mVtxColors[VERTEXCOLOR_MAX] ATTRIBUTE_ALIGN(4);
|
||||
/* 0xD4 */ ATTRIBUTE_ALIGN(4) ut::Color mVtxColors[VERTEXCOLOR_MAX];
|
||||
/* 0xE4 */ detail::TexCoordAry mTexCoordAry;
|
||||
};
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace nw4hbm {
|
||||
public:
|
||||
/* 0x14 */ ut::LinkListNode mLinkNode;
|
||||
|
||||
static u8 mMramBuf[LOAD_BUFFER_SIZE] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 mMramBuf[LOAD_BUFFER_SIZE];
|
||||
};
|
||||
typedef ut::LinkList<LoadCommand, offsetof(LoadCommand, mLinkNode)> LoadCommandList;
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ static u32 __relnchFl = 0;
|
||||
|
||||
typedef struct IOSRpcRequest {
|
||||
IOSResourceRequest request;
|
||||
IOSIpcCb cb ATTRIBUTE_ALIGN(32); // I am assuming this is aligned due to where cbArg is stored, and I see nothing between cb and callback_arg?
|
||||
ATTRIBUTE_ALIGN(32) IOSIpcCb cb; // I am assuming this is aligned due to where cbArg is stored, and I see nothing between cb and callback_arg?
|
||||
void* callback_arg;
|
||||
u32 relaunch_flag;
|
||||
OSThreadQueue thread_queue;
|
||||
@@ -37,7 +37,7 @@ static IOSRpcRequest* __relnchRpcSave = 0;
|
||||
|
||||
#define ROUNDUP(sz) (((u32)(sz) + (IPC_BUF_CNT / 2 - 1)) & ~(u32)(IPC_BUF_CNT / 2 - 1))
|
||||
|
||||
static u8 __rpcBuf[ROUNDUP(sizeof(IOSRpcRequest))] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 __rpcBuf[ROUNDUP(sizeof(IOSRpcRequest))];
|
||||
|
||||
static struct {
|
||||
u32 rcount;
|
||||
|
||||
@@ -33,8 +33,8 @@ enum LibState {
|
||||
};
|
||||
|
||||
static enum LibState s_libState = STATE_NOT_INITIALIZED;
|
||||
static char s_currentDir[64] ATTRIBUTE_ALIGN(32) = "/";
|
||||
static char s_homeDir[64] ATTRIBUTE_ALIGN(32) = "";
|
||||
ATTRIBUTE_ALIGN(32) static char s_currentDir[64] = "/";
|
||||
ATTRIBUTE_ALIGN(32) static char s_homeDir[64] = "";
|
||||
|
||||
static BOOL nandOnShutdown(BOOL final, u32 event);
|
||||
void nandConvertPath(char* abspath, const char* wd, const char* relpath);
|
||||
@@ -262,7 +262,7 @@ s32 nandConvertErrorCode(const ISFSError err) {
|
||||
for (; i < sizeof(ERRMAP) / 4; i = i + 2) {
|
||||
if (ERRMAP[i] == err) {
|
||||
if (err == ISFS_ERROR_ECC_CRIT || err == ISFS_ERROR_HMAC || err == ISFS_ERROR_UNKNOWN || err == IOS_ERROR_UNKNOWN || err == IOS_ERROR_ECC_CRIT) {
|
||||
char buf[128] ATTRIBUTE_ALIGN(64);
|
||||
ATTRIBUTE_ALIGN(64) char buf[128];
|
||||
sprintf(buf, "ISFS error code: %d", err);
|
||||
NANDLoggingAddMessageAsync(nandLoggingCallback, err, buf);
|
||||
}
|
||||
@@ -279,7 +279,7 @@ s32 nandConvertErrorCode(const ISFSError err) {
|
||||
|
||||
OSReport("CAUTION! Unexpected error code [%d] was found.\n", err);
|
||||
{
|
||||
char buf[128] ATTRIBUTE_ALIGN(64);
|
||||
ATTRIBUTE_ALIGN(64) char buf[128];
|
||||
sprintf(buf, "ISFS unexpected error code: %d", err);
|
||||
NANDLoggingAddMessageAsync(nandLoggingCallback, err, buf);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
static IOSFd s_fd = -255;
|
||||
static IOSError s_err = ISFS_ERROR_UNKNOWN;
|
||||
static int s_stage;
|
||||
static char s_message[256] ATTRIBUTE_ALIGN(64);
|
||||
ATTRIBUTE_ALIGN(64) static char s_message[256];
|
||||
static NANDLoggingCallback s_callback = 0;
|
||||
|
||||
static void asyncRoutine(ISFSError, void*);
|
||||
@@ -70,8 +70,8 @@ static void callbackRoutine(BOOL result) {
|
||||
|
||||
static void asyncRoutine(ISFSError result, void *ctxt) {
|
||||
ISFSError ret = ISFS_ERROR_UNKNOWN;
|
||||
static char s_rBuf[256] ATTRIBUTE_ALIGN(64);
|
||||
static char s_wBuf[256] ATTRIBUTE_ALIGN(64);
|
||||
ATTRIBUTE_ALIGN(64) static char s_rBuf[256];
|
||||
ATTRIBUTE_ALIGN(64) static char s_wBuf[256];
|
||||
++s_stage;
|
||||
|
||||
if (s_stage == 2) {
|
||||
|
||||
@@ -293,7 +293,7 @@ s32 NANDMove(const char* path, const char* destDir) {
|
||||
}
|
||||
|
||||
static ISFSError nandGetFileStatus(IOSFd fd, u32* length, u32* pos) {
|
||||
ISFSFileStats fstat ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) ISFSFileStats fstat;
|
||||
ISFSError result = ISFS_GetFileStats(fd, &fstat);
|
||||
if (result == ISFS_ERROR_OK) {
|
||||
if (length) {
|
||||
|
||||
@@ -8,7 +8,7 @@ void __OSRelaunchTitle(u32 resetCode) {
|
||||
s32 rc = 0;
|
||||
u32 ticketCnt = 1;
|
||||
ESTicketView* tik = NULL;
|
||||
ESTitleId titleId ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) ESTitleId titleId;
|
||||
__OSPlayTimeType type = OSPLAYTIME_PERMANENT;
|
||||
u32 remain = 0;
|
||||
u8* bi2 = NULL;
|
||||
|
||||
@@ -85,7 +85,7 @@ NWC24Err NWC24iSynchronizeRtcCounter(BOOL param_0) {
|
||||
}
|
||||
|
||||
NWC24Err NWC24SuspendScheduler(void) {
|
||||
static u8 susResult[0x20] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 susResult[0x20];
|
||||
NWC24Err rt = NWC24_OK;
|
||||
NWC24Err closeRt = NWC24_OK;
|
||||
IOSFd fd;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <revolution/nand.h>
|
||||
|
||||
static BOOL PlayRecordGet = FALSE;
|
||||
static OSPlayRecord PlayRecord ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static OSPlayRecord PlayRecord;
|
||||
static NANDFileInfo FileInfo;
|
||||
static NANDCommandBlock Block;
|
||||
static s32 PlayRecordState = 9;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#include "__os.h"
|
||||
|
||||
OSAlarm __OSExpireAlarm ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) OSAlarm __OSExpireAlarm;
|
||||
OSTime __OSExpireTime;
|
||||
OSPlayTimeCallbackFunc __OSExpireCallback;
|
||||
BOOL __OSExpireSetExpiredFlag;
|
||||
@@ -103,7 +103,7 @@ BOOL __OSWriteExpiredFlag(void) {
|
||||
s32 rv = 0;
|
||||
NANDFileInfo nInfo;
|
||||
BOOL openNInfo = FALSE;
|
||||
u8 titleId[32] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 titleId[32];
|
||||
|
||||
rv = NANDPrivateCreate("/shared2/expired", 63, 0);
|
||||
|
||||
@@ -162,7 +162,7 @@ BOOL __OSWriteExpiredFlagIfSet(void) {
|
||||
void* __OSPlayTimeRebootThread(void* args) {
|
||||
BOOL enabled;
|
||||
u32 frames, fadeShift = 1;
|
||||
__OSExpireAIFadeStruct aiFade ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) __OSExpireAIFadeStruct aiFade;
|
||||
|
||||
__OSExpireAIFade = &aiFade;
|
||||
memset(__OSExpireAIFade, 0, sizeof(__OSExpireAIFadeStruct));
|
||||
@@ -227,9 +227,9 @@ out:
|
||||
s32 __OSGetPlayTime(ESTicketView* ticket, __OSPlayTimeType* type, u32* playTime) {
|
||||
s32 rv;
|
||||
u32 i;
|
||||
ESLpEntry lpEntry[8] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) ESLpEntry lpEntry[8];
|
||||
u32 numCc = 0, seenOther = 0;
|
||||
ESTicketView ticketAligned ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) ESTicketView ticketAligned;
|
||||
|
||||
ASSERTLINE(601, ticket && type && playTime);
|
||||
|
||||
@@ -288,7 +288,7 @@ out:
|
||||
|
||||
s32 __OSGetPlayTimeCurrent(__OSPlayTimeType* type, u32* playTime) {
|
||||
s32 rv;
|
||||
ESTicketView ticket ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) ESTicketView ticket;
|
||||
|
||||
ASSERTLINE(676, type && playTime);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
#include "__os.h"
|
||||
|
||||
static SramControl Scb ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT);
|
||||
ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) static SramControl Scb;
|
||||
|
||||
// prototypes
|
||||
static int GetRTC(u32* rtc);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <revolution/nand.h>
|
||||
#include <cstring>
|
||||
|
||||
static OSStateFlags StateFlags ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static OSStateFlags StateFlags;
|
||||
|
||||
static u32 CheckSum(OSStateFlags* flags) {
|
||||
u32* ptr, i, sum;
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
|
||||
#include <revolution/private/iosrestypes.h>
|
||||
|
||||
static u32 StmImInBuf[8] ATTRIBUTE_ALIGN(32);
|
||||
static u32 StmImOutBuf[8] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u32 StmImInBuf[8];
|
||||
ATTRIBUTE_ALIGN(32) static u32 StmImOutBuf[8];
|
||||
|
||||
static u32 StmVdInBuf[8] ATTRIBUTE_ALIGN(32);
|
||||
static u32 StmVdOutBuf[8] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u32 StmVdInBuf[8];
|
||||
ATTRIBUTE_ALIGN(32) static u32 StmVdOutBuf[8];
|
||||
|
||||
static u32 StmEhInBuf[8] ATTRIBUTE_ALIGN(32);
|
||||
static u32 StmEhOutBuf[8] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u32 StmEhInBuf[8];
|
||||
ATTRIBUTE_ALIGN(32) static u32 StmEhOutBuf[8];
|
||||
|
||||
static OSResetCallback ResetCallback;
|
||||
static OSPowerCallback PowerCallback;
|
||||
|
||||
@@ -873,4 +873,4 @@ void* OSGetThreadSpecific(s32 index) {
|
||||
|
||||
#include "global.h"
|
||||
extern u8 Debug_BBA_804516D0;
|
||||
u8 Debug_BBA_804516D0 ATTRIBUTE_ALIGN(8);
|
||||
ATTRIBUTE_ALIGN(8) u8 Debug_BBA_804516D0;
|
||||
|
||||
@@ -75,8 +75,8 @@ static const char ConfDirName[] = "/shared2/sys";
|
||||
static const char ConfFileName[] = "/shared2/sys/SYSCONF";
|
||||
static const char ProductInfoFileName[] = "/title/00000001/00000002/data/setting.txt";
|
||||
|
||||
static u8 ConfBuf[0x4000] ATTRIBUTE_ALIGN(32);
|
||||
static u8 ConfBufForFlush[0x4000] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 ConfBuf[0x4000];
|
||||
ATTRIBUTE_ALIGN(32) static u8 ConfBufForFlush[0x4000];
|
||||
|
||||
static u8 Initialized;
|
||||
static u8 DirtyFlag;
|
||||
|
||||
@@ -262,7 +262,7 @@ void WPADiManageHandler(OSAlarm*, OSContext*) {
|
||||
BTA_HhGetAclQueueInfo();
|
||||
}
|
||||
|
||||
u8 __WPADiManageHandlerStack[4096] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __WPADiManageHandlerStack[4096];
|
||||
|
||||
void WPADiManageHandler0(OSAlarm* alarm, OSContext* context) {
|
||||
OSSwitchFiberEx((u32)alarm, (u32)context, 0, 0, (u32)WPADiManageHandler, (u32)(__WPADiManageHandlerStack + sizeof(__WPADiManageHandlerStack)));
|
||||
|
||||
@@ -41,7 +41,7 @@ WUDControlBlock _wcb;
|
||||
WUDDevInfo _work;
|
||||
static WUDDiscResp _discResp;
|
||||
SCBtDeviceInfoArray _scArray;
|
||||
u8 __WUDHandlerStack[0x1000] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __WUDHandlerStack[0x1000];
|
||||
|
||||
extern u8 _scFlush;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"id": "dev.twilitrealm.shadow_mod",
|
||||
"name": "[Demo] Dynamic Shadows",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"author": "encounter",
|
||||
"description": "Demo showcasing dynamic shadow maps: re-renders geometry from the sun (or moon) point of view and composites real-time shadows over the world, with screen-space contact shadows for fine detail."
|
||||
}
|
||||
|
||||
+276
-38
@@ -20,7 +20,7 @@
|
||||
#include "dolphin/gx/GXPixel.h"
|
||||
#include "dolphin/gx/GXTransform.h"
|
||||
#include "m_Do/m_Do_mtx.h"
|
||||
#include "mods/hook.hpp"
|
||||
#include "mods/svc/hook.hpp"
|
||||
#include "mods/service.hpp"
|
||||
#include "mods/svc/camera.h"
|
||||
#include "mods/svc/config.h"
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "mods/svc/log.h"
|
||||
#include "mods/svc/resource.h"
|
||||
#include "mods/svc/ui.h"
|
||||
#include "mods/svc/window.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -45,6 +46,7 @@ IMPORT_SERVICE(GfxService, svc_gfx);
|
||||
IMPORT_SERVICE(CameraService, svc_camera);
|
||||
IMPORT_SERVICE(HookService, svc_hook);
|
||||
IMPORT_SERVICE(LogService, svc_log);
|
||||
IMPORT_SERVICE(WindowService, svc_window);
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -62,6 +64,7 @@ ConfigVarHandle g_cvarDebugView = 0;
|
||||
GfxDrawTypeHandle g_drawType = 0;
|
||||
GfxStageHookHandle g_sceneBeginHook = 0;
|
||||
GfxStageHookHandle g_sceneAfterTerrainHook = 0;
|
||||
GfxStageHookHandle g_sceneAfterOpaqueHook = 0;
|
||||
GfxStageHookHandle g_frameBeforeHudHook = 0;
|
||||
UiWindowHandle g_controlsWindow = 0;
|
||||
ResourceBuffer g_shaderSource = RESOURCE_BUFFER_INIT;
|
||||
@@ -70,6 +73,11 @@ WGPURenderPipeline g_compositePipeline = nullptr; // multiply blend
|
||||
WGPURenderPipeline g_compositeDebugPipeline = nullptr; // no blend (debug views)
|
||||
WGPUBindGroupLayout g_compositeLayout = nullptr;
|
||||
WGPUBindGroupLayout g_compositeDebugLayout = nullptr;
|
||||
WGPURenderPipeline g_debugPresentPipeline = nullptr;
|
||||
WGPUBindGroupLayout g_debugPresentLayout = nullptr;
|
||||
WGPUTextureFormat g_debugPresentFormat = WGPUTextureFormat_Undefined;
|
||||
WindowHandle g_debugWindow = 0;
|
||||
GfxPresentTargetHandle g_debugPresentTarget = 0;
|
||||
|
||||
struct MapPassOutput {
|
||||
bool ready = false;
|
||||
@@ -187,6 +195,34 @@ int64_t get_debug_mode() {
|
||||
return std::clamp<int64_t>(get_int_option(g_cvarDebugView, 0), 0, 10);
|
||||
}
|
||||
|
||||
bool debug_window_open() {
|
||||
return g_debugWindow != 0 && g_debugPresentTarget != 0;
|
||||
}
|
||||
|
||||
ModResult close_debug_window() {
|
||||
if (g_debugPresentTarget != 0) {
|
||||
const auto result = svc_gfx->unregister_present_target(mod_ctx, g_debugPresentTarget);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
g_debugPresentTarget = 0;
|
||||
}
|
||||
if (g_debugWindow != 0) {
|
||||
const auto result = svc_window->destroy_window(mod_ctx, g_debugWindow);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
g_debugWindow = 0;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void on_debug_window_event(ModContext*, WindowHandle, const WindowEvent* event, void*) {
|
||||
if (event->type == WINDOW_EVENT_CLOSE_REQUESTED && close_debug_window() != MOD_OK) {
|
||||
svc_log->error(mod_ctx, "failed to close shadow debug window");
|
||||
}
|
||||
}
|
||||
|
||||
bool matrix_ready(const Mtx m) {
|
||||
float basis = 0.0f;
|
||||
for (int r = 0; r < 3; ++r) {
|
||||
@@ -395,6 +431,90 @@ bool build_composite_pipeline(
|
||||
return outLayout != nullptr;
|
||||
}
|
||||
|
||||
void release_debug_present_pipeline() {
|
||||
if (g_debugPresentPipeline != nullptr) {
|
||||
wgpuRenderPipelineRelease(g_debugPresentPipeline);
|
||||
g_debugPresentPipeline = nullptr;
|
||||
}
|
||||
if (g_debugPresentLayout != nullptr) {
|
||||
wgpuBindGroupLayoutRelease(g_debugPresentLayout);
|
||||
g_debugPresentLayout = nullptr;
|
||||
}
|
||||
g_debugPresentFormat = WGPUTextureFormat_Undefined;
|
||||
}
|
||||
|
||||
bool ensure_debug_present_pipeline(const GfxPresentContext& ctx) {
|
||||
if (g_debugPresentPipeline != nullptr && g_debugPresentFormat == ctx.target_format) {
|
||||
return true;
|
||||
}
|
||||
release_debug_present_pipeline();
|
||||
|
||||
WGPUShaderSourceWGSL wgsl = WGPU_SHADER_SOURCE_WGSL_INIT;
|
||||
wgsl.code = {static_cast<const char*>(g_shaderSource.data), g_shaderSource.size};
|
||||
WGPUShaderModuleDescriptor moduleDesc = WGPU_SHADER_MODULE_DESCRIPTOR_INIT;
|
||||
moduleDesc.nextInChain = &wgsl.chain;
|
||||
moduleDesc.label = {"shadow debug present", WGPU_STRLEN};
|
||||
WGPUShaderModule module = wgpuDeviceCreateShaderModule(ctx.device, &moduleDesc);
|
||||
if (module == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
WGPUColorTargetState colorTarget = WGPU_COLOR_TARGET_STATE_INIT;
|
||||
colorTarget.format = ctx.target_format;
|
||||
WGPUFragmentState fragment = WGPU_FRAGMENT_STATE_INIT;
|
||||
fragment.module = module;
|
||||
fragment.entryPoint = {"fs_main", WGPU_STRLEN};
|
||||
fragment.targetCount = 1;
|
||||
fragment.targets = &colorTarget;
|
||||
|
||||
WGPURenderPipelineDescriptor pipelineDesc = WGPU_RENDER_PIPELINE_DESCRIPTOR_INIT;
|
||||
pipelineDesc.label = {"shadow debug present", WGPU_STRLEN};
|
||||
pipelineDesc.vertex.module = module;
|
||||
pipelineDesc.vertex.entryPoint = {"vs_main", WGPU_STRLEN};
|
||||
pipelineDesc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
|
||||
pipelineDesc.multisample.count = 1;
|
||||
pipelineDesc.fragment = &fragment;
|
||||
g_debugPresentPipeline = wgpuDeviceCreateRenderPipeline(ctx.device, &pipelineDesc);
|
||||
wgpuShaderModuleRelease(module);
|
||||
if (g_debugPresentPipeline == nullptr) {
|
||||
return false;
|
||||
}
|
||||
g_debugPresentLayout = wgpuRenderPipelineGetBindGroupLayout(g_debugPresentPipeline, 0);
|
||||
if (g_debugPresentLayout == nullptr) {
|
||||
release_debug_present_pipeline();
|
||||
return false;
|
||||
}
|
||||
g_debugPresentFormat = ctx.target_format;
|
||||
return true;
|
||||
}
|
||||
|
||||
WGPUBindGroup create_composite_bind_group(WGPUDevice device, WGPUBindGroupLayout layout,
|
||||
WGPUBuffer uniformBuffer, const DrawPayload& data) {
|
||||
if (data.sceneDepth == nullptr || data.shadowMap == nullptr || data.lightColor == nullptr ||
|
||||
layout == nullptr || uniformBuffer == nullptr)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
WGPUBindGroupEntry entries[4] = {WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT,
|
||||
WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT};
|
||||
entries[0].binding = 0;
|
||||
entries[0].textureView = data.sceneDepth;
|
||||
entries[1].binding = 1;
|
||||
entries[1].textureView = data.shadowMap;
|
||||
entries[2].binding = 2;
|
||||
entries[2].buffer = uniformBuffer;
|
||||
entries[2].offset = data.uniform_offset;
|
||||
entries[2].size = data.uniform_size;
|
||||
entries[3].binding = 3;
|
||||
entries[3].textureView = data.lightColor;
|
||||
WGPUBindGroupDescriptor bindGroupDesc = WGPU_BIND_GROUP_DESCRIPTOR_INIT;
|
||||
bindGroupDesc.layout = layout;
|
||||
bindGroupDesc.entryCount = 4;
|
||||
bindGroupDesc.entries = entries;
|
||||
return wgpuDeviceCreateBindGroup(device, &bindGroupDesc);
|
||||
}
|
||||
|
||||
// Render worker thread: fullscreen deferred-shadow composite.
|
||||
void on_draw(
|
||||
ModContext*, const GfxDrawContext* ctx, const void* payload, size_t payloadSize, void*) {
|
||||
@@ -407,29 +527,12 @@ void on_draw(
|
||||
WGPURenderPipeline pipeline =
|
||||
data.debug_mode != 0 ? g_compositeDebugPipeline : g_compositePipeline;
|
||||
WGPUBindGroupLayout layout = data.debug_mode != 0 ? g_compositeDebugLayout : g_compositeLayout;
|
||||
if (data.sceneDepth == nullptr || data.shadowMap == nullptr || data.lightColor == nullptr ||
|
||||
pipeline == nullptr)
|
||||
{
|
||||
if (pipeline == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
WGPUBindGroupEntry entries[4] = {WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT,
|
||||
WGPU_BIND_GROUP_ENTRY_INIT, WGPU_BIND_GROUP_ENTRY_INIT};
|
||||
entries[0].binding = 0;
|
||||
entries[0].textureView = data.sceneDepth;
|
||||
entries[1].binding = 1;
|
||||
entries[1].textureView = data.shadowMap;
|
||||
entries[2].binding = 2;
|
||||
entries[2].buffer = ctx->uniform_buffer;
|
||||
entries[2].offset = data.uniform_offset;
|
||||
entries[2].size = data.uniform_size;
|
||||
entries[3].binding = 3;
|
||||
entries[3].textureView = data.lightColor;
|
||||
WGPUBindGroupDescriptor bindGroupDesc = WGPU_BIND_GROUP_DESCRIPTOR_INIT;
|
||||
bindGroupDesc.layout = layout;
|
||||
bindGroupDesc.entryCount = 4;
|
||||
bindGroupDesc.entries = entries;
|
||||
WGPUBindGroup bindGroup = wgpuDeviceCreateBindGroup(ctx->device, &bindGroupDesc);
|
||||
WGPUBindGroup bindGroup =
|
||||
create_composite_bind_group(ctx->device, layout, ctx->uniform_buffer, data);
|
||||
if (bindGroup == nullptr) {
|
||||
return;
|
||||
}
|
||||
@@ -440,6 +543,81 @@ void on_draw(
|
||||
wgpuBindGroupRelease(bindGroup);
|
||||
}
|
||||
|
||||
// Render worker thread: draw the selected diagnostic into the auxiliary surface.
|
||||
void on_debug_present(
|
||||
ModContext*, const GfxPresentContext* ctx, const void* payload, size_t payloadSize, void*) {
|
||||
WGPUBindGroup bindGroup = nullptr;
|
||||
if (payloadSize == sizeof(DrawPayload)) {
|
||||
DrawPayload data;
|
||||
std::memcpy(&data, payload, sizeof(data));
|
||||
if (ensure_debug_present_pipeline(*ctx)) {
|
||||
bindGroup = create_composite_bind_group(
|
||||
ctx->device, g_debugPresentLayout, ctx->uniform_buffer, data);
|
||||
}
|
||||
}
|
||||
|
||||
WGPURenderPassColorAttachment colorAttachment = WGPU_RENDER_PASS_COLOR_ATTACHMENT_INIT;
|
||||
colorAttachment.view = ctx->target_view;
|
||||
colorAttachment.loadOp = WGPULoadOp_Clear;
|
||||
colorAttachment.storeOp = WGPUStoreOp_Store;
|
||||
colorAttachment.clearValue = WGPUColor{0.0, 0.0, 0.0, 1.0};
|
||||
WGPURenderPassDescriptor passDesc = WGPU_RENDER_PASS_DESCRIPTOR_INIT;
|
||||
passDesc.label = {"shadow debug present", WGPU_STRLEN};
|
||||
passDesc.colorAttachmentCount = 1;
|
||||
passDesc.colorAttachments = &colorAttachment;
|
||||
WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(ctx->encoder, &passDesc);
|
||||
|
||||
if (bindGroup != nullptr) {
|
||||
wgpuRenderPassEncoderSetPipeline(pass, g_debugPresentPipeline);
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, bindGroup, 0, nullptr);
|
||||
wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0);
|
||||
wgpuBindGroupRelease(bindGroup);
|
||||
}
|
||||
|
||||
wgpuRenderPassEncoderEnd(pass);
|
||||
wgpuRenderPassEncoderRelease(pass);
|
||||
}
|
||||
|
||||
ModResult open_debug_window() {
|
||||
if (g_debugWindow != 0) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
|
||||
WindowDesc windowDesc = WINDOW_DESC_INIT;
|
||||
windowDesc.title = "Shadow Debug View";
|
||||
windowDesc.width = 720;
|
||||
windowDesc.height = 480;
|
||||
windowDesc.on_event = on_debug_window_event;
|
||||
auto result = svc_window->create_window(mod_ctx, &windowDesc, &g_debugWindow);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
GfxPresentTargetDesc presentDesc = GFX_PRESENT_TARGET_DESC_INIT;
|
||||
presentDesc.label = "Shadow debug surface";
|
||||
presentDesc.render = on_debug_present;
|
||||
result = svc_gfx->register_window_present_target(
|
||||
mod_ctx, g_debugWindow, &presentDesc, &g_debugPresentTarget);
|
||||
if (result != MOD_OK) {
|
||||
close_debug_window();
|
||||
return result;
|
||||
}
|
||||
|
||||
result = svc_window->show_window(mod_ctx, g_debugWindow);
|
||||
if (result != MOD_OK) {
|
||||
close_debug_window();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void on_toggle_debug_window(ModContext*, void*) {
|
||||
const auto result = debug_window_open() ? close_debug_window() : open_debug_window();
|
||||
if (result != MOD_OK) {
|
||||
svc_log->error(mod_ctx, debug_window_open() ? "failed to close shadow debug window" :
|
||||
"failed to open shadow debug window");
|
||||
}
|
||||
}
|
||||
|
||||
// Picks the sun or moon (whichever is above the horizon) and returns the normalized
|
||||
// world-space direction *toward* the light plus a horizon fade factor. False = no light.
|
||||
bool compute_light(float outDirToLight[3], float& outFade) {
|
||||
@@ -595,7 +773,7 @@ void restore_actual_light_debug() {
|
||||
void on_scene_begin(ModContext*, const GfxStageContext* stageCtx, void*) {
|
||||
restore_actual_light_debug();
|
||||
capture_scene_camera(stageCtx);
|
||||
if (!get_bool_option(g_cvarEnabled, true) || get_debug_mode() != 9) {
|
||||
if (!get_bool_option(g_cvarEnabled, true) || get_debug_mode() != 9 || debug_window_open()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -653,7 +831,7 @@ void render_shadow_map(
|
||||
return;
|
||||
}
|
||||
const int64_t debugMode = get_debug_mode();
|
||||
if (debugMode == 9) {
|
||||
if (debugMode == 9 && !debug_window_open()) {
|
||||
return;
|
||||
}
|
||||
if (!matrix_ready(replayView)) {
|
||||
@@ -749,19 +927,30 @@ void render_shadow_map(
|
||||
g_mapPass.ready = true;
|
||||
}
|
||||
|
||||
// Game thread, after the full 3D scene: deferred composite.
|
||||
void on_frame_before_hud(ModContext*, const GfxStageContext*, void*) {
|
||||
// Game thread, after opaque scene draws and before translucent/fog overlays: deferred composite.
|
||||
void on_scene_after_opaque(ModContext*, const GfxStageContext*, void*) {
|
||||
const int64_t debugMode = get_debug_mode();
|
||||
const bool presentDebug = debug_window_open();
|
||||
restore_actual_light_debug();
|
||||
|
||||
if (presentDebug && debugMode == 0) {
|
||||
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, nullptr, 0);
|
||||
}
|
||||
|
||||
const MapPassOutput mapPass = std::exchange(g_mapPass, {});
|
||||
if (debugMode == 9) {
|
||||
if (debugMode == 9 && !debug_window_open()) {
|
||||
return;
|
||||
}
|
||||
if (!mapPass.ready || mapPass.shadowMap == nullptr || mapPass.lightColor == nullptr) {
|
||||
if (presentDebug && debugMode != 0) {
|
||||
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, nullptr, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!g_sceneCamera.valid) {
|
||||
if (presentDebug && debugMode != 0) {
|
||||
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, nullptr, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const CameraInfo& camera = g_sceneCamera.info;
|
||||
@@ -773,6 +962,9 @@ void on_frame_before_hud(ModContext*, const GfxStageContext*, void*) {
|
||||
if (svc_gfx->resolve_pass(mod_ctx, &resolveDesc, &resolved) != MOD_OK ||
|
||||
resolved.depth == nullptr)
|
||||
{
|
||||
if (presentDebug && debugMode != 0) {
|
||||
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, nullptr, 0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -797,7 +989,7 @@ void on_frame_before_hud(ModContext*, const GfxStageContext*, void*) {
|
||||
uniforms.inv_size[0] = 1.0f / uniforms.size[0];
|
||||
uniforms.inv_size[1] = 1.0f / uniforms.size[1];
|
||||
uniforms.edge_fade_width =
|
||||
static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarEdgeFadeWidth, 32), 0, 128));
|
||||
static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarEdgeFadeWidth, 32), 0, 256));
|
||||
uniforms.strength =
|
||||
mapPass.fade *
|
||||
static_cast<float>(std::clamp<int64_t>(get_int_option(g_cvarStrength, 45), 0, 100)) /
|
||||
@@ -806,15 +998,36 @@ void on_frame_before_hud(ModContext*, const GfxStageContext*, void*) {
|
||||
uniforms.contact_enabled = get_bool_option(g_cvarContactShadows, false) ? 1.0f : 0.0f;
|
||||
uniforms.contact_thickness = 25.0f;
|
||||
uniforms.contact_length = 60.0f;
|
||||
uniforms.debug_mode = static_cast<uint32_t>(debugMode);
|
||||
// Camera Replay intentionally uses the gameplay-camera offscreen pass instead of the light
|
||||
// shadow map, so it remains diagnostic on both windows. Other external diagnostics leave the
|
||||
// main window on the normal shadow composite.
|
||||
uniforms.debug_mode = presentDebug && debugMode != 10 ? 0u : static_cast<uint32_t>(debugMode);
|
||||
|
||||
GfxRange uniformRange{0, 0};
|
||||
if (svc_gfx->push_uniform(mod_ctx, &uniforms, sizeof(uniforms), &uniformRange) != MOD_OK) {
|
||||
return;
|
||||
}
|
||||
const DrawPayload payload{resolved.depth, mapPass.shadowMap, mapPass.lightColor,
|
||||
uniformRange.offset, uniformRange.size, static_cast<uint32_t>(debugMode)};
|
||||
uniformRange.offset, uniformRange.size, uniforms.debug_mode};
|
||||
svc_gfx->push_draw(mod_ctx, g_drawType, &payload, sizeof(payload));
|
||||
|
||||
if (presentDebug && debugMode != 0) {
|
||||
uniforms.debug_mode = static_cast<uint32_t>(debugMode);
|
||||
GfxRange debugUniformRange{0, 0};
|
||||
if (svc_gfx->push_uniform(mod_ctx, &uniforms, sizeof(uniforms), &debugUniformRange) !=
|
||||
MOD_OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const DrawPayload debugPayload{resolved.depth, mapPass.shadowMap, mapPass.lightColor,
|
||||
debugUniformRange.offset, debugUniformRange.size, uniforms.debug_mode};
|
||||
svc_gfx->push_present(mod_ctx, g_debugPresentTarget, &debugPayload, sizeof(debugPayload));
|
||||
}
|
||||
}
|
||||
|
||||
// Frame tail hook: only needed to restore light-view debug camera state before HUD.
|
||||
void on_frame_before_hud(ModContext*, const GfxStageContext*, void*) {
|
||||
restore_actual_light_debug();
|
||||
}
|
||||
|
||||
void add_control(UiElementHandle pane, const UiControlDesc& desc) {
|
||||
@@ -873,7 +1086,7 @@ ModResult build_controls_tab(
|
||||
"dynamic shadows. This can be expensive.");
|
||||
add_number(left, "Coverage", g_cvarBoxRadius, 1000, 20000, 500, nullptr,
|
||||
"Radius of the shadowed area around the camera, in world units. Smaller is sharper.");
|
||||
add_number(left, "Fade Out", g_cvarEdgeFadeWidth, 0, 128, 32, " texels",
|
||||
add_number(left, "Fade Out", g_cvarEdgeFadeWidth, 0, 256, 32, " texels",
|
||||
"Fade out shadows gradually near the edge of the coverage area.");
|
||||
|
||||
svc_ui->pane_add_section(mod_ctx, left, "Appearance");
|
||||
@@ -899,6 +1112,14 @@ ModResult build_controls_tab(
|
||||
"Bounds: valid X in red, valid Y in green, and valid depth in blue<br/>Light View: "
|
||||
"renders the game world directly from the light camera<br/>Camera Replay: "
|
||||
"captures the same draw-list replay from the gameplay camera");
|
||||
UiControlDesc debugWindowControl = UI_CONTROL_DESC_INIT;
|
||||
debugWindowControl.kind = UI_CONTROL_BUTTON;
|
||||
debugWindowControl.label = "Open / Close Debug Window";
|
||||
debugWindowControl.help_rml =
|
||||
"Shows the selected debug view in an auxiliary WebGPU window. Standard diagnostics leave "
|
||||
"the main view on the normal shadow composite.";
|
||||
debugWindowControl.on_pressed = on_toggle_debug_window;
|
||||
add_control(left, debugWindowControl);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
@@ -935,6 +1156,12 @@ ModResult build_panel(ModContext*, UiElementHandle panel, void*, ModError*) {
|
||||
control.label = "Open Controls";
|
||||
control.on_pressed = on_open_controls;
|
||||
add_control(panel, control);
|
||||
|
||||
control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_BUTTON;
|
||||
control.label = "Open / Close Debug Window";
|
||||
control.on_pressed = on_toggle_debug_window;
|
||||
add_control(panel, control);
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
@@ -1000,7 +1227,7 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
result = register_int_option("edgeFadeWidth", 32, g_cvarEdgeFadeWidth, error);
|
||||
result = register_int_option("edgeFadeWidth", 128, g_cvarEdgeFadeWidth, error);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
@@ -1041,6 +1268,12 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||
{
|
||||
return mods::set_error(error, MOD_ERROR, "failed to register stage hook");
|
||||
}
|
||||
stageDesc.callback = on_scene_after_opaque;
|
||||
if (svc_gfx->register_stage_hook(
|
||||
mod_ctx, GFX_STAGE_SCENE_AFTER_OPAQUE, &stageDesc, &g_sceneAfterOpaqueHook) != MOD_OK)
|
||||
{
|
||||
return mods::set_error(error, MOD_ERROR, "failed to register stage hook");
|
||||
}
|
||||
stageDesc.callback = on_frame_before_hud;
|
||||
if (svc_gfx->register_stage_hook(
|
||||
mod_ctx, GFX_STAGE_FRAME_BEFORE_HUD, &stageDesc, &g_frameBeforeHudHook) != MOD_OK)
|
||||
@@ -1051,18 +1284,18 @@ MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||
// Skip the game's own shadow rendering while the dynamic pass is active: the
|
||||
// shadowControl pair covers the actor real/blob shadows, drawCloudShadow the weather
|
||||
// cloud shadows.
|
||||
if (mods::hook_add_pre<GameShadowImageDraw>(svc_hook, on_game_shadow_pre) != MOD_OK ||
|
||||
mods::hook_add_pre<GameShadowDraw>(svc_hook, on_game_shadow_pre) != MOD_OK ||
|
||||
mods::hook_add_pre<CloudShadowDraw>(svc_hook, on_game_shadow_pre) != MOD_OK)
|
||||
if (mods::hook::add_pre<GameShadowImageDraw>(on_game_shadow_pre) != MOD_OK ||
|
||||
mods::hook::add_pre<GameShadowDraw>(on_game_shadow_pre) != MOD_OK ||
|
||||
mods::hook::add_pre<CloudShadowDraw>(on_game_shadow_pre) != MOD_OK)
|
||||
{
|
||||
return mods::set_error(error, MOD_ERROR, "failed to hook game shadow rendering");
|
||||
}
|
||||
if (mods::hook_add_pre<ClipperSphereClip>(svc_hook, on_frustum_clip_pre) != MOD_OK ||
|
||||
mods::hook_add_pre<ClipperBoxClip>(svc_hook, on_frustum_clip_pre) != MOD_OK)
|
||||
if (mods::hook::add_pre<ClipperSphereClip>(on_frustum_clip_pre) != MOD_OK ||
|
||||
mods::hook::add_pre<ClipperBoxClip>(on_frustum_clip_pre) != MOD_OK)
|
||||
{
|
||||
return mods::set_error(error, MOD_ERROR, "failed to hook frustum clipping");
|
||||
}
|
||||
if (mods::hook_add_pre<CopyTex>(svc_hook, on_copy_tex_pre) != MOD_OK) {
|
||||
if (mods::hook::add_pre<CopyTex>(on_copy_tex_pre) != MOD_OK) {
|
||||
return mods::set_error(error, MOD_ERROR, "failed to hook GXCopyTex");
|
||||
}
|
||||
UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT;
|
||||
@@ -1078,6 +1311,8 @@ MOD_EXPORT ModResult mod_update(ModError*) {
|
||||
|
||||
MOD_EXPORT ModResult mod_shutdown(ModError*) {
|
||||
restore_actual_light_debug();
|
||||
close_debug_window();
|
||||
release_debug_present_pipeline();
|
||||
svc_resource->free(mod_ctx, &g_shaderSource);
|
||||
if (g_compositePipeline != nullptr) {
|
||||
wgpuRenderPipelineRelease(g_compositePipeline);
|
||||
@@ -1099,8 +1334,11 @@ MOD_EXPORT ModResult mod_shutdown(ModError*) {
|
||||
g_cvarStrength = 0;
|
||||
g_cvarPcf = g_cvarBias = g_cvarBoxRadius = g_cvarEdgeFadeWidth = g_cvarContactShadows =
|
||||
g_cvarDebugView = 0;
|
||||
g_drawType = g_sceneBeginHook = g_sceneAfterTerrainHook = g_frameBeforeHudHook = 0;
|
||||
g_drawType = g_sceneBeginHook = g_sceneAfterTerrainHook = g_sceneAfterOpaqueHook =
|
||||
g_frameBeforeHudHook = 0;
|
||||
g_controlsWindow = 0;
|
||||
g_debugWindow = 0;
|
||||
g_debugPresentTarget = 0;
|
||||
g_mapPass = {};
|
||||
g_sceneCamera.valid = false;
|
||||
g_sceneCamera.raw_valid = false;
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
project(window_demo CXX)
|
||||
|
||||
if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR)
|
||||
set(DUSK_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../.." CACHE PATH "Path to dusk source root")
|
||||
option(DUSK_MOD_USE_FULL_TREE "Use full build instead of the minimal mod SDK" OFF)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
if (DUSK_MOD_USE_FULL_TREE)
|
||||
add_subdirectory("${DUSK_DIR}" dusk EXCLUDE_FROM_ALL)
|
||||
else ()
|
||||
add_subdirectory("${DUSK_DIR}/sdk" dusk-sdk EXCLUDE_FROM_ALL)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
add_mod(window_demo
|
||||
FEATURES fmt webgpu
|
||||
SOURCES src/logging.cpp src/mod.cpp
|
||||
MOD_JSON mod.json
|
||||
BUNDLE
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "dev.twilitrealm.window_demo",
|
||||
"name": "[Demo] Extra Window",
|
||||
"version": "1.0.0",
|
||||
"author": "Twilit Realm",
|
||||
"description": "Demonstrates creating an extra window through WindowService and rendering to it with GfxService."
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#include "logging.hpp"
|
||||
|
||||
#include "mods/svc/log.hpp"
|
||||
#include "mods/svc/window.h"
|
||||
|
||||
namespace {
|
||||
|
||||
const char* window_event_name(WindowEventType type) {
|
||||
switch (type) {
|
||||
case WINDOW_EVENT_CLOSE_REQUESTED:
|
||||
return "close requested";
|
||||
case WINDOW_EVENT_RESIZED:
|
||||
return "resized";
|
||||
case WINDOW_EVENT_MOVED:
|
||||
return "moved";
|
||||
case WINDOW_EVENT_FOCUS_GAINED:
|
||||
return "focus gained";
|
||||
case WINDOW_EVENT_FOCUS_LOST:
|
||||
return "focus lost";
|
||||
case WINDOW_EVENT_SHOWN:
|
||||
return "shown";
|
||||
case WINDOW_EVENT_HIDDEN:
|
||||
return "hidden";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void window_demo::log_window_event(const WindowEvent* event) {
|
||||
mods::log::info(
|
||||
"window event: {}; position=({}, {}), size={}x{}, pixels={}x{}, scale={:.2f}",
|
||||
window_event_name(event->type), event->x, event->y, event->width, event->height,
|
||||
event->pixel_width, event->pixel_height, event->display_scale);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
struct WindowEvent;
|
||||
|
||||
namespace window_demo {
|
||||
|
||||
void log_window_event(const WindowEvent* event);
|
||||
|
||||
} // namespace window_demo
|
||||
@@ -0,0 +1,229 @@
|
||||
#include "logging.hpp"
|
||||
|
||||
#include "mods/service.hpp"
|
||||
#include "mods/svc/gfx.h"
|
||||
#include "mods/svc/log.hpp"
|
||||
#include "mods/svc/ui.h"
|
||||
#include "mods/svc/window.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <webgpu/webgpu.h>
|
||||
|
||||
DEFINE_MOD();
|
||||
IMPORT_SERVICE(LogService, svc_log);
|
||||
IMPORT_SERVICE(UiService, svc_ui);
|
||||
IMPORT_SERVICE(WindowService, svc_window);
|
||||
IMPORT_SERVICE(GfxService, svc_gfx);
|
||||
|
||||
namespace {
|
||||
|
||||
WindowHandle g_window = 0;
|
||||
GfxPresentTargetHandle g_presentTarget = 0;
|
||||
GfxStageHookHandle g_stageHook = 0;
|
||||
uint32_t g_frame = 0;
|
||||
bool g_recreatePresentTarget = false;
|
||||
|
||||
struct ClearPayload {
|
||||
float red;
|
||||
float green;
|
||||
float blue;
|
||||
float alpha;
|
||||
};
|
||||
static_assert(sizeof(ClearPayload) <= GFX_INLINE_DRAW_PAYLOAD_SIZE);
|
||||
|
||||
ModResult close_window() {
|
||||
g_recreatePresentTarget = false;
|
||||
if (g_presentTarget != 0) {
|
||||
const auto result = svc_gfx->unregister_present_target(mod_ctx, g_presentTarget);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
g_presentTarget = 0;
|
||||
}
|
||||
if (g_window != 0) {
|
||||
const auto result = svc_window->destroy_window(mod_ctx, g_window);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
g_window = 0;
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void on_window_event(ModContext*, WindowHandle, const WindowEvent* event, void*) {
|
||||
window_demo::log_window_event(event);
|
||||
|
||||
if (event->type == WINDOW_EVENT_CLOSE_REQUESTED) {
|
||||
if (close_window() != MOD_OK) {
|
||||
mods::log::error("failed to close auxiliary window");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render worker thread: record a clear of the acquired auxiliary surface texture.
|
||||
void on_present(
|
||||
ModContext*, const GfxPresentContext* ctx, const void* payload, size_t payloadSize, void*) {
|
||||
if (payloadSize != sizeof(ClearPayload)) {
|
||||
return;
|
||||
}
|
||||
ClearPayload color;
|
||||
std::memcpy(&color, payload, sizeof(color));
|
||||
|
||||
WGPURenderPassColorAttachment colorAttachment = WGPU_RENDER_PASS_COLOR_ATTACHMENT_INIT;
|
||||
colorAttachment.view = ctx->target_view;
|
||||
colorAttachment.loadOp = WGPULoadOp_Clear;
|
||||
colorAttachment.storeOp = WGPUStoreOp_Store;
|
||||
colorAttachment.clearValue = WGPUColor{
|
||||
color.red,
|
||||
color.green,
|
||||
color.blue,
|
||||
color.alpha,
|
||||
};
|
||||
|
||||
WGPURenderPassDescriptor passDesc = WGPU_RENDER_PASS_DESCRIPTOR_INIT;
|
||||
passDesc.label = {"Auxiliary window clear", WGPU_STRLEN};
|
||||
passDesc.colorAttachmentCount = 1;
|
||||
passDesc.colorAttachments = &colorAttachment;
|
||||
WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(ctx->encoder, &passDesc);
|
||||
wgpuRenderPassEncoderEnd(pass);
|
||||
wgpuRenderPassEncoderRelease(pass);
|
||||
}
|
||||
|
||||
ModResult register_present_target() {
|
||||
if (g_window == 0 || g_presentTarget != 0) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
GfxPresentTargetDesc presentDesc = GFX_PRESENT_TARGET_DESC_INIT;
|
||||
presentDesc.label = "Auxiliary window surface";
|
||||
presentDesc.render = on_present;
|
||||
presentDesc.preferred_alpha_mode =
|
||||
WGPUCompositeAlphaMode_Premultiplied; // For transparent window
|
||||
return svc_gfx->register_window_present_target(
|
||||
mod_ctx, g_window, &presentDesc, &g_presentTarget);
|
||||
}
|
||||
|
||||
ModResult open_window() {
|
||||
if (g_window != 0) {
|
||||
return MOD_CONFLICT;
|
||||
}
|
||||
|
||||
WindowDesc windowDesc = WINDOW_DESC_INIT;
|
||||
windowDesc.title = "Mod window";
|
||||
windowDesc.width = 640;
|
||||
windowDesc.height = 480;
|
||||
windowDesc.on_event = on_window_event;
|
||||
windowDesc.flags |= WINDOW_FLAG_TRANSPARENT; // For transparent window
|
||||
auto result = svc_window->create_window(mod_ctx, &windowDesc, &g_window);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = register_present_target();
|
||||
if (result != MOD_OK) {
|
||||
close_window();
|
||||
return result;
|
||||
}
|
||||
|
||||
result = svc_window->show_window(mod_ctx, g_window);
|
||||
if (result != MOD_OK) {
|
||||
close_window();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void on_frame_after_hud(ModContext*, const GfxStageContext*, void*) {
|
||||
if (g_presentTarget == 0) {
|
||||
return;
|
||||
}
|
||||
const float phase = static_cast<float>(g_frame++) * 0.015f;
|
||||
const ClearPayload color{
|
||||
.red = 0.08f + 0.06f * (std::sin(phase) + 1.0f),
|
||||
.green = 0.10f + 0.06f * (std::sin(phase + 2.1f) + 1.0f),
|
||||
.blue = 0.14f + 0.08f * (std::sin(phase + 4.2f) + 1.0f),
|
||||
.alpha = 0.5f,
|
||||
};
|
||||
if (svc_gfx->push_present(mod_ctx, g_presentTarget, &color, sizeof(color)) == MOD_ERROR) {
|
||||
g_recreatePresentTarget = true;
|
||||
}
|
||||
}
|
||||
|
||||
void on_toggle_window(ModContext*, void*) {
|
||||
if (g_window != 0) {
|
||||
if (close_window() != MOD_OK) {
|
||||
mods::log::error("failed to close auxiliary window");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (open_window() != MOD_OK) {
|
||||
mods::log::error("failed to open auxiliary window");
|
||||
}
|
||||
}
|
||||
|
||||
ModResult build_panel(ModContext*, UiElementHandle panel, void*, ModError*) {
|
||||
UiControlDesc control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_BUTTON;
|
||||
control.label = "Open / Close Window";
|
||||
control.on_pressed = on_toggle_window;
|
||||
return svc_ui->pane_add_control(mod_ctx, panel, &control, nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C" {
|
||||
|
||||
MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||
GfxStageHookDesc stageDesc = GFX_STAGE_HOOK_DESC_INIT;
|
||||
stageDesc.callback = on_frame_after_hud;
|
||||
if (svc_gfx->register_stage_hook(
|
||||
mod_ctx, GFX_STAGE_FRAME_AFTER_HUD, &stageDesc, &g_stageHook) != MOD_OK)
|
||||
{
|
||||
return mods::set_error(error, MOD_ERROR, "failed to register presentation hook");
|
||||
}
|
||||
|
||||
UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT;
|
||||
panelDesc.build = build_panel;
|
||||
if (svc_ui->register_mods_panel(mod_ctx, &panelDesc) != MOD_OK) {
|
||||
svc_gfx->unregister_stage_hook(mod_ctx, g_stageHook);
|
||||
g_stageHook = 0;
|
||||
return mods::set_error(error, MOD_ERROR, "failed to register mod panel");
|
||||
}
|
||||
|
||||
if (open_window() != MOD_OK) {
|
||||
svc_gfx->unregister_stage_hook(mod_ctx, g_stageHook);
|
||||
g_stageHook = 0;
|
||||
return mods::set_error(error, MOD_ERROR, "failed to open auxiliary window");
|
||||
}
|
||||
|
||||
mods::log::info("auxiliary WebGPU window ready");
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_update(ModError* error) {
|
||||
if (!g_recreatePresentTarget || g_window == 0) {
|
||||
return MOD_OK;
|
||||
}
|
||||
g_recreatePresentTarget = false;
|
||||
if (g_presentTarget != 0) {
|
||||
const auto result = svc_gfx->unregister_present_target(mod_ctx, g_presentTarget);
|
||||
if (result != MOD_OK) {
|
||||
return mods::set_error(error, result, "failed to unregister lost present target");
|
||||
}
|
||||
g_presentTarget = 0;
|
||||
}
|
||||
const auto result = register_present_target();
|
||||
if (result != MOD_OK) {
|
||||
return mods::set_error(error, result, "failed to recreate present target");
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_shutdown(ModError*) {
|
||||
if (g_stageHook != 0) {
|
||||
svc_gfx->unregister_stage_hook(mod_ctx, g_stageHook);
|
||||
g_stageHook = 0;
|
||||
}
|
||||
close_window();
|
||||
return MOD_OK;
|
||||
}
|
||||
}
|
||||
+13
-32
@@ -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
|
||||
|
||||
@@ -21,45 +21,23 @@ export JAVA_HOME="/usr/lib/jvm/java-17-openjdk"
|
||||
```bash
|
||||
cmake --preset android-arm64
|
||||
cmake --build --preset android-arm64
|
||||
|
||||
cmake --preset android-x86_64
|
||||
cmake --build --preset android-x86_64
|
||||
```
|
||||
|
||||
These builds produce:
|
||||
|
||||
- `build/android-arm64/Binaries/libmain.so`
|
||||
- `build/android-x86_64/Binaries/libmain.so`
|
||||
|
||||
## Stage Libraries Into APK Project
|
||||
|
||||
```bash
|
||||
./android/scripts/stage-jni-libs.sh
|
||||
```
|
||||
|
||||
This copies:
|
||||
|
||||
- `libmain.so` -> `android/app/src/main/jniLibs/arm64-v8a/`
|
||||
- `libmain.so` -> `android/app/src/main/jniLibs/x86_64/`
|
||||
|
||||
## 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
|
||||
```
|
||||
This build produces `build/android-arm64/libmain.so`
|
||||
|
||||
## 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)
|
||||
|
||||
@@ -67,10 +45,13 @@ You can pass command-line args through the activity intent:
|
||||
|
||||
```bash
|
||||
adb shell am start -n dev.twilitrealm.dusk/.DuskActivity \
|
||||
--es dusk_args "--backend vulkan"
|
||||
--es borealis_args "--backend vulkan"
|
||||
```
|
||||
|
||||
Supported extras:
|
||||
|
||||
- `dusk_args`: single shell-like argument string
|
||||
- `dusk_argv`: string-array argv
|
||||
- `borealis_args`: single shell-like argument string
|
||||
- `borealis_argv`: string-array argv
|
||||
|
||||
The legacy `dusk_args` and `dusk_argv` names remain accepted during the shell
|
||||
transition.
|
||||
|
||||
@@ -2,74 +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 duskGeneratedAssetsDir = layout.buildDirectory.dir('generated/assets/dusklight').get().asFile
|
||||
def syncDuskAssets = tasks.register('syncDuskAssets', Sync) {
|
||||
from(new File(duskRepoDir, 'res')) {
|
||||
into 'res'
|
||||
exclude '**/.DS_Store'
|
||||
}
|
||||
// Staged by platforms/android/scripts/stage-jni-libs.sh
|
||||
from(new File(projectDir, 'src/main/bundled_mods')) {
|
||||
into 'mods'
|
||||
include '*.dusk'
|
||||
}
|
||||
into duskGeneratedAssetsDir
|
||||
}
|
||||
def borealisDir = new File(duskRepoDir, 'extern/borealis')
|
||||
|
||||
android {
|
||||
namespace 'dev.twilitrealm.dusk'
|
||||
compileSdk 36
|
||||
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')
|
||||
]
|
||||
|
||||
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 = ['src/main/jniLibs']
|
||||
assets.srcDirs = [duskGeneratedAssetsDir]
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include 'arm64-v8a', 'x86_64'
|
||||
universalApk false
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
apply from: new File(borealisDir, 'platforms/android/gradle/borealis-application.gradle')
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
|
||||
tasks.configureEach { task ->
|
||||
if ((task.name.startsWith('merge') && task.name.endsWith('Assets')) ||
|
||||
task.name.toLowerCase().contains('lint')) {
|
||||
task.dependsOn(syncDuskAssets)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-4
@@ -1,4 +1 @@
|
||||
# Keep SDL activity and related JNI bridge methods.
|
||||
-keep class org.libsdl.app.** { *; }
|
||||
-keep class dev.twilitrealm.dusk.DuskHttpClient { *; }
|
||||
-keep class dev.twilitrealm.dusk.DuskHttpClient$Response { *; }
|
||||
-keep class dev.twilitrealm.dusk.DuskActivity { *; }
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
android:resource="@xml/game_mode_config" />
|
||||
|
||||
<provider
|
||||
android:name="dev.twilitrealm.dusk.DuskDocumentsProvider"
|
||||
android:authorities="dev.twilitrealm.dusk.documents"
|
||||
android:name="dev.encounter.borealis.BorealisDocumentsProvider"
|
||||
android:authorities="${applicationId}.documents"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||
|
||||
@@ -1,102 +1,23 @@
|
||||
package dev.twilitrealm.dusk;
|
||||
|
||||
import android.app.ActionBar;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowInsetsController;
|
||||
|
||||
import org.libsdl.app.SDLActivity;
|
||||
import org.libsdl.app.SDLSurface;
|
||||
import dev.encounter.borealis.BorealisActivity;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DuskActivity extends SDLActivity {
|
||||
public class DuskActivity extends BorealisActivity {
|
||||
private static final String TAG = "DuskActivity";
|
||||
private static final float DEFAULT_SURFACE_FRAME_RATE = 60.0f;
|
||||
private static final int FOLDER_DIALOG_REQUEST_CODE = 0x4455;
|
||||
private static final int MANAGE_STORAGE_REQUEST_CODE = 0x4456;
|
||||
private static final String EXTERNAL_STORAGE_AUTHORITY =
|
||||
"com.android.externalstorage.documents";
|
||||
|
||||
private long folderDialogUserdata = 0;
|
||||
private boolean awaitingManageStoragePermission = false;
|
||||
|
||||
private static native void nativeFolderDialogResult(long userdata, String path, String error);
|
||||
|
||||
private static String[] splitArgs(String raw) {
|
||||
List<String> out = new ArrayList<>();
|
||||
StringBuilder current = new StringBuilder();
|
||||
boolean inSingle = false;
|
||||
boolean inDouble = false;
|
||||
boolean escaped = false;
|
||||
|
||||
for (int i = 0; i < raw.length(); ++i) {
|
||||
char c = raw.charAt(i);
|
||||
if (escaped) {
|
||||
current.append(c);
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (c == '\\' && !inSingle) {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (c == '"' && !inSingle) {
|
||||
inDouble = !inDouble;
|
||||
continue;
|
||||
}
|
||||
if (c == '\'' && !inDouble) {
|
||||
inSingle = !inSingle;
|
||||
continue;
|
||||
}
|
||||
if (!inSingle && !inDouble && Character.isWhitespace(c)) {
|
||||
if (current.length() > 0) {
|
||||
out.add(current.toString());
|
||||
current.setLength(0);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
current.append(c);
|
||||
}
|
||||
|
||||
if (escaped) {
|
||||
current.append('\\');
|
||||
}
|
||||
if (current.length() > 0) {
|
||||
out.add(current.toString());
|
||||
}
|
||||
return out.toArray(new String[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
extractBundledMods();
|
||||
super.onCreate(savedInstanceState);
|
||||
hideSystemBars();
|
||||
}
|
||||
|
||||
// Bundled mod packages ship as APK assets, which the native loader cannot read directly;
|
||||
@@ -143,459 +64,23 @@ public class DuskActivity extends SDLActivity {
|
||||
file.delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SDLSurface createSDLSurface(Context context) {
|
||||
return new DuskSurface(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
hideSystemBars();
|
||||
if (awaitingManageStoragePermission) {
|
||||
resumeFolderDialogAfterPermissionGrant();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean hasFocus) {
|
||||
super.onWindowFocusChanged(hasFocus);
|
||||
if (hasFocus) {
|
||||
hideSystemBars();
|
||||
}
|
||||
}
|
||||
|
||||
private void hideSystemBars() {
|
||||
Window window = getWindow();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.setDecorFitsSystemWindows(false);
|
||||
WindowInsetsController ctrl = window.getDecorView().getWindowInsetsController();
|
||||
if (ctrl != null) {
|
||||
ctrl.setSystemBarsBehavior(
|
||||
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
ctrl.hide(WindowInsets.Type.systemBars());
|
||||
}
|
||||
} else {
|
||||
View decorView = window.getDecorView();
|
||||
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||
decorView.setSystemUiVisibility(uiOptions);
|
||||
ActionBar actionBar = getActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getLibraries() {
|
||||
// SDL3 is statically linked into libmain.so in this build.
|
||||
return new String[] {
|
||||
"main"
|
||||
};
|
||||
}
|
||||
|
||||
public void setPreferredSurfaceFrameRate(float frameRate) {
|
||||
runOnUiThread(() -> {
|
||||
if (mSurface instanceof DuskSurface) {
|
||||
((DuskSurface)mSurface).setPreferredFrameRate(frameRate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static final class DuskSurface extends SDLSurface {
|
||||
private float preferredFrameRate = DEFAULT_SURFACE_FRAME_RATE;
|
||||
|
||||
DuskSurface(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
super.surfaceChanged(holder, format, width, height);
|
||||
setTargetFrameRate(holder);
|
||||
}
|
||||
|
||||
void setPreferredFrameRate(float frameRate) {
|
||||
preferredFrameRate = frameRate;
|
||||
setTargetFrameRate(getHolder());
|
||||
}
|
||||
|
||||
private void setTargetFrameRate(SurfaceHolder holder) {
|
||||
if (!mIsSurfaceReady || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
return;
|
||||
}
|
||||
|
||||
Surface surface = holder != null ? holder.getSurface() : getHolder().getSurface();
|
||||
if (surface == null || !surface.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
float targetFrameRate = getMaxSupportedFrameRate();
|
||||
if (preferredFrameRate > 0.0f) {
|
||||
targetFrameRate = preferredFrameRate;
|
||||
}
|
||||
if (targetFrameRate <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
surface.setFrameRate(
|
||||
targetFrameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
|
||||
Log.v(TAG, "Requested surface frame rate " + targetFrameRate + " fps");
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(TAG, "Failed to request surface frame rate", e);
|
||||
}
|
||||
}
|
||||
|
||||
private float getMaxSupportedFrameRate() {
|
||||
if (mDisplay == null) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float maxFrameRate = mDisplay.getRefreshRate();
|
||||
Display.Mode[] modes = mDisplay.getSupportedModes();
|
||||
if (modes == null) {
|
||||
return maxFrameRate;
|
||||
}
|
||||
|
||||
for (Display.Mode mode : modes) {
|
||||
maxFrameRate = Math.max(maxFrameRate, mode.getRefreshRate());
|
||||
}
|
||||
return maxFrameRate;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getArguments() {
|
||||
String[] arguments = super.getArguments();
|
||||
if (arguments.length > 0) {
|
||||
return arguments;
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
if (intent != null) {
|
||||
String[] argv = intent.getStringArrayExtra("dusk_argv");
|
||||
if (argv != null && argv.length > 0) {
|
||||
return argv;
|
||||
}
|
||||
|
||||
String rawArgs = intent.getStringExtra("dusk_args");
|
||||
if (rawArgs != null) {
|
||||
String trimmed = rawArgs.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
return splitArgs(trimmed);
|
||||
}
|
||||
}
|
||||
if (intent == null) {
|
||||
return arguments;
|
||||
}
|
||||
return new String[0];
|
||||
String[] argv = intent.getStringArrayExtra("dusk_argv");
|
||||
if (argv != null && argv.length > 0) {
|
||||
return argv;
|
||||
}
|
||||
String rawArgs = intent.getStringExtra("dusk_args");
|
||||
return rawArgs == null ? arguments : splitArguments(rawArgs.trim());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
persistUriPermissions(data);
|
||||
}
|
||||
if (requestCode == FOLDER_DIALOG_REQUEST_CODE) {
|
||||
finishFolderDialog(resultCode, data);
|
||||
return;
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
public boolean showFolderDialog(long userdata) {
|
||||
if (userdata == 0 || folderDialogUserdata != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
folderDialogUserdata = userdata;
|
||||
if (requiresManageStoragePermission() && !hasManageStoragePermission()) {
|
||||
if (!requestManageStoragePermission()) {
|
||||
finishFolderDialogWithError("Unable to request Android file access permission");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
openFolderDialog();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void openFolderDialog() {
|
||||
runOnUiThread(() -> {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
|
||||
|
||||
try {
|
||||
startActivityForResult(intent, FOLDER_DIALOG_REQUEST_CODE);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w(TAG, "Unable to open folder dialog.", e);
|
||||
finishFolderDialog(Activity.RESULT_CANCELED, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean requiresManageStoragePermission() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;
|
||||
}
|
||||
|
||||
private boolean hasManageStoragePermission() {
|
||||
return !requiresManageStoragePermission() || Environment.isExternalStorageManager();
|
||||
}
|
||||
|
||||
private boolean requestManageStoragePermission() {
|
||||
if (!requiresManageStoragePermission()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
awaitingManageStoragePermission = true;
|
||||
runOnUiThread(() -> {
|
||||
if (tryStartManageStorageIntent(
|
||||
new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||
.setData(Uri.parse("package:" + getPackageName()))) ||
|
||||
tryStartManageStorageIntent(
|
||||
new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
finishFolderDialogWithError("Unable to request Android file access permission");
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean tryStartManageStorageIntent(Intent intent) {
|
||||
try {
|
||||
startActivityForResult(intent, MANAGE_STORAGE_REQUEST_CODE);
|
||||
return true;
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w(TAG, "Unable to open all-files access settings.", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void resumeFolderDialogAfterPermissionGrant() {
|
||||
awaitingManageStoragePermission = false;
|
||||
if (folderDialogUserdata == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasManageStoragePermission()) {
|
||||
openFolderDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
finishFolderDialogWithError(
|
||||
"Allow \"All files access\" for Dusklight before choosing a custom data folder");
|
||||
}
|
||||
|
||||
private void finishFolderDialogWithError(String error) {
|
||||
long userdata = folderDialogUserdata;
|
||||
folderDialogUserdata = 0;
|
||||
awaitingManageStoragePermission = false;
|
||||
if (userdata != 0) {
|
||||
nativeFolderDialogResult(userdata, null, error);
|
||||
}
|
||||
}
|
||||
|
||||
private void finishFolderDialog(int resultCode, Intent data) {
|
||||
long userdata = folderDialogUserdata;
|
||||
folderDialogUserdata = 0;
|
||||
if (userdata == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resultCode == RESULT_OK && data != null && data.getData() != null) {
|
||||
String path = getRealPathForUri(data.getData());
|
||||
if (path != null && !path.isEmpty()) {
|
||||
nativeFolderDialogResult(userdata, path, null);
|
||||
} else {
|
||||
nativeFolderDialogResult(
|
||||
userdata, null, "Selected folder is not available as a filesystem path");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
nativeFolderDialogResult(userdata, null, null);
|
||||
}
|
||||
|
||||
private String getRealPathForUri(Uri uri) {
|
||||
if (uri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String scheme = uri.getScheme();
|
||||
if ("file".equals(scheme)) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
if (!"content".equals(scheme) ||
|
||||
!EXTERNAL_STORAGE_AUTHORITY.equals(uri.getAuthority()) ||
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return getExternalStoragePathForDocumentId(getExternalStorageDocumentId(uri));
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.w(TAG, "Unable to resolve URI: " + uri, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getExternalStorageDocumentId(Uri uri) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && isTreeDocumentUri(uri)) {
|
||||
return DocumentsContract.getTreeDocumentId(uri);
|
||||
}
|
||||
|
||||
return DocumentsContract.getDocumentId(uri);
|
||||
}
|
||||
|
||||
private static boolean isTreeDocumentUri(Uri uri) {
|
||||
List<String> segments = uri.getPathSegments();
|
||||
return segments.size() >= 2 && "tree".equals(segments.get(0));
|
||||
}
|
||||
|
||||
private String getExternalStoragePathForDocumentId(String documentId) {
|
||||
if (documentId == null || documentId.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (documentId.startsWith("raw:")) {
|
||||
return documentId.substring("raw:".length());
|
||||
}
|
||||
|
||||
String[] parts = documentId.split(":", 2);
|
||||
String volumeId = parts[0];
|
||||
String relativePath = parts.length > 1 ? parts[1] : "";
|
||||
|
||||
File root = getExternalStorageRoot(volumeId);
|
||||
if (root == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return relativePath.isEmpty()
|
||||
? root.getAbsolutePath()
|
||||
: new File(root, relativePath).getAbsolutePath();
|
||||
}
|
||||
|
||||
private File getExternalStorageRoot(String volumeId) {
|
||||
if ("primary".equalsIgnoreCase(volumeId)) {
|
||||
return Environment.getExternalStorageDirectory();
|
||||
}
|
||||
if ("home".equalsIgnoreCase(volumeId)) {
|
||||
return new File(
|
||||
Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOCUMENTS);
|
||||
}
|
||||
|
||||
File[] externalFilesDirs = getExternalFilesDirs(null);
|
||||
if (externalFilesDirs != null) {
|
||||
for (File externalFilesDir : externalFilesDirs) {
|
||||
File root = getStorageRootForExternalFilesDir(externalFilesDir);
|
||||
if (root != null && volumeId.equalsIgnoreCase(root.getName())) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File fallback = new File("/storage", volumeId);
|
||||
return fallback.exists() ? fallback : null;
|
||||
}
|
||||
|
||||
private File getStorageRootForExternalFilesDir(File externalFilesDir) {
|
||||
if (externalFilesDir == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String path = externalFilesDir.getAbsolutePath();
|
||||
int androidDir = path.indexOf("/Android/");
|
||||
if (androidDir <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new File(path.substring(0, androidDir));
|
||||
}
|
||||
|
||||
private void persistUriPermissions(Intent data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int permissionFlags =
|
||||
data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
if (permissionFlags == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = data.getData();
|
||||
if (uri != null) {
|
||||
persistUriPermission(uri, permissionFlags);
|
||||
}
|
||||
|
||||
ClipData clipData = data.getClipData();
|
||||
if (clipData == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < clipData.getItemCount(); ++i) {
|
||||
Uri itemUri = clipData.getItemAt(i).getUri();
|
||||
if (itemUri != null) {
|
||||
persistUriPermission(itemUri, permissionFlags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void persistUriPermission(Uri uri, int permissionFlags) {
|
||||
if ((permissionFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
|
||||
persistUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION, "read");
|
||||
}
|
||||
if ((permissionFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
|
||||
persistUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, "write");
|
||||
}
|
||||
}
|
||||
|
||||
private void persistUriPermission(Uri uri, int permissionFlag, String permissionName) {
|
||||
try {
|
||||
getContentResolver().takePersistableUriPermission(uri, permissionFlag);
|
||||
} catch (SecurityException | IllegalArgumentException e) {
|
||||
Log.w(TAG, "Unable to persist " + permissionName + " URI permission for " + uri, e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getDisplayNameForUri(String uriString) {
|
||||
if (uriString == null || uriString.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Uri uri = Uri.parse(uriString);
|
||||
if ("content".equals(uri.getScheme())) {
|
||||
try (Cursor cursor = getContentResolver().query(
|
||||
uri, new String[] { OpenableColumns.DISPLAY_NAME }, null, null, null))
|
||||
{
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int displayNameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (displayNameColumn >= 0) {
|
||||
String displayName = cursor.getString(displayNameColumn);
|
||||
if (displayName != null && !displayName.isEmpty()) {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SecurityException | IllegalArgumentException e) {
|
||||
Log.w(TAG, "Unable to query display name for " + uri, e);
|
||||
}
|
||||
} else if ("file".equals(uri.getScheme())) {
|
||||
String path = uri.getPath();
|
||||
if (path != null && !path.isEmpty()) {
|
||||
String name = new File(path).getName();
|
||||
if (!name.isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String lastSegment = uri.getLastPathSegment();
|
||||
return lastSegment != null ? lastSegment : "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,467 +0,0 @@
|
||||
package dev.twilitrealm.dusk;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
import android.provider.DocumentsContract.Root;
|
||||
import android.provider.DocumentsProvider;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class DuskDocumentsProvider extends DocumentsProvider {
|
||||
public static final String AUTHORITY = "dev.twilitrealm.dusk.documents";
|
||||
|
||||
private static final String ROOT_ID = "dusk";
|
||||
private static final String ROOT_DOCUMENT_ID = "root";
|
||||
private static final String LOCATION_DESCRIPTOR_NAME = "data_location.json";
|
||||
private static final String DIRECTORY_MIME_TYPE = Document.MIME_TYPE_DIR;
|
||||
|
||||
private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
|
||||
Root.COLUMN_ROOT_ID,
|
||||
Root.COLUMN_FLAGS,
|
||||
Root.COLUMN_TITLE,
|
||||
Root.COLUMN_DOCUMENT_ID,
|
||||
Root.COLUMN_ICON,
|
||||
Root.COLUMN_AVAILABLE_BYTES,
|
||||
Root.COLUMN_SUMMARY
|
||||
};
|
||||
|
||||
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
|
||||
Document.COLUMN_DOCUMENT_ID,
|
||||
Document.COLUMN_DISPLAY_NAME,
|
||||
Document.COLUMN_FLAGS,
|
||||
Document.COLUMN_MIME_TYPE,
|
||||
Document.COLUMN_LAST_MODIFIED,
|
||||
Document.COLUMN_SIZE
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
if (!isCustomDataPathEnabled()) {
|
||||
ensureUserDirectories();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
|
||||
final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
|
||||
if (isCustomDataPathEnabled()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
final File root = getRootDirectory();
|
||||
final MatrixCursor.RowBuilder row = result.newRow();
|
||||
|
||||
row.add(Root.COLUMN_ROOT_ID, ROOT_ID);
|
||||
row.add(Root.COLUMN_FLAGS,
|
||||
Root.FLAG_LOCAL_ONLY |
|
||||
Root.FLAG_SUPPORTS_CREATE |
|
||||
Root.FLAG_SUPPORTS_IS_CHILD);
|
||||
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.app_name));
|
||||
row.add(Root.COLUMN_DOCUMENT_ID, ROOT_DOCUMENT_ID);
|
||||
row.add(Root.COLUMN_ICON, R.mipmap.icon);
|
||||
row.add(Root.COLUMN_AVAILABLE_BYTES, root.getFreeSpace());
|
||||
row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.documents_provider_summary));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
|
||||
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
|
||||
includeDocument(result, documentId, getFileForDocumentId(documentId));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
|
||||
throws FileNotFoundException
|
||||
{
|
||||
return queryChildDocumentsInternal(parentDocumentId, projection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, Bundle queryArgs)
|
||||
throws FileNotFoundException
|
||||
{
|
||||
return queryChildDocumentsInternal(parentDocumentId, projection);
|
||||
}
|
||||
|
||||
private Cursor queryChildDocumentsInternal(String parentDocumentId, String[] projection)
|
||||
throws FileNotFoundException
|
||||
{
|
||||
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
|
||||
final File parent = getFileForDocumentId(parentDocumentId);
|
||||
final File[] files = parent.listFiles();
|
||||
result.setNotificationUri(getContext().getContentResolver(), getChildDocumentsUri(parentDocumentId));
|
||||
|
||||
if (files == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (File file : files) {
|
||||
includeDocument(result, getDocumentIdForFile(file), file);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChildDocument(String parentDocumentId, String documentId) {
|
||||
try {
|
||||
final File parent = getFileForDocumentId(parentDocumentId);
|
||||
final File child = getFileForDocumentId(documentId);
|
||||
return isInside(parent, child);
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createDocument(String parentDocumentId, String mimeType, String displayName)
|
||||
throws FileNotFoundException
|
||||
{
|
||||
final File parent = getFileForDocumentId(parentDocumentId);
|
||||
if (!parent.isDirectory()) {
|
||||
throw new FileNotFoundException("Parent is not a directory: " + parentDocumentId);
|
||||
}
|
||||
|
||||
final String safeDisplayName = sanitizeDisplayName(displayName);
|
||||
final File file = buildUniqueFile(parent, safeDisplayName);
|
||||
final boolean created;
|
||||
if (DIRECTORY_MIME_TYPE.equals(mimeType)) {
|
||||
created = file.mkdir();
|
||||
} else {
|
||||
try {
|
||||
created = file.createNewFile();
|
||||
} catch (IOException e) {
|
||||
throw asFileNotFound("Unable to create document", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!created) {
|
||||
throw new FileNotFoundException("Unable to create document: " + displayName);
|
||||
}
|
||||
|
||||
notifyChildrenChanged(parentDocumentId);
|
||||
return getDocumentIdForFile(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String renameDocument(String documentId, String displayName) throws FileNotFoundException {
|
||||
final File file = getFileForDocumentId(documentId);
|
||||
if (ROOT_DOCUMENT_ID.equals(documentId)) {
|
||||
throw new FileNotFoundException("Cannot rename root document");
|
||||
}
|
||||
|
||||
final File target = buildUniqueFile(file.getParentFile(), sanitizeDisplayName(displayName));
|
||||
final String parentDocumentId = getDocumentIdForFile(file.getParentFile());
|
||||
if (!file.renameTo(target)) {
|
||||
throw new FileNotFoundException("Unable to rename document: " + documentId);
|
||||
}
|
||||
notifyDocumentChanged(documentId);
|
||||
notifyDocumentChanged(getDocumentIdForFile(target));
|
||||
notifyChildrenChanged(parentDocumentId);
|
||||
return getDocumentIdForFile(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteDocument(String documentId) throws FileNotFoundException {
|
||||
if (ROOT_DOCUMENT_ID.equals(documentId)) {
|
||||
throw new FileNotFoundException("Cannot delete root document");
|
||||
}
|
||||
|
||||
final File file = getFileForDocumentId(documentId);
|
||||
final String parentDocumentId = getDocumentIdForFile(file.getParentFile());
|
||||
deleteRecursively(file);
|
||||
notifyDocumentChanged(documentId);
|
||||
notifyChildrenChanged(parentDocumentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal)
|
||||
throws FileNotFoundException
|
||||
{
|
||||
return ParcelFileDescriptor.open(getFileForDocumentId(documentId), modeToParcelMode(mode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AssetFileDescriptor openDocumentThumbnail(String documentId, android.graphics.Point sizeHint,
|
||||
CancellationSignal signal) throws FileNotFoundException
|
||||
{
|
||||
throw new FileNotFoundException("Thumbnails are not supported");
|
||||
}
|
||||
|
||||
private void includeDocument(MatrixCursor result, String documentId, File file) throws FileNotFoundException {
|
||||
final MatrixCursor.RowBuilder row = result.newRow();
|
||||
final boolean isDirectory = file.isDirectory();
|
||||
final String displayName = ROOT_DOCUMENT_ID.equals(documentId)
|
||||
? getContext().getString(R.string.documents_provider_root_name)
|
||||
: file.getName();
|
||||
|
||||
int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME;
|
||||
if (isDirectory) {
|
||||
flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
|
||||
} else if (file.canWrite()) {
|
||||
flags |= Document.FLAG_SUPPORTS_WRITE;
|
||||
}
|
||||
if (ROOT_DOCUMENT_ID.equals(documentId)) {
|
||||
flags &= ~(Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME);
|
||||
}
|
||||
|
||||
row.add(Document.COLUMN_DOCUMENT_ID, documentId);
|
||||
row.add(Document.COLUMN_DISPLAY_NAME, displayName);
|
||||
row.add(Document.COLUMN_FLAGS, flags);
|
||||
row.add(Document.COLUMN_MIME_TYPE, isDirectory ? DIRECTORY_MIME_TYPE : getMimeType(file));
|
||||
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
|
||||
row.add(Document.COLUMN_SIZE, isDirectory ? null : file.length());
|
||||
}
|
||||
|
||||
private File getRootDirectory() throws FileNotFoundException {
|
||||
if (isCustomDataPathEnabled()) {
|
||||
throw new FileNotFoundException(
|
||||
"Dusk DocumentsProvider is disabled while a custom data path is configured");
|
||||
}
|
||||
|
||||
final File root = getContext().getFilesDir();
|
||||
if (root == null) {
|
||||
throw new FileNotFoundException("Dusklight files directory is unavailable");
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
private File getFileForDocumentId(String documentId) throws FileNotFoundException {
|
||||
final File root = getRootDirectory();
|
||||
if (ROOT_DOCUMENT_ID.equals(documentId)) {
|
||||
return root;
|
||||
}
|
||||
if (!documentId.startsWith(ROOT_DOCUMENT_ID + "/")) {
|
||||
throw new FileNotFoundException("Invalid document id: " + documentId);
|
||||
}
|
||||
|
||||
final String relativePath = documentId.substring(ROOT_DOCUMENT_ID.length() + 1);
|
||||
final File file = new File(root, relativePath);
|
||||
if (!isInside(root, file)) {
|
||||
throw new FileNotFoundException("Document escapes Dusklight files directory: " + documentId);
|
||||
}
|
||||
if (!file.exists()) {
|
||||
throw new FileNotFoundException("Document does not exist: " + documentId);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private String getDocumentIdForFile(File file) throws FileNotFoundException {
|
||||
final File root = getRootDirectory();
|
||||
if (sameFile(root, file)) {
|
||||
return ROOT_DOCUMENT_ID;
|
||||
}
|
||||
if (!isInside(root, file)) {
|
||||
throw new FileNotFoundException("File escapes Dusklight files directory: " + file);
|
||||
}
|
||||
|
||||
final String rootPath = canonicalPath(root);
|
||||
final String filePath = canonicalPath(file);
|
||||
return ROOT_DOCUMENT_ID + "/" + filePath.substring(rootPath.length() + 1);
|
||||
}
|
||||
|
||||
private void ensureUserDirectories() {
|
||||
final File root = getContext().getFilesDir();
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
new File(root, "texture_replacements").mkdirs();
|
||||
new File(root, "USA/Card A").mkdirs();
|
||||
new File(root, "EUR/Card A").mkdirs();
|
||||
}
|
||||
|
||||
private boolean isCustomDataPathEnabled() {
|
||||
if (getContext() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final File filesDir = getContext().getFilesDir();
|
||||
if (filesDir == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final File descriptor = new File(filesDir, LOCATION_DESCRIPTOR_NAME);
|
||||
if (!descriptor.isFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final JSONObject json = new JSONObject(readText(descriptor));
|
||||
return "custom".equals(json.optString("mode", "default"));
|
||||
} catch (IOException | JSONException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String readText(File file) throws IOException {
|
||||
try (FileInputStream input = new FileInputStream(file);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream())
|
||||
{
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = input.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, bytesRead);
|
||||
}
|
||||
return output.toString(StandardCharsets.UTF_8.name());
|
||||
}
|
||||
}
|
||||
|
||||
private static String[] resolveRootProjection(String[] projection) {
|
||||
return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
|
||||
}
|
||||
|
||||
private static String[] resolveDocumentProjection(String[] projection) {
|
||||
return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
|
||||
}
|
||||
|
||||
private static String sanitizeDisplayName(String displayName) throws FileNotFoundException {
|
||||
if (displayName == null) {
|
||||
throw new FileNotFoundException("Document name is empty");
|
||||
}
|
||||
|
||||
final String sanitized = displayName.trim();
|
||||
if (sanitized.isEmpty() || ".".equals(sanitized) || "..".equals(sanitized) ||
|
||||
sanitized.contains("/") || sanitized.contains("\\"))
|
||||
{
|
||||
throw new FileNotFoundException("Invalid document name: " + displayName);
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private static File buildUniqueFile(File parent, String displayName) {
|
||||
File file = new File(parent, displayName);
|
||||
if (!file.exists()) {
|
||||
return file;
|
||||
}
|
||||
|
||||
final int dot = displayName.lastIndexOf('.');
|
||||
final String baseName = dot > 0 ? displayName.substring(0, dot) : displayName;
|
||||
final String extension = dot > 0 ? displayName.substring(dot) : "";
|
||||
for (int i = 1; i < 100; ++i) {
|
||||
file = new File(parent, baseName + " (" + i + ")" + extension);
|
||||
if (!file.exists()) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return new File(parent, baseName + " (" + System.currentTimeMillis() + ")" + extension);
|
||||
}
|
||||
|
||||
private static int modeToParcelMode(String mode) {
|
||||
if ("r".equals(mode)) {
|
||||
return ParcelFileDescriptor.MODE_READ_ONLY;
|
||||
}
|
||||
if ("w".equals(mode) || "wt".equals(mode)) {
|
||||
return ParcelFileDescriptor.MODE_WRITE_ONLY |
|
||||
ParcelFileDescriptor.MODE_CREATE |
|
||||
ParcelFileDescriptor.MODE_TRUNCATE;
|
||||
}
|
||||
if ("wa".equals(mode)) {
|
||||
return ParcelFileDescriptor.MODE_WRITE_ONLY |
|
||||
ParcelFileDescriptor.MODE_CREATE |
|
||||
ParcelFileDescriptor.MODE_APPEND;
|
||||
}
|
||||
if ("rw".equals(mode)) {
|
||||
return ParcelFileDescriptor.MODE_READ_WRITE |
|
||||
ParcelFileDescriptor.MODE_CREATE;
|
||||
}
|
||||
if ("rwt".equals(mode)) {
|
||||
return ParcelFileDescriptor.MODE_READ_WRITE |
|
||||
ParcelFileDescriptor.MODE_CREATE |
|
||||
ParcelFileDescriptor.MODE_TRUNCATE;
|
||||
}
|
||||
return ParcelFileDescriptor.MODE_READ_ONLY;
|
||||
}
|
||||
|
||||
private static String getMimeType(File file) {
|
||||
final int dot = file.getName().lastIndexOf('.');
|
||||
if (dot >= 0) {
|
||||
final String extension = file.getName().substring(dot + 1).toLowerCase();
|
||||
final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
if (mimeType != null) {
|
||||
return mimeType;
|
||||
}
|
||||
}
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
private Uri getChildDocumentsUri(String parentDocumentId) {
|
||||
return DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId);
|
||||
}
|
||||
|
||||
private void notifyChildrenChanged(String parentDocumentId) {
|
||||
final ContentResolver resolver = getContext().getContentResolver();
|
||||
resolver.notifyChange(getChildDocumentsUri(parentDocumentId), null, false);
|
||||
}
|
||||
|
||||
private void notifyDocumentChanged(String documentId) {
|
||||
final ContentResolver resolver = getContext().getContentResolver();
|
||||
resolver.notifyChange(DocumentsContract.buildDocumentUri(AUTHORITY, documentId), null, false);
|
||||
}
|
||||
|
||||
private static void deleteRecursively(File file) throws FileNotFoundException {
|
||||
if (file.isDirectory()) {
|
||||
final File[] children = file.listFiles();
|
||||
if (children != null) {
|
||||
for (File child : children) {
|
||||
deleteRecursively(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!file.delete()) {
|
||||
throw new FileNotFoundException("Unable to delete document: " + file);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isInside(File parent, File child) {
|
||||
try {
|
||||
final String parentPath = canonicalPath(parent);
|
||||
final String childPath = canonicalPath(child);
|
||||
return childPath.equals(parentPath) || childPath.startsWith(parentPath + File.separator);
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean sameFile(File a, File b) {
|
||||
try {
|
||||
return canonicalPath(a).equals(canonicalPath(b));
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String canonicalPath(File file) throws FileNotFoundException {
|
||||
try {
|
||||
return file.getCanonicalPath();
|
||||
} catch (IOException e) {
|
||||
throw asFileNotFound("Unable to resolve path", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static FileNotFoundException asFileNotFound(String message, IOException cause) {
|
||||
final FileNotFoundException exception = new FileNotFoundException(message + ": " + cause.getMessage());
|
||||
exception.initCause(cause);
|
||||
return exception;
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
package dev.twilitrealm.dusk;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
|
||||
public final class DuskHttpClient {
|
||||
public static final int ERROR_NONE = 0;
|
||||
public static final int ERROR_INVALID_URL = 1;
|
||||
public static final int ERROR_UNSUPPORTED_SCHEME = 2;
|
||||
public static final int ERROR_TIMEOUT = 3;
|
||||
public static final int ERROR_TOO_LARGE = 4;
|
||||
public static final int ERROR_NETWORK = 5;
|
||||
|
||||
private static final int MAX_REDIRECTS = 5;
|
||||
|
||||
public static final class Response {
|
||||
public int error;
|
||||
public String message;
|
||||
public int statusCode;
|
||||
public String[] headerNames;
|
||||
public String[] headerValues;
|
||||
public byte[] body;
|
||||
|
||||
Response(int error, String message, int statusCode, String[] headerNames,
|
||||
String[] headerValues, byte[] body) {
|
||||
this.error = error;
|
||||
this.message = message;
|
||||
this.statusCode = statusCode;
|
||||
this.headerNames = headerNames != null ? headerNames : new String[0];
|
||||
this.headerValues = headerValues != null ? headerValues : new String[0];
|
||||
this.body = body != null ? body : new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
private DuskHttpClient() {
|
||||
}
|
||||
|
||||
public static Response get(String url, String[] headerNames, String[] headerValues,
|
||||
int timeoutMs, long maxBodyBytes) {
|
||||
if (url == null || url.isEmpty()) {
|
||||
return fail(ERROR_INVALID_URL, "URL is empty");
|
||||
}
|
||||
|
||||
try {
|
||||
URL currentUrl = new URL(url);
|
||||
if (!isHttps(currentUrl)) {
|
||||
return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported");
|
||||
}
|
||||
|
||||
for (int redirect = 0; redirect <= MAX_REDIRECTS; ++redirect) {
|
||||
HttpsURLConnection connection =
|
||||
(HttpsURLConnection) currentUrl.openConnection();
|
||||
try {
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setConnectTimeout(timeoutMs);
|
||||
connection.setReadTimeout(timeoutMs);
|
||||
connection.setUseCaches(false);
|
||||
connection.setInstanceFollowRedirects(false);
|
||||
applyHeaders(connection, headerNames, headerValues);
|
||||
|
||||
int statusCode = connection.getResponseCode();
|
||||
if (isRedirect(statusCode)) {
|
||||
String location = connection.getHeaderField("Location");
|
||||
if (location == null || location.isEmpty()) {
|
||||
return fail(ERROR_NETWORK, "Redirect response did not include Location",
|
||||
statusCode, connection, new byte[0]);
|
||||
}
|
||||
|
||||
URL nextUrl = new URL(currentUrl, location);
|
||||
if (!isHttps(nextUrl)) {
|
||||
return fail(ERROR_UNSUPPORTED_SCHEME,
|
||||
"Only https:// redirects are supported", statusCode,
|
||||
connection, new byte[0]);
|
||||
}
|
||||
currentUrl = nextUrl;
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] body = readBody(connection, statusCode, maxBodyBytes);
|
||||
return success(statusCode, connection, body);
|
||||
} catch (ResponseTooLargeException e) {
|
||||
return fail(ERROR_TOO_LARGE, "Response body exceeded the configured limit",
|
||||
safeStatusCode(connection), connection, e.partialBody);
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
return fail(ERROR_NETWORK, "Too many redirects");
|
||||
} catch (MalformedURLException e) {
|
||||
return fail(ERROR_INVALID_URL, "Failed to parse URL");
|
||||
} catch (SocketTimeoutException e) {
|
||||
return fail(ERROR_TIMEOUT, "Request timed out");
|
||||
} catch (IOException e) {
|
||||
String message = e.getMessage();
|
||||
return fail(ERROR_NETWORK, message != null ? message : e.toString());
|
||||
} catch (ClassCastException e) {
|
||||
return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported");
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyHeaders(HttpsURLConnection connection, String[] names,
|
||||
String[] values) {
|
||||
if (names == null || values == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int count = Math.min(names.length, values.length);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (names[i] != null && values[i] != null) {
|
||||
connection.setRequestProperty(names[i], values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isHttps(URL url) {
|
||||
return "https".equalsIgnoreCase(url.getProtocol());
|
||||
}
|
||||
|
||||
private static boolean isRedirect(int statusCode) {
|
||||
return statusCode == HttpURLConnection.HTTP_MOVED_PERM ||
|
||||
statusCode == HttpURLConnection.HTTP_MOVED_TEMP ||
|
||||
statusCode == HttpURLConnection.HTTP_SEE_OTHER ||
|
||||
statusCode == 307 ||
|
||||
statusCode == 308;
|
||||
}
|
||||
|
||||
private static byte[] readBody(HttpsURLConnection connection, int statusCode,
|
||||
long maxBodyBytes) throws IOException,
|
||||
ResponseTooLargeException {
|
||||
InputStream stream = statusCode >= HttpURLConnection.HTTP_BAD_REQUEST ?
|
||||
connection.getErrorStream() : connection.getInputStream();
|
||||
if (stream == null) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
try (InputStream bodyStream = stream;
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
byte[] buffer = new byte[8192];
|
||||
long total = 0;
|
||||
while (true) {
|
||||
int read = bodyStream.read(buffer);
|
||||
if (read < 0) {
|
||||
return out.toByteArray();
|
||||
}
|
||||
if (read == 0) {
|
||||
continue;
|
||||
}
|
||||
if (read > maxBodyBytes || total > maxBodyBytes - read) {
|
||||
throw new ResponseTooLargeException(out.toByteArray());
|
||||
}
|
||||
out.write(buffer, 0, read);
|
||||
total += read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int safeStatusCode(HttpsURLConnection connection) {
|
||||
try {
|
||||
return connection.getResponseCode();
|
||||
} catch (IOException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static Response success(int statusCode, HttpsURLConnection connection, byte[] body) {
|
||||
HeaderLists headers = readHeaders(connection);
|
||||
return new Response(ERROR_NONE, "", statusCode, headers.names, headers.values, body);
|
||||
}
|
||||
|
||||
private static Response fail(int error, String message) {
|
||||
return new Response(error, message, 0, null, null, null);
|
||||
}
|
||||
|
||||
private static Response fail(int error, String message, int statusCode,
|
||||
HttpsURLConnection connection, byte[] body) {
|
||||
HeaderLists headers = readHeaders(connection);
|
||||
return new Response(error, message, statusCode, headers.names, headers.values, body);
|
||||
}
|
||||
|
||||
private static HeaderLists readHeaders(HttpsURLConnection connection) {
|
||||
List<String> names = new ArrayList<>();
|
||||
List<String> values = new ArrayList<>();
|
||||
|
||||
Map<String, List<String>> headerFields = connection.getHeaderFields();
|
||||
if (headerFields == null) {
|
||||
return new HeaderLists(new String[0], new String[0]);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : headerFields.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
if (name == null) {
|
||||
continue;
|
||||
}
|
||||
List<String> entryValues = entry.getValue();
|
||||
if (entryValues == null || entryValues.isEmpty()) {
|
||||
names.add(name);
|
||||
values.add("");
|
||||
continue;
|
||||
}
|
||||
for (String value : entryValues) {
|
||||
names.add(name);
|
||||
values.add(value != null ? value : "");
|
||||
}
|
||||
}
|
||||
|
||||
return new HeaderLists(names.toArray(new String[0]), values.toArray(new String[0]));
|
||||
}
|
||||
|
||||
private static final class HeaderLists {
|
||||
final String[] names;
|
||||
final String[] values;
|
||||
|
||||
HeaderLists(String[] names, String[] values) {
|
||||
this.names = names;
|
||||
this.values = values;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ResponseTooLargeException extends Exception {
|
||||
final byte[] partialBody;
|
||||
|
||||
ResponseTooLargeException(byte[] partialBody) {
|
||||
this.partialBody = partialBody;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.hardware.usb.UsbDevice;
|
||||
|
||||
interface HIDDevice
|
||||
{
|
||||
public int getId();
|
||||
public int getVendorId();
|
||||
public int getProductId();
|
||||
public String getSerialNumber();
|
||||
public int getVersion();
|
||||
public String getManufacturerName();
|
||||
public String getProductName();
|
||||
public UsbDevice getDevice();
|
||||
public boolean open();
|
||||
public int writeReport(byte[] report, boolean feature);
|
||||
public boolean readReport(byte[] report, boolean feature);
|
||||
public void setFrozen(boolean frozen);
|
||||
public void close();
|
||||
public void shutdown();
|
||||
}
|
||||
@@ -1,829 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCallback;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothGattDescriptor;
|
||||
import android.bluetooth.BluetoothManager;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
import android.bluetooth.BluetoothGattService;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.os.*;
|
||||
|
||||
//import com.android.internal.util.HexDump;
|
||||
|
||||
import java.lang.Runnable;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.UUID;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice {
|
||||
|
||||
private static final String TAG = "hidapi";
|
||||
private HIDDeviceManager mManager;
|
||||
private BluetoothDevice mDevice;
|
||||
private int mDeviceId;
|
||||
private BluetoothGatt mGatt;
|
||||
private boolean mIsRegistered = false;
|
||||
private boolean mIsConnected = false;
|
||||
private boolean mIsChromebook = false;
|
||||
private boolean mIsReconnecting = false;
|
||||
private boolean mHasEnabledNotifications = false;
|
||||
private boolean mHasSeenInputUpdate = false;
|
||||
private boolean mFrozen = false;
|
||||
private LinkedList<GattOperation> mOperations;
|
||||
GattOperation mCurrentOperation = null;
|
||||
private Handler mHandler;
|
||||
private int mProductId = -1;
|
||||
private int mReportId = 0;
|
||||
private UUID mInputCharacteristic;
|
||||
|
||||
private static final int D0G_BLE2_PID = 0x1106;
|
||||
private static final int TRITON_BLE_PID = 0x1303;
|
||||
|
||||
|
||||
private static final int TRANSPORT_AUTO = 0;
|
||||
private static final int TRANSPORT_BREDR = 1;
|
||||
private static final int TRANSPORT_LE = 2;
|
||||
|
||||
private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000;
|
||||
|
||||
static final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3");
|
||||
static final UUID inputCharacteristicD0G = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3");
|
||||
static final UUID inputCharacteristicTriton_0x45 = UUID.fromString("100F6C7A-1735-4313-B402-38567131E5F3");
|
||||
static final UUID inputCharacteristicTriton_0x47 = UUID.fromString("100F6C7C-1735-4313-B402-38567131E5F3");
|
||||
static final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3");
|
||||
static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 };
|
||||
|
||||
private HashMap<Integer, BluetoothGattCharacteristic> mOutputReportChars = new HashMap<Integer, BluetoothGattCharacteristic>();
|
||||
|
||||
static class GattOperation {
|
||||
private enum Operation {
|
||||
CHR_READ,
|
||||
CHR_WRITE,
|
||||
ENABLE_NOTIFICATION
|
||||
}
|
||||
|
||||
Operation mOp;
|
||||
UUID mUuid;
|
||||
byte[] mValue;
|
||||
BluetoothGatt mGatt;
|
||||
boolean mResult = true;
|
||||
int mDelayMs = 0;
|
||||
|
||||
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
|
||||
mGatt = gatt;
|
||||
mOp = operation;
|
||||
mUuid = uuid;
|
||||
}
|
||||
|
||||
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, int delayMs) {
|
||||
mGatt = gatt;
|
||||
mOp = operation;
|
||||
mUuid = uuid;
|
||||
mDelayMs = delayMs;
|
||||
}
|
||||
|
||||
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
|
||||
mGatt = gatt;
|
||||
mOp = operation;
|
||||
mUuid = uuid;
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value, int delayMs) {
|
||||
mGatt = gatt;
|
||||
mOp = operation;
|
||||
mUuid = uuid;
|
||||
mValue = value;
|
||||
mDelayMs = delayMs;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
// This is executed in main thread
|
||||
BluetoothGattCharacteristic chr;
|
||||
|
||||
switch (mOp) {
|
||||
case CHR_READ:
|
||||
chr = getCharacteristic(mUuid);
|
||||
//Log.v(TAG, "Reading characteristic " + chr.getUuid());
|
||||
if (!mGatt.readCharacteristic(chr)) {
|
||||
Log.e(TAG, "Unable to read characteristic " + mUuid.toString());
|
||||
mResult = false;
|
||||
break;
|
||||
}
|
||||
mResult = true;
|
||||
break;
|
||||
case CHR_WRITE:
|
||||
chr = getCharacteristic(mUuid);
|
||||
//Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
|
||||
chr.setValue(mValue);
|
||||
if (!mGatt.writeCharacteristic(chr)) {
|
||||
Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
|
||||
mResult = false;
|
||||
break;
|
||||
}
|
||||
mResult = true;
|
||||
break;
|
||||
case ENABLE_NOTIFICATION:
|
||||
chr = getCharacteristic(mUuid);
|
||||
//Log.v(TAG, "Writing descriptor of " + chr.getUuid());
|
||||
if (chr != null) {
|
||||
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
|
||||
if (cccd != null) {
|
||||
int properties = chr.getProperties();
|
||||
byte[] value;
|
||||
if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
|
||||
value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
|
||||
} else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) {
|
||||
value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
|
||||
} else {
|
||||
Log.e(TAG, "Unable to start notifications on input characteristic");
|
||||
mResult = false;
|
||||
return;
|
||||
}
|
||||
|
||||
mGatt.setCharacteristicNotification(chr, true);
|
||||
cccd.setValue(value);
|
||||
if (!mGatt.writeDescriptor(cccd)) {
|
||||
Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
|
||||
mResult = false;
|
||||
return;
|
||||
}
|
||||
mResult = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean finish() {
|
||||
return mResult;
|
||||
}
|
||||
|
||||
public int getDelayMs() { return mDelayMs; }
|
||||
|
||||
private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
|
||||
BluetoothGattService valveService = mGatt.getService(steamControllerService);
|
||||
if (valveService == null)
|
||||
return null;
|
||||
return valveService.getCharacteristic(uuid);
|
||||
}
|
||||
|
||||
static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) {
|
||||
return new GattOperation(gatt, Operation.CHR_READ, uuid);
|
||||
}
|
||||
|
||||
static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) {
|
||||
return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value);
|
||||
}
|
||||
|
||||
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
|
||||
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid);
|
||||
}
|
||||
|
||||
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid, int delayMs) {
|
||||
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid, delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) {
|
||||
mManager = manager;
|
||||
mDevice = device;
|
||||
mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier());
|
||||
mIsRegistered = false;
|
||||
mIsChromebook = SDLActivity.isChromebook();
|
||||
mOperations = new LinkedList<GattOperation>();
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
mGatt = connectGatt();
|
||||
mHasEnabledNotifications = false;
|
||||
mHasSeenInputUpdate = false;
|
||||
// final HIDDeviceBLESteamController finalThis = this;
|
||||
// mHandler.postDelayed(new Runnable() {
|
||||
// @Override
|
||||
// void run() {
|
||||
// finalThis.checkConnectionForChromebookIssue();
|
||||
// }
|
||||
// }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
String getIdentifier() {
|
||||
return String.format("SteamController.%s", mDevice.getAddress());
|
||||
}
|
||||
|
||||
BluetoothGatt getGatt() {
|
||||
return mGatt;
|
||||
}
|
||||
|
||||
// Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
|
||||
// of TRANSPORT_LE. Let's force ourselves to connect low energy.
|
||||
private BluetoothGatt connectGatt(boolean managed) {
|
||||
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
|
||||
try {
|
||||
return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
|
||||
} catch (Exception e) {
|
||||
return mDevice.connectGatt(mManager.getContext(), managed, this);
|
||||
}
|
||||
} else {
|
||||
return mDevice.connectGatt(mManager.getContext(), managed, this);
|
||||
}
|
||||
}
|
||||
|
||||
private BluetoothGatt connectGatt() {
|
||||
return connectGatt(false);
|
||||
}
|
||||
|
||||
protected int getConnectionState() {
|
||||
|
||||
Context context = mManager.getContext();
|
||||
if (context == null) {
|
||||
// We are lacking any context to get our Bluetooth information. We'll just assume disconnected.
|
||||
return BluetoothProfile.STATE_DISCONNECTED;
|
||||
}
|
||||
|
||||
BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
|
||||
if (btManager == null) {
|
||||
// This device doesn't support Bluetooth. We should never be here, because how did
|
||||
// we instantiate a device to start with?
|
||||
return BluetoothProfile.STATE_DISCONNECTED;
|
||||
}
|
||||
|
||||
return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
|
||||
}
|
||||
|
||||
void reconnect() {
|
||||
|
||||
if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
|
||||
mGatt.disconnect();
|
||||
mGatt = connectGatt();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected void checkConnectionForChromebookIssue() {
|
||||
if (!mIsChromebook) {
|
||||
// We only do this on Chromebooks, because otherwise it's really annoying to just attempt
|
||||
// over and over.
|
||||
return;
|
||||
}
|
||||
|
||||
int connectionState = getConnectionState();
|
||||
|
||||
switch (connectionState) {
|
||||
case BluetoothProfile.STATE_CONNECTED:
|
||||
if (!mIsConnected) {
|
||||
// We are in the Bad Chromebook Place. We can force a disconnect
|
||||
// to try to recover.
|
||||
Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect.");
|
||||
mIsReconnecting = true;
|
||||
mGatt.disconnect();
|
||||
mGatt = connectGatt(false);
|
||||
break;
|
||||
}
|
||||
else if (!isRegistered()) {
|
||||
if (mGatt.getServices().size() > 0) {
|
||||
Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover.");
|
||||
probeService(this);
|
||||
}
|
||||
else {
|
||||
Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover.");
|
||||
mIsReconnecting = true;
|
||||
mGatt.disconnect();
|
||||
mGatt = connectGatt(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case BluetoothProfile.STATE_DISCONNECTED:
|
||||
Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover.");
|
||||
|
||||
mIsReconnecting = true;
|
||||
mGatt.disconnect();
|
||||
mGatt = connectGatt(false);
|
||||
break;
|
||||
|
||||
case BluetoothProfile.STATE_CONNECTING:
|
||||
Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer.");
|
||||
break;
|
||||
}
|
||||
|
||||
final HIDDeviceBLESteamController finalThis = this;
|
||||
mHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
finalThis.checkConnectionForChromebookIssue();
|
||||
}
|
||||
}, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
private boolean isRegistered() {
|
||||
return mIsRegistered;
|
||||
}
|
||||
|
||||
private void setRegistered() {
|
||||
mIsRegistered = true;
|
||||
}
|
||||
|
||||
private boolean probeService(HIDDeviceBLESteamController controller) {
|
||||
|
||||
if (isRegistered()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!mIsConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.v(TAG, "probeService controller=" + controller);
|
||||
|
||||
for (BluetoothGattService service : mGatt.getServices()) {
|
||||
if (service.getUuid().equals(steamControllerService)) {
|
||||
Log.v(TAG, "Found Valve steam controller service " + service.getUuid());
|
||||
|
||||
for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
|
||||
if (chr.getUuid().equals(inputCharacteristicTriton_0x45)) {
|
||||
Log.v(TAG, "Found Triton input characteristic 0x45");
|
||||
mProductId = TRITON_BLE_PID;
|
||||
mReportId = 0x45;
|
||||
mInputCharacteristic = chr.getUuid();
|
||||
} else if (chr.getUuid().equals(inputCharacteristicTriton_0x47)) {
|
||||
Log.v(TAG, "Found Triton input characteristic 0x47");
|
||||
mProductId = TRITON_BLE_PID;
|
||||
mReportId = 0x47;
|
||||
mInputCharacteristic = chr.getUuid();
|
||||
} else if (chr.getUuid().equals(inputCharacteristicD0G)) {
|
||||
Log.v(TAG, "Found D0G input characteristic");
|
||||
mProductId = D0G_BLE2_PID;
|
||||
mReportId = 0x03;
|
||||
mInputCharacteristic = chr.getUuid();
|
||||
} else {
|
||||
Pattern reportPattern = Pattern.compile("100F6C([0-9A-Z]{2})", Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = reportPattern.matcher(chr.getUuid().toString());
|
||||
|
||||
if (matcher.find()) {
|
||||
try {
|
||||
int reportId = Integer.parseInt(matcher.group(1), 16);
|
||||
|
||||
reportId -= 0x35;
|
||||
if (reportId >= 0x80) {
|
||||
// This is a Triton output report characteristic that we need to care about.
|
||||
Log.v(TAG, "Found Triton output report 0x" + Integer.toString(reportId, 16));
|
||||
mOutputReportChars.put(reportId, chr);
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException nfe) {
|
||||
Log.w(TAG, "Could not parse report characteristic " + chr.getUuid().toString() + ": " + nfe.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
|
||||
if (chr.getUuid().equals(mInputCharacteristic)) {
|
||||
// Start notifications
|
||||
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
|
||||
if (cccd != null) {
|
||||
enableNotification(chr.getUuid());
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) {
|
||||
Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us.");
|
||||
mIsConnected = false;
|
||||
mIsReconnecting = true;
|
||||
mGatt.disconnect();
|
||||
mGatt = connectGatt(false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void finishCurrentGattOperation() {
|
||||
GattOperation op = null;
|
||||
synchronized (mOperations) {
|
||||
if (mCurrentOperation != null) {
|
||||
op = mCurrentOperation;
|
||||
mCurrentOperation = null;
|
||||
}
|
||||
}
|
||||
if (op != null) {
|
||||
boolean result = op.finish(); // TODO: Maybe in main thread as well?
|
||||
|
||||
// Our operation failed, let's add it back to the beginning of our queue.
|
||||
if (!result) {
|
||||
mOperations.addFirst(op);
|
||||
}
|
||||
}
|
||||
executeNextGattOperation();
|
||||
}
|
||||
|
||||
private void executeNextGattOperation() {
|
||||
synchronized (mOperations) {
|
||||
if (mCurrentOperation != null)
|
||||
return;
|
||||
|
||||
if (mOperations.isEmpty())
|
||||
return;
|
||||
|
||||
mCurrentOperation = mOperations.removeFirst();
|
||||
}
|
||||
|
||||
Runnable gattOperationRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (mOperations) {
|
||||
if (mCurrentOperation == null) {
|
||||
Log.e(TAG, "Current operation null in executor?");
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentOperation.run();
|
||||
// now wait for the GATT callback and when it comes, finish this operation
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (mCurrentOperation.getDelayMs() == 0) {
|
||||
// Run in main thread
|
||||
mHandler.post(gattOperationRunnable);
|
||||
}
|
||||
else {
|
||||
// If we have a delay on this operation, wait before we post it.
|
||||
mHandler.postDelayed(gattOperationRunnable, mCurrentOperation.getDelayMs());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void queueGattOperation(GattOperation op) {
|
||||
synchronized (mOperations) {
|
||||
mOperations.add(op);
|
||||
}
|
||||
executeNextGattOperation();
|
||||
}
|
||||
|
||||
private void enableNotification(UUID chrUuid) {
|
||||
// Add a 500ms delay to notification write for Amazon Fire TV devices, as otherwise if we do this too quickly after connecting
|
||||
// it will return success and then silently drop the operation on the floor.
|
||||
GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid, 500);
|
||||
queueGattOperation(op);
|
||||
|
||||
// Amazon Fire devices can also silently timeout on writeDescriptor, so
|
||||
// set up a little delayed check that will attempt to write a second time.
|
||||
//
|
||||
// While this only seems to be needed on Amazon Fire TV devices at present, it
|
||||
// doesn't hurt to have a retry on other devices as well.
|
||||
//
|
||||
final HIDDeviceBLESteamController finalThis = this;
|
||||
final UUID finalUuid = chrUuid;
|
||||
mHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!finalThis.mHasEnabledNotifications) {
|
||||
|
||||
if (finalThis.mHasSeenInputUpdate) {
|
||||
// Amazon Five devices may have enabled notifications on the input characteristic and not given us a callback. If we've seen
|
||||
// input reports, though, somewhat by definition notifications are enabled.
|
||||
Log.w(TAG, "WriteDescriptor has never returned, but we've seen input reports. Moving on with controller initialization.");
|
||||
finalThis.mHasEnabledNotifications = true;
|
||||
finalThis.enableValveMode();
|
||||
return;
|
||||
}
|
||||
|
||||
// Give one more try.
|
||||
GattOperation retry = HIDDeviceBLESteamController.GattOperation.enableNotification(finalThis.mGatt, finalUuid, 500);
|
||||
finalThis.queueGattOperation(retry);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
void writeCharacteristic(UUID uuid, byte[] value) {
|
||||
GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value);
|
||||
queueGattOperation(op);
|
||||
}
|
||||
|
||||
void readCharacteristic(UUID uuid) {
|
||||
GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid);
|
||||
queueGattOperation(op);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////// BluetoothGattCallback overridden methods
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothGatt g, int status, int newState) {
|
||||
//Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
|
||||
mIsReconnecting = false;
|
||||
if (newState == 2) {
|
||||
mIsConnected = true;
|
||||
// Run directly, without GattOperation
|
||||
if (!isRegistered()) {
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mGatt.discoverServices();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (newState == 0) {
|
||||
mIsConnected = false;
|
||||
}
|
||||
|
||||
// Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
|
||||
//Log.v(TAG, "onServicesDiscovered status=" + status);
|
||||
if (status == 0) {
|
||||
if (gatt.getServices().size() == 0) {
|
||||
Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack.");
|
||||
mIsReconnecting = true;
|
||||
mIsConnected = false;
|
||||
gatt.disconnect();
|
||||
mGatt = connectGatt(false);
|
||||
} else {
|
||||
if (getProductId() == TRITON_BLE_PID) {
|
||||
// Android will not properly play well with Data Length Extensions without manually requesting a large MTU,
|
||||
// and Triton controllers require DLE support.
|
||||
//
|
||||
// 517 is basically a "magic number" as far as Android's bluetooth code is concerned, so do not change
|
||||
// this value. It is functionally "please enable data length extensions" on some Android builds.
|
||||
mGatt.requestMtu(517);
|
||||
}
|
||||
|
||||
probeService(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||
//Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid());
|
||||
|
||||
if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) {
|
||||
mManager.HIDDeviceReportResponse(getId(), characteristic.getValue());
|
||||
}
|
||||
|
||||
finishCurrentGattOperation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||
//Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
|
||||
|
||||
if (characteristic.getUuid().equals(reportCharacteristic)) {
|
||||
// Only register controller with the native side once it has been fully configured
|
||||
if (!isRegistered()) {
|
||||
Log.v(TAG, "Registering Steam Controller with ID: " + getId());
|
||||
mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true, mReportId);
|
||||
setRegistered();
|
||||
}
|
||||
}
|
||||
|
||||
finishCurrentGattOperation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||
// Enable this for verbose logging of controller input reports
|
||||
//Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue()));
|
||||
|
||||
if (characteristic.getUuid().equals(mInputCharacteristic) && !mFrozen) {
|
||||
mHasSeenInputUpdate = true;
|
||||
mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
|
||||
//Log.v(TAG, "onDescriptorRead status=" + status);
|
||||
}
|
||||
|
||||
private void enableValveMode()
|
||||
{
|
||||
BluetoothGattService valveService = mGatt.getService(steamControllerService);
|
||||
if (valveService == null)
|
||||
return;
|
||||
|
||||
BluetoothGattCharacteristic reportChr = valveService.getCharacteristic(reportCharacteristic);
|
||||
if (reportChr != null) {
|
||||
if (getProductId() == TRITON_BLE_PID) {
|
||||
// For Triton we just mark things registered.
|
||||
Log.v(TAG, "Registering Triton Steam Controller with ID: " + getId());
|
||||
mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true, mReportId);
|
||||
setRegistered();
|
||||
} else {
|
||||
// For the original controller, we need to manually enter Valve mode.
|
||||
Log.v(TAG, "Writing report characteristic to enter valve mode");
|
||||
reportChr.setValue(enterValveMode);
|
||||
mGatt.writeCharacteristic(reportChr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
|
||||
BluetoothGattCharacteristic chr = descriptor.getCharacteristic();
|
||||
//Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid());
|
||||
|
||||
if (chr.getUuid().equals(mInputCharacteristic)) {
|
||||
mHasEnabledNotifications = true;
|
||||
enableValveMode();
|
||||
}
|
||||
|
||||
finishCurrentGattOperation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
|
||||
//Log.v(TAG, "onReliableWriteCompleted status=" + status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
|
||||
//Log.v(TAG, "onReadRemoteRssi status=" + status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
|
||||
//Log.v(TAG, "onMtuChanged status=" + status);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////// Public API
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return mDeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVendorId() {
|
||||
// Valve Corporation
|
||||
final int VALVE_USB_VID = 0x28DE;
|
||||
return VALVE_USB_VID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProductId() {
|
||||
if (mProductId > 0) {
|
||||
// We've already set a product ID.
|
||||
return mProductId;
|
||||
}
|
||||
|
||||
if (mDevice.getName().startsWith("Steam Ctrl")) {
|
||||
// We're a newer Triton device
|
||||
mProductId = TRITON_BLE_PID;
|
||||
} else {
|
||||
// We're an OG Steam Controller
|
||||
mProductId = D0G_BLE2_PID;
|
||||
}
|
||||
|
||||
return mProductId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSerialNumber() {
|
||||
// This will be read later via feature report by Steam
|
||||
return "12345";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVersion() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturerName() {
|
||||
return "Valve Corporation";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProductName() {
|
||||
return "Steam Controller";
|
||||
}
|
||||
|
||||
@Override
|
||||
public UsbDevice getDevice() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean open() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int writeReport(byte[] report, boolean feature) {
|
||||
if (!isRegistered()) {
|
||||
Log.e(TAG, "Attempted writeReport before Steam Controller is registered!");
|
||||
if (mIsConnected) {
|
||||
probeService(this);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (feature) {
|
||||
// We need to skip the first byte, as that doesn't go over the air
|
||||
byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
|
||||
//Log.v(TAG, "writeFeatureReport " + HexDump.dumpHexString(actual_report));
|
||||
writeCharacteristic(reportCharacteristic, actual_report);
|
||||
return report.length;
|
||||
} else {
|
||||
// If we're an original-recipe Steam Controller we just write to the characteristic directly.
|
||||
if (getProductId() == D0G_BLE2_PID) {
|
||||
//Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report));
|
||||
writeCharacteristic(reportCharacteristic, report);
|
||||
return report.length;
|
||||
}
|
||||
|
||||
// If we're a Triton, we need to find the correct report characteristic.
|
||||
if (report.length > 0) {
|
||||
int reportId = report[0] & 0xFF;
|
||||
BluetoothGattCharacteristic targetedReportCharacteristic = mOutputReportChars.get(reportId);
|
||||
if (targetedReportCharacteristic != null) {
|
||||
byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
|
||||
//Log.v(TAG, "writeOutputReport 0x" + Integer.toString(reportId, 16) + " " + HexDump.dumpHexString(report));
|
||||
writeCharacteristic(targetedReportCharacteristic.getUuid(), actual_report);
|
||||
return report.length;
|
||||
} else {
|
||||
Log.w(TAG, "Got report write request for unknown report type 0x" + Integer.toString(reportId, 16));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readReport(byte[] report, boolean feature) {
|
||||
if (!isRegistered()) {
|
||||
Log.e(TAG, "Attempted readReport before Steam Controller is registered!");
|
||||
if (mIsConnected) {
|
||||
probeService(this);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (feature) {
|
||||
readCharacteristic(reportCharacteristic);
|
||||
return true;
|
||||
} else {
|
||||
// Not implemented
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFrozen(boolean frozen) {
|
||||
mFrozen = frozen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
close();
|
||||
|
||||
BluetoothGatt g = mGatt;
|
||||
if (g != null) {
|
||||
g.disconnect();
|
||||
g.close();
|
||||
mGatt = null;
|
||||
}
|
||||
mManager = null;
|
||||
mIsRegistered = false;
|
||||
mIsConnected = false;
|
||||
mOperations.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,698 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.PendingIntent;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothManager;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.hardware.usb.*;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class HIDDeviceManager {
|
||||
private static final String TAG = "hidapi";
|
||||
private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION";
|
||||
|
||||
private static HIDDeviceManager sManager;
|
||||
private static int sManagerRefCount = 0;
|
||||
|
||||
static public HIDDeviceManager acquire(Context context) {
|
||||
if (sManagerRefCount == 0) {
|
||||
sManager = new HIDDeviceManager(context);
|
||||
}
|
||||
++sManagerRefCount;
|
||||
return sManager;
|
||||
}
|
||||
|
||||
static public void release(HIDDeviceManager manager) {
|
||||
if (manager == sManager) {
|
||||
--sManagerRefCount;
|
||||
if (sManagerRefCount == 0) {
|
||||
sManager.close();
|
||||
sManager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Context mContext;
|
||||
private HashMap<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>();
|
||||
private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>();
|
||||
private int mNextDeviceId = 0;
|
||||
private SharedPreferences mSharedPreferences = null;
|
||||
private boolean mIsChromebook = false;
|
||||
private UsbManager mUsbManager;
|
||||
private Handler mHandler;
|
||||
private BluetoothManager mBluetoothManager;
|
||||
private List<BluetoothDevice> mLastBluetoothDevices;
|
||||
|
||||
private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
||||
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
handleUsbDeviceAttached(usbDevice);
|
||||
} else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
|
||||
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
handleUsbDeviceDetached(usbDevice);
|
||||
} else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) {
|
||||
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
// Bluetooth device was connected. If it was a Steam Controller, handle it
|
||||
if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
|
||||
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
Log.d(TAG, "Bluetooth device connected: " + device);
|
||||
|
||||
if (isSteamController(device)) {
|
||||
connectBluetoothDevice(device);
|
||||
}
|
||||
}
|
||||
|
||||
// Bluetooth device was disconnected, remove from controller manager (if any)
|
||||
if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
|
||||
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
Log.d(TAG, "Bluetooth device disconnected: " + device);
|
||||
|
||||
disconnectBluetoothDevice(device);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private HIDDeviceManager(final Context context) {
|
||||
mContext = context;
|
||||
|
||||
HIDDeviceRegisterCallback();
|
||||
|
||||
mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE);
|
||||
mIsChromebook = SDLActivity.isChromebook();
|
||||
|
||||
// if (shouldClear) {
|
||||
// SharedPreferences.Editor spedit = mSharedPreferences.edit();
|
||||
// spedit.clear();
|
||||
// spedit.apply();
|
||||
// }
|
||||
// else
|
||||
{
|
||||
mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0);
|
||||
}
|
||||
}
|
||||
|
||||
Context getContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
int getDeviceIDForIdentifier(String identifier) {
|
||||
SharedPreferences.Editor spedit = mSharedPreferences.edit();
|
||||
|
||||
int result = mSharedPreferences.getInt(identifier, 0);
|
||||
if (result == 0) {
|
||||
result = mNextDeviceId++;
|
||||
spedit.putInt("next_device_id", mNextDeviceId);
|
||||
}
|
||||
|
||||
spedit.putInt(identifier, result);
|
||||
spedit.apply();
|
||||
return result;
|
||||
}
|
||||
|
||||
private void initializeUSB() {
|
||||
mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE);
|
||||
if (mUsbManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
// Logging
|
||||
for (UsbDevice device : mUsbManager.getDeviceList().values()) {
|
||||
Log.i(TAG,"Path: " + device.getDeviceName());
|
||||
Log.i(TAG,"Manufacturer: " + device.getManufacturerName());
|
||||
Log.i(TAG,"Product: " + device.getProductName());
|
||||
Log.i(TAG,"ID: " + device.getDeviceId());
|
||||
Log.i(TAG,"Class: " + device.getDeviceClass());
|
||||
Log.i(TAG,"Protocol: " + device.getDeviceProtocol());
|
||||
Log.i(TAG,"Vendor ID " + device.getVendorId());
|
||||
Log.i(TAG,"Product ID: " + device.getProductId());
|
||||
Log.i(TAG,"Interface count: " + device.getInterfaceCount());
|
||||
Log.i(TAG,"---------------------------------------");
|
||||
|
||||
// Get interface details
|
||||
for (int index = 0; index < device.getInterfaceCount(); index++) {
|
||||
UsbInterface mUsbInterface = device.getInterface(index);
|
||||
Log.i(TAG," ***** *****");
|
||||
Log.i(TAG," Interface index: " + index);
|
||||
Log.i(TAG," Interface ID: " + mUsbInterface.getId());
|
||||
Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass());
|
||||
Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass());
|
||||
Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol());
|
||||
Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount());
|
||||
|
||||
// Get endpoint details
|
||||
for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++)
|
||||
{
|
||||
UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi);
|
||||
Log.i(TAG," ++++ ++++ ++++");
|
||||
Log.i(TAG," Endpoint index: " + epi);
|
||||
Log.i(TAG," Attributes: " + mEndpoint.getAttributes());
|
||||
Log.i(TAG," Direction: " + mEndpoint.getDirection());
|
||||
Log.i(TAG," Number: " + mEndpoint.getEndpointNumber());
|
||||
Log.i(TAG," Interval: " + mEndpoint.getInterval());
|
||||
Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize());
|
||||
Log.i(TAG," Type: " + mEndpoint.getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i(TAG," No more devices connected.");
|
||||
*/
|
||||
|
||||
// Register for USB broadcasts and permission completions
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
|
||||
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
|
||||
filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION);
|
||||
if (Build.VERSION.SDK_INT >= 33) { /* Android 13.0 (TIRAMISU) */
|
||||
mContext.registerReceiver(mUsbBroadcast, filter, Context.RECEIVER_EXPORTED);
|
||||
} else {
|
||||
mContext.registerReceiver(mUsbBroadcast, filter);
|
||||
}
|
||||
|
||||
for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
|
||||
handleUsbDeviceAttached(usbDevice);
|
||||
}
|
||||
}
|
||||
|
||||
UsbManager getUSBManager() {
|
||||
return mUsbManager;
|
||||
}
|
||||
|
||||
private void shutdownUSB() {
|
||||
try {
|
||||
mContext.unregisterReceiver(mUsbBroadcast);
|
||||
} catch (Exception e) {
|
||||
// We may not have registered, that's okay
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) {
|
||||
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
|
||||
return true;
|
||||
}
|
||||
if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) {
|
||||
final int XB360_IFACE_SUBCLASS = 93;
|
||||
final int XB360_IFACE_PROTOCOL = 1; // Wired
|
||||
final int XB360W_IFACE_PROTOCOL = 129; // Wireless
|
||||
final int[] SUPPORTED_VENDORS = {
|
||||
0x0079, // GPD Win 2
|
||||
0x044f, // Thrustmaster
|
||||
0x045e, // Microsoft
|
||||
0x046d, // Logitech
|
||||
0x056e, // Elecom
|
||||
0x06a3, // Saitek
|
||||
0x0738, // Mad Catz
|
||||
0x07ff, // Mad Catz
|
||||
0x0e6f, // PDP
|
||||
0x0f0d, // Hori
|
||||
0x1038, // SteelSeries
|
||||
0x11c9, // Nacon
|
||||
0x12ab, // Unknown
|
||||
0x1430, // RedOctane
|
||||
0x146b, // BigBen
|
||||
0x1532, // Razer Sabertooth
|
||||
0x15e4, // Numark
|
||||
0x162e, // Joytech
|
||||
0x1689, // Razer Onza
|
||||
0x1949, // Lab126, Inc.
|
||||
0x1bad, // Harmonix
|
||||
0x20d6, // PowerA
|
||||
0x24c6, // PowerA
|
||||
0x2c22, // Qanba
|
||||
0x2dc8, // 8BitDo
|
||||
0x3537, // GameSir
|
||||
0x37d7, // Flydigi
|
||||
0x9886, // ASTRO Gaming
|
||||
};
|
||||
|
||||
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||
usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
|
||||
(usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL ||
|
||||
usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) {
|
||||
int vendor_id = usbDevice.getVendorId();
|
||||
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||
if (vendor_id == supportedVid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) {
|
||||
final int XB1_IFACE_SUBCLASS = 71;
|
||||
final int XB1_IFACE_PROTOCOL = 208;
|
||||
final int[] SUPPORTED_VENDORS = {
|
||||
0x03f0, // HP
|
||||
0x044f, // Thrustmaster
|
||||
0x045e, // Microsoft
|
||||
0x0738, // Mad Catz
|
||||
0x0b05, // ASUS
|
||||
0x0e6f, // PDP
|
||||
0x0f0d, // Hori
|
||||
0x10f5, // Turtle Beach
|
||||
0x1532, // Razer Wildcat
|
||||
0x20d6, // PowerA
|
||||
0x24c6, // PowerA
|
||||
0x294b, // Snakebyte
|
||||
0x2dc8, // 8BitDo
|
||||
0x2e24, // Hyperkin
|
||||
0x2e95, // SCUF
|
||||
0x3285, // Nacon
|
||||
0x3537, // GameSir
|
||||
0x366c, // ByoWave
|
||||
};
|
||||
|
||||
if (usbInterface.getId() == 0 &&
|
||||
usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||
usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
|
||||
usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
|
||||
int vendor_id = usbDevice.getVendorId();
|
||||
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||
if (vendor_id == supportedVid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleUsbDeviceAttached(UsbDevice usbDevice) {
|
||||
connectHIDDeviceUSB(usbDevice);
|
||||
}
|
||||
|
||||
private void handleUsbDeviceDetached(UsbDevice usbDevice) {
|
||||
List<Integer> devices = new ArrayList<Integer>();
|
||||
for (HIDDevice device : mDevicesById.values()) {
|
||||
if (usbDevice.equals(device.getDevice())) {
|
||||
devices.add(device.getId());
|
||||
}
|
||||
}
|
||||
for (int id : devices) {
|
||||
HIDDevice device = mDevicesById.get(id);
|
||||
mDevicesById.remove(id);
|
||||
device.shutdown();
|
||||
HIDDeviceDisconnected(id);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) {
|
||||
for (HIDDevice device : mDevicesById.values()) {
|
||||
if (usbDevice.equals(device.getDevice())) {
|
||||
boolean opened = false;
|
||||
if (permission_granted) {
|
||||
opened = device.open();
|
||||
}
|
||||
HIDDeviceOpenResult(device.getId(), opened);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void connectHIDDeviceUSB(UsbDevice usbDevice) {
|
||||
synchronized (this) {
|
||||
int interface_mask = 0;
|
||||
for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) {
|
||||
UsbInterface usbInterface = usbDevice.getInterface(interface_index);
|
||||
if (isHIDDeviceInterface(usbDevice, usbInterface)) {
|
||||
// Check to see if we've already added this interface
|
||||
// This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive
|
||||
int interface_id = usbInterface.getId();
|
||||
if ((interface_mask & (1 << interface_id)) != 0) {
|
||||
continue;
|
||||
}
|
||||
interface_mask |= (1 << interface_id);
|
||||
|
||||
HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index);
|
||||
int id = device.getId();
|
||||
mDevicesById.put(id, device);
|
||||
HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol(), false, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeBluetooth() {
|
||||
Log.d(TAG, "Initializing Bluetooth");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 31 /* Android 12 */ &&
|
||||
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH_CONNECT");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT <= 30 /* Android 11.0 (R) */ &&
|
||||
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
|
||||
Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find bonded bluetooth controllers and create SteamControllers for them
|
||||
mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
|
||||
if (mBluetoothManager == null) {
|
||||
// This device doesn't support Bluetooth.
|
||||
return;
|
||||
}
|
||||
|
||||
BluetoothAdapter btAdapter = mBluetoothManager.getAdapter();
|
||||
if (btAdapter == null) {
|
||||
// This device has Bluetooth support in the codebase, but has no available adapters.
|
||||
return;
|
||||
}
|
||||
|
||||
// Get our bonded devices.
|
||||
for (BluetoothDevice device : btAdapter.getBondedDevices()) {
|
||||
|
||||
Log.d(TAG, "Bluetooth device available: " + device);
|
||||
if (isSteamController(device)) {
|
||||
connectBluetoothDevice(device);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// NOTE: These don't work on Chromebooks, to my undying dismay.
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
|
||||
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
|
||||
if (Build.VERSION.SDK_INT >= 33) { /* Android 13.0 (TIRAMISU) */
|
||||
mContext.registerReceiver(mBluetoothBroadcast, filter, Context.RECEIVER_EXPORTED);
|
||||
} else {
|
||||
mContext.registerReceiver(mBluetoothBroadcast, filter);
|
||||
}
|
||||
|
||||
if (mIsChromebook) {
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
mLastBluetoothDevices = new ArrayList<BluetoothDevice>();
|
||||
|
||||
// final HIDDeviceManager finalThis = this;
|
||||
// mHandler.postDelayed(new Runnable() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// finalThis.chromebookConnectionHandler();
|
||||
// }
|
||||
// }, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
private void shutdownBluetooth() {
|
||||
try {
|
||||
mContext.unregisterReceiver(mBluetoothBroadcast);
|
||||
} catch (Exception e) {
|
||||
// We may not have registered, that's okay
|
||||
}
|
||||
}
|
||||
|
||||
// Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly.
|
||||
// This function provides a sort of dummy version of that, watching for changes in the
|
||||
// connected devices and attempting to add controllers as things change.
|
||||
void chromebookConnectionHandler() {
|
||||
if (!mIsChromebook) {
|
||||
return;
|
||||
}
|
||||
|
||||
ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>();
|
||||
ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>();
|
||||
|
||||
List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
|
||||
|
||||
for (BluetoothDevice bluetoothDevice : currentConnected) {
|
||||
if (!mLastBluetoothDevices.contains(bluetoothDevice)) {
|
||||
connected.add(bluetoothDevice);
|
||||
}
|
||||
}
|
||||
for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) {
|
||||
if (!currentConnected.contains(bluetoothDevice)) {
|
||||
disconnected.add(bluetoothDevice);
|
||||
}
|
||||
}
|
||||
|
||||
mLastBluetoothDevices = currentConnected;
|
||||
|
||||
for (BluetoothDevice bluetoothDevice : disconnected) {
|
||||
disconnectBluetoothDevice(bluetoothDevice);
|
||||
}
|
||||
for (BluetoothDevice bluetoothDevice : connected) {
|
||||
connectBluetoothDevice(bluetoothDevice);
|
||||
}
|
||||
|
||||
final HIDDeviceManager finalThis = this;
|
||||
mHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
finalThis.chromebookConnectionHandler();
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) {
|
||||
Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice);
|
||||
synchronized (this) {
|
||||
if (mBluetoothDevices.containsKey(bluetoothDevice)) {
|
||||
Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect");
|
||||
|
||||
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
|
||||
device.reconnect();
|
||||
|
||||
return false;
|
||||
}
|
||||
HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice);
|
||||
int id = device.getId();
|
||||
mBluetoothDevices.put(bluetoothDevice, device);
|
||||
mDevicesById.put(id, device);
|
||||
|
||||
// The Steam Controller will mark itself connected once initialization is complete
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) {
|
||||
synchronized (this) {
|
||||
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
|
||||
if (device == null)
|
||||
return;
|
||||
|
||||
int id = device.getId();
|
||||
mBluetoothDevices.remove(bluetoothDevice);
|
||||
mDevicesById.remove(id);
|
||||
device.shutdown();
|
||||
HIDDeviceDisconnected(id);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isSteamController(BluetoothDevice bluetoothDevice) {
|
||||
// Sanity check. If you pass in a null device, by definition it is never a Steam Controller.
|
||||
if (bluetoothDevice == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the device has no local name, we really don't want to try an equality check against it.
|
||||
if (bluetoothDevice.getName() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Steam Controllers will always support Bluetooth Low Energy
|
||||
if ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Match on the name either the original Steam Controller or the new second-generation one advertise with.
|
||||
return bluetoothDevice.getName().equals("SteamController") || bluetoothDevice.getName().startsWith("Steam Ctrl");
|
||||
}
|
||||
|
||||
private void close() {
|
||||
shutdownUSB();
|
||||
shutdownBluetooth();
|
||||
synchronized (this) {
|
||||
for (HIDDevice device : mDevicesById.values()) {
|
||||
device.shutdown();
|
||||
}
|
||||
mDevicesById.clear();
|
||||
mBluetoothDevices.clear();
|
||||
HIDDeviceReleaseCallback();
|
||||
}
|
||||
}
|
||||
|
||||
public void setFrozen(boolean frozen) {
|
||||
synchronized (this) {
|
||||
for (HIDDevice device : mDevicesById.values()) {
|
||||
device.setFrozen(frozen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private HIDDevice getDevice(int id) {
|
||||
synchronized (this) {
|
||||
HIDDevice result = mDevicesById.get(id);
|
||||
if (result == null) {
|
||||
Log.v(TAG, "No device for id: " + id);
|
||||
Log.v(TAG, "Available devices: " + mDevicesById.keySet());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////// JNI interface functions
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
boolean initialize(boolean usb, boolean bluetooth) {
|
||||
Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")");
|
||||
|
||||
if (usb) {
|
||||
initializeUSB();
|
||||
}
|
||||
if (bluetooth) {
|
||||
initializeBluetooth();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean openDevice(int deviceID) {
|
||||
Log.v(TAG, "openDevice deviceID=" + deviceID);
|
||||
HIDDevice device = getDevice(deviceID);
|
||||
if (device == null) {
|
||||
HIDDeviceDisconnected(deviceID);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Look to see if this is a USB device and we have permission to access it
|
||||
UsbDevice usbDevice = device.getDevice();
|
||||
if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) {
|
||||
HIDDeviceOpenPending(deviceID);
|
||||
try {
|
||||
final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31
|
||||
int flags;
|
||||
if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
|
||||
flags = FLAG_MUTABLE;
|
||||
} else {
|
||||
flags = 0;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(HIDDeviceManager.ACTION_USB_PERMISSION);
|
||||
intent.setPackage(mContext.getPackageName());
|
||||
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, intent, flags));
|
||||
} catch (Exception e) {
|
||||
Log.v(TAG, "Couldn't request permission for USB device " + usbDevice);
|
||||
HIDDeviceOpenResult(deviceID, false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return device.open();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int writeReport(int deviceID, byte[] report, boolean feature) {
|
||||
try {
|
||||
//Log.v(TAG, "writeReport deviceID=" + deviceID + " length=" + report.length);
|
||||
HIDDevice device;
|
||||
device = getDevice(deviceID);
|
||||
if (device == null) {
|
||||
HIDDeviceDisconnected(deviceID);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return device.writeReport(report, feature);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
boolean readReport(int deviceID, byte[] report, boolean feature) {
|
||||
try {
|
||||
//Log.v(TAG, "readReport deviceID=" + deviceID);
|
||||
HIDDevice device;
|
||||
device = getDevice(deviceID);
|
||||
if (device == null) {
|
||||
HIDDeviceDisconnected(deviceID);
|
||||
return false;
|
||||
}
|
||||
|
||||
return device.readReport(report, feature);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void closeDevice(int deviceID) {
|
||||
try {
|
||||
Log.v(TAG, "closeDevice deviceID=" + deviceID);
|
||||
HIDDevice device;
|
||||
device = getDevice(deviceID);
|
||||
if (device == null) {
|
||||
HIDDeviceDisconnected(deviceID);
|
||||
return;
|
||||
}
|
||||
|
||||
device.close();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////// Native methods
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private native void HIDDeviceRegisterCallback();
|
||||
private native void HIDDeviceReleaseCallback();
|
||||
|
||||
native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol, boolean bBluetooth, int reportID);
|
||||
native void HIDDeviceOpenPending(int deviceID);
|
||||
native void HIDDeviceOpenResult(int deviceID, boolean opened);
|
||||
native void HIDDeviceDisconnected(int deviceID);
|
||||
|
||||
native void HIDDeviceInputReport(int deviceID, byte[] report);
|
||||
native void HIDDeviceReportResponse(int deviceID, byte[] report);
|
||||
}
|
||||
@@ -1,354 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.hardware.usb.*;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
class HIDDeviceUSB implements HIDDevice {
|
||||
|
||||
private static final String TAG = "hidapi";
|
||||
|
||||
protected HIDDeviceManager mManager;
|
||||
protected UsbDevice mDevice;
|
||||
protected int mInterfaceIndex;
|
||||
protected int mInterface;
|
||||
protected int mDeviceId;
|
||||
protected UsbDeviceConnection mConnection;
|
||||
protected UsbEndpoint mInputEndpoint;
|
||||
protected UsbEndpoint mOutputEndpoint;
|
||||
protected InputThread mInputThread;
|
||||
protected boolean mRunning;
|
||||
protected boolean mFrozen;
|
||||
protected boolean mClaimed;
|
||||
|
||||
public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) {
|
||||
mManager = manager;
|
||||
mDevice = usbDevice;
|
||||
mInterfaceIndex = interface_index;
|
||||
mInterface = mDevice.getInterface(mInterfaceIndex).getId();
|
||||
mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier());
|
||||
mRunning = false;
|
||||
mClaimed = false;
|
||||
}
|
||||
|
||||
String getIdentifier() {
|
||||
return String.format(Locale.ENGLISH, "%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return mDeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVendorId() {
|
||||
return mDevice.getVendorId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProductId() {
|
||||
return mDevice.getProductId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSerialNumber() {
|
||||
String result = null;
|
||||
try {
|
||||
result = mDevice.getSerialNumber();
|
||||
}
|
||||
catch (SecurityException exception) {
|
||||
//Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage());
|
||||
}
|
||||
if (result == null) {
|
||||
result = "";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVersion() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturerName() {
|
||||
String result;
|
||||
result = mDevice.getManufacturerName();
|
||||
if (result == null) {
|
||||
result = String.format("%x", getVendorId());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProductName() {
|
||||
String result;
|
||||
result = mDevice.getProductName();
|
||||
if (result == null) {
|
||||
result = String.format("%x", getProductId());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UsbDevice getDevice() {
|
||||
return mDevice;
|
||||
}
|
||||
|
||||
String getDeviceName() {
|
||||
return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean open() {
|
||||
mConnection = mManager.getUSBManager().openDevice(mDevice);
|
||||
if (mConnection == null) {
|
||||
Log.w(TAG, "Unable to open USB device " + getDeviceName());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Force claim our interface
|
||||
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
|
||||
if (!mConnection.claimInterface(iface, true)) {
|
||||
Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName());
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
mClaimed = true;
|
||||
|
||||
// Find the endpoints
|
||||
for (int j = 0; j < iface.getEndpointCount(); j++) {
|
||||
UsbEndpoint endpt = iface.getEndpoint(j);
|
||||
switch (endpt.getDirection()) {
|
||||
case UsbConstants.USB_DIR_IN:
|
||||
if (mInputEndpoint == null) {
|
||||
mInputEndpoint = endpt;
|
||||
}
|
||||
break;
|
||||
case UsbConstants.USB_DIR_OUT:
|
||||
if (mOutputEndpoint == null) {
|
||||
mOutputEndpoint = endpt;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the required endpoints were present. The original Steam Controller and the wireless dongle for it do NOT
|
||||
// actually have -- or require -- output endpoints, so we need to accept only an input one for them or else we'll fall
|
||||
// back to the Android system gamepad functionality (and lose our paddles et al).
|
||||
if (mInputEndpoint == null) {
|
||||
Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName());
|
||||
mConnection.releaseInterface(iface);
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start listening for input
|
||||
mRunning = true;
|
||||
mInputThread = new InputThread();
|
||||
mInputThread.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int writeReport(byte[] report, boolean feature) {
|
||||
if (mConnection == null) {
|
||||
Log.w(TAG, "writeReport() called with no device connection");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!mClaimed) {
|
||||
Log.w(TAG, "writeReport() called but some other process currently owns the USB device");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (feature) {
|
||||
int res = -1;
|
||||
int offset = 0;
|
||||
int length = report.length;
|
||||
boolean skipped_report_id = false;
|
||||
byte report_number = report[0];
|
||||
|
||||
if (report_number == 0x0) {
|
||||
++offset;
|
||||
--length;
|
||||
skipped_report_id = true;
|
||||
}
|
||||
|
||||
res = mConnection.controlTransfer(
|
||||
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT,
|
||||
0x09/*HID set_report*/,
|
||||
(3/*HID feature*/ << 8) | report_number,
|
||||
mInterface,
|
||||
report, offset, length,
|
||||
1000/*timeout millis*/);
|
||||
|
||||
if (res < 0) {
|
||||
Log.w(TAG, "writeFeatureReport() returned " + res + " on device " + getDeviceName());
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (skipped_report_id) {
|
||||
++length;
|
||||
}
|
||||
return length;
|
||||
} else {
|
||||
if (mOutputEndpoint == null)
|
||||
{
|
||||
Log.e(TAG, "Tried to write an output report to an interface with no output endpoint!");
|
||||
return -1;
|
||||
}
|
||||
int res = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000);
|
||||
if (res != report.length) {
|
||||
Log.w(TAG, "writeOutputReport() returned " + res + " on device " + getDeviceName());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readReport(byte[] report, boolean feature) {
|
||||
int res = -1;
|
||||
int offset = 0;
|
||||
int length = report.length;
|
||||
boolean skipped_report_id = false;
|
||||
byte report_number = report[0];
|
||||
|
||||
if (mConnection == null) {
|
||||
Log.w(TAG, "readReport() called with no device connection");
|
||||
return false;
|
||||
}
|
||||
if (!mClaimed) {
|
||||
if (feature) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (report_number == 0x0) {
|
||||
/* Offset the return buffer by 1, so that the report ID
|
||||
will remain in byte 0. */
|
||||
++offset;
|
||||
--length;
|
||||
skipped_report_id = true;
|
||||
}
|
||||
|
||||
res = mConnection.controlTransfer(
|
||||
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN,
|
||||
0x01/*HID get_report*/,
|
||||
((feature ? 3/*HID feature*/ : 1/*HID Input*/) << 8) | report_number,
|
||||
mInterface,
|
||||
report, offset, length,
|
||||
1000/*timeout millis*/);
|
||||
|
||||
if (res < 0) {
|
||||
Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (skipped_report_id) {
|
||||
++res;
|
||||
++length;
|
||||
}
|
||||
|
||||
byte[] data;
|
||||
if (res == length) {
|
||||
data = report;
|
||||
} else {
|
||||
data = Arrays.copyOfRange(report, 0, res);
|
||||
}
|
||||
mManager.HIDDeviceReportResponse(mDeviceId, data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
mRunning = false;
|
||||
if (mInputThread != null) {
|
||||
while (mInputThread.isAlive()) {
|
||||
mInputThread.interrupt();
|
||||
try {
|
||||
mInputThread.join();
|
||||
} catch (InterruptedException e) {
|
||||
// Keep trying until we're done
|
||||
}
|
||||
}
|
||||
mInputThread = null;
|
||||
}
|
||||
if (mConnection != null) {
|
||||
if (mClaimed) {
|
||||
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
|
||||
mConnection.releaseInterface(iface);
|
||||
}
|
||||
mConnection.close();
|
||||
mConnection = null;
|
||||
mClaimed = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
close();
|
||||
mManager = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFrozen(boolean frozen) {
|
||||
mFrozen = frozen;
|
||||
|
||||
/* If we have a valid device connection and the claim state doesn't match what we want, try to correct that. */
|
||||
if (mConnection != null && mClaimed == mFrozen) {
|
||||
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
|
||||
if (frozen) {
|
||||
mClaimed = !mConnection.releaseInterface(iface);
|
||||
if (mClaimed) {
|
||||
Log.e(TAG, "Tried to release claim on USB device, but failed!");
|
||||
}
|
||||
} else {
|
||||
mClaimed = mConnection.claimInterface(iface, true);
|
||||
if (!mClaimed) {
|
||||
Log.e(TAG, "Tried to regain claim on USB device, but failed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected class InputThread extends Thread {
|
||||
@Override
|
||||
public void run() {
|
||||
int packetSize = mInputEndpoint.getMaxPacketSize();
|
||||
byte[] packet = new byte[packetSize];
|
||||
while (mRunning) {
|
||||
int r;
|
||||
try
|
||||
{
|
||||
r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e);
|
||||
break;
|
||||
}
|
||||
if (r < 0) {
|
||||
// Could be a timeout or an I/O error
|
||||
}
|
||||
if (r > 0) {
|
||||
byte[] data;
|
||||
if (r == packetSize) {
|
||||
data = packet;
|
||||
} else {
|
||||
data = Arrays.copyOfRange(packet, 0, r);
|
||||
}
|
||||
|
||||
if (!mFrozen) {
|
||||
mManager.HIDDeviceInputReport(mDeviceId, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
SDL library initialization
|
||||
*/
|
||||
public class SDL {
|
||||
|
||||
// This function should be called first and sets up the native code
|
||||
// so it can call into the Java classes
|
||||
static public void setupJNI() {
|
||||
SDLActivity.nativeSetupJNI();
|
||||
SDLAudioManager.nativeSetupJNI();
|
||||
SDLControllerManager.nativeSetupJNI();
|
||||
}
|
||||
|
||||
// This function should be called each time the activity is started
|
||||
static public void initialize() {
|
||||
setContext(null);
|
||||
|
||||
SDLActivity.initialize();
|
||||
SDLAudioManager.initialize();
|
||||
SDLControllerManager.initialize();
|
||||
}
|
||||
|
||||
// This function stores the current activity (SDL or not)
|
||||
static public void setContext(Activity context) {
|
||||
SDLAudioManager.setContext(context);
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
static public Activity getContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
|
||||
loadLibrary(libraryName, mContext);
|
||||
}
|
||||
|
||||
static void loadLibrary(String libraryName, Context context) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
|
||||
|
||||
if (libraryName == null) {
|
||||
throw new NullPointerException("No library name provided.");
|
||||
}
|
||||
|
||||
try {
|
||||
// Let's see if we have ReLinker available in the project. This is necessary for
|
||||
// some projects that have huge numbers of local libraries bundled, and thus may
|
||||
// trip a bug in Android's native library loader which ReLinker works around. (If
|
||||
// loadLibrary works properly, ReLinker will simply use the normal Android method
|
||||
// internally.)
|
||||
//
|
||||
// To use ReLinker, just add it as a dependency. For more information, see
|
||||
// https://github.com/KeepSafe/ReLinker for ReLinker's repository.
|
||||
//
|
||||
Class<?> relinkClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
|
||||
Class<?> relinkListenerClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
|
||||
Class<?> contextClass = context.getClassLoader().loadClass("android.content.Context");
|
||||
Class<?> stringClass = context.getClassLoader().loadClass("java.lang.String");
|
||||
|
||||
// Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if
|
||||
// they've changed during updates.
|
||||
Method forceMethod = relinkClass.getDeclaredMethod("force");
|
||||
Object relinkInstance = forceMethod.invoke(null);
|
||||
Class<?> relinkInstanceClass = relinkInstance.getClass();
|
||||
|
||||
// Actually load the library!
|
||||
Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass);
|
||||
loadMethod.invoke(relinkInstance, context, libraryName, null, null);
|
||||
}
|
||||
catch (final Throwable e) {
|
||||
// Fall back
|
||||
try {
|
||||
System.loadLibrary(libraryName);
|
||||
}
|
||||
catch (final UnsatisfiedLinkError ule) {
|
||||
throw ule;
|
||||
}
|
||||
catch (final SecurityException se) {
|
||||
throw se;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static Activity mContext;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,126 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioDeviceCallback;
|
||||
import android.media.AudioDeviceInfo;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
|
||||
class SDLAudioManager {
|
||||
protected static final String TAG = "SDLAudio";
|
||||
|
||||
protected static Context mContext;
|
||||
|
||||
private static AudioDeviceCallback mAudioDeviceCallback;
|
||||
|
||||
static void initialize() {
|
||||
mAudioDeviceCallback = null;
|
||||
|
||||
if(Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */)
|
||||
{
|
||||
mAudioDeviceCallback = new AudioDeviceCallback() {
|
||||
@Override
|
||||
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
|
||||
for (AudioDeviceInfo deviceInfo : addedDevices) {
|
||||
nativeAddAudioDevice(deviceInfo.isSink(), deviceInfo.getProductName().toString(), deviceInfo.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
|
||||
for (AudioDeviceInfo deviceInfo : removedDevices) {
|
||||
nativeRemoveAudioDevice(deviceInfo.isSink(), deviceInfo.getId());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static void setContext(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
static void release(Context context) {
|
||||
// no-op atm
|
||||
}
|
||||
|
||||
// Audio
|
||||
|
||||
private static AudioDeviceInfo getInputAudioDeviceInfo(int deviceId) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) {
|
||||
if (deviceInfo.getId() == deviceId) {
|
||||
return deviceInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static AudioDeviceInfo getPlaybackAudioDeviceInfo(int deviceId) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
|
||||
if (deviceInfo.getId() == deviceId) {
|
||||
return deviceInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static void registerAudioDeviceCallback() {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
// get an initial list now, before hotplug callbacks fire.
|
||||
for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
|
||||
if (dev.getType() == AudioDeviceInfo.TYPE_TELEPHONY) {
|
||||
continue; // Device cannot be opened
|
||||
}
|
||||
nativeAddAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId());
|
||||
}
|
||||
for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) {
|
||||
nativeAddAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId());
|
||||
}
|
||||
audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null);
|
||||
}
|
||||
}
|
||||
|
||||
static void unregisterAudioDeviceCallback() {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback);
|
||||
}
|
||||
}
|
||||
|
||||
/** This method is called by SDL using JNI. */
|
||||
static void audioSetThreadPriority(boolean recording, int device_id) {
|
||||
try {
|
||||
|
||||
/* Set thread name */
|
||||
if (recording) {
|
||||
Thread.currentThread().setName("SDLAudioC" + device_id);
|
||||
} else {
|
||||
Thread.currentThread().setName("SDLAudioP" + device_id);
|
||||
}
|
||||
|
||||
/* Set thread priority */
|
||||
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.v(TAG, "modify thread properties failed " + e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
static native void nativeSetupJNI();
|
||||
|
||||
static native void nativeRemoveAudioDevice(boolean recording, int deviceId);
|
||||
|
||||
static native void nativeAddAudioDevice(boolean recording, String name, int deviceId);
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,66 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.content.*;
|
||||
import android.text.InputType;
|
||||
import android.view.*;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
/* This is a fake invisible editor view that receives the input and defines the
|
||||
* pan&scan region
|
||||
*/
|
||||
public class SDLDummyEdit extends View implements View.OnKeyListener
|
||||
{
|
||||
InputConnection ic;
|
||||
int input_type;
|
||||
|
||||
SDLDummyEdit(Context context) {
|
||||
super(context);
|
||||
setFocusableInTouchMode(true);
|
||||
setFocusable(true);
|
||||
setOnKeyListener(this);
|
||||
}
|
||||
|
||||
void setInputType(int input_type) {
|
||||
this.input_type = input_type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCheckIsTextEditor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
return SDLActivity.handleKeyEvent(v, keyCode, event, ic);
|
||||
}
|
||||
|
||||
//
|
||||
@Override
|
||||
public boolean onKeyPreIme (int keyCode, KeyEvent event) {
|
||||
// As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event
|
||||
// FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639
|
||||
// FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not
|
||||
// FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout
|
||||
// FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
|
||||
// FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :)
|
||||
if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) {
|
||||
SDLActivity.onNativeKeyboardFocusLost();
|
||||
}
|
||||
}
|
||||
return super.onKeyPreIme(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
||||
ic = new SDLInputConnection(this, true);
|
||||
|
||||
outAttrs.inputType = input_type;
|
||||
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI |
|
||||
EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */;
|
||||
|
||||
return ic;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.content.*;
|
||||
import android.os.Build;
|
||||
import android.text.Editable;
|
||||
import android.view.*;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
import android.widget.EditText;
|
||||
|
||||
class SDLInputConnection extends BaseInputConnection
|
||||
{
|
||||
protected EditText mEditText;
|
||||
protected String mCommittedText = "";
|
||||
|
||||
SDLInputConnection(View targetView, boolean fullEditor) {
|
||||
super(targetView, fullEditor);
|
||||
mEditText = new EditText(SDL.getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editable getEditable() {
|
||||
return mEditText.getEditableText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sendKeyEvent(KeyEvent event) {
|
||||
/*
|
||||
* This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard)
|
||||
* However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses
|
||||
* and so we need to generate them ourselves in commitText. To avoid duplicates on the handful of keys
|
||||
* that still do, we empty this out.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Return DOES still generate a key event, however. So rather than using it as the 'click a button' key
|
||||
* as we do with physical keyboards, let's just use it to hide the keyboard.
|
||||
*/
|
||||
|
||||
if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
|
||||
if (SDLActivity.onNativeSoftReturnKey()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return super.sendKeyEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commitText(CharSequence text, int newCursorPosition) {
|
||||
if (!super.commitText(text, newCursorPosition)) {
|
||||
return false;
|
||||
}
|
||||
updateText();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setComposingText(CharSequence text, int newCursorPosition) {
|
||||
if (!super.setComposingText(text, newCursorPosition)) {
|
||||
return false;
|
||||
}
|
||||
updateText();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
|
||||
// Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection
|
||||
// and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
|
||||
if (beforeLength > 0 && afterLength == 0) {
|
||||
// backspace(s)
|
||||
while (beforeLength-- > 0) {
|
||||
nativeGenerateScancodeForUnichar('\b');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!super.deleteSurroundingText(beforeLength, afterLength)) {
|
||||
return false;
|
||||
}
|
||||
updateText();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void updateText() {
|
||||
final Editable content = getEditable();
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String text = content.toString();
|
||||
int compareLength = Math.min(text.length(), mCommittedText.length());
|
||||
int matchLength, offset;
|
||||
|
||||
/* Backspace over characters that are no longer in the string */
|
||||
for (matchLength = 0; matchLength < compareLength; ) {
|
||||
int codePoint = mCommittedText.codePointAt(matchLength);
|
||||
if (codePoint != text.codePointAt(matchLength)) {
|
||||
break;
|
||||
}
|
||||
matchLength += Character.charCount(codePoint);
|
||||
}
|
||||
/* FIXME: This doesn't handle graphemes, like '🌬️' */
|
||||
for (offset = matchLength; offset < mCommittedText.length(); ) {
|
||||
int codePoint = mCommittedText.codePointAt(offset);
|
||||
nativeGenerateScancodeForUnichar('\b');
|
||||
offset += Character.charCount(codePoint);
|
||||
}
|
||||
|
||||
if (matchLength < text.length()) {
|
||||
String pendingText = text.subSequence(matchLength, text.length()).toString();
|
||||
if (!SDLActivity.dispatchingKeyEvent()) {
|
||||
for (offset = 0; offset < pendingText.length(); ) {
|
||||
int codePoint = pendingText.codePointAt(offset);
|
||||
if (codePoint == '\n') {
|
||||
if (SDLActivity.onNativeSoftReturnKey()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* Higher code points don't generate simulated scancodes */
|
||||
if (codePoint > 0 && codePoint < 128) {
|
||||
nativeGenerateScancodeForUnichar((char)codePoint);
|
||||
}
|
||||
offset += Character.charCount(codePoint);
|
||||
}
|
||||
}
|
||||
SDLInputConnection.nativeCommitText(pendingText, 0);
|
||||
}
|
||||
mCommittedText = text;
|
||||
}
|
||||
|
||||
public static native void nativeCommitText(String text, int newCursorPosition);
|
||||
|
||||
public static native void nativeGenerateScancodeForUnichar(char c);
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
|
||||
// This class coordinates synchronized access to sensor manager registration
|
||||
//
|
||||
// This prevents a java.util.ConcurrentModificationException exception on
|
||||
// Android 16, specifically on the Samsung Tab S9 Ultra.
|
||||
|
||||
class SDLSensorManager
|
||||
{
|
||||
static private SDLSensorManager mManager = new SDLSensorManager();
|
||||
|
||||
public static void registerListener(SensorManager manager, SensorEventListener listener, Sensor sensor, int samplingPeriodUs) {
|
||||
mManager.RegisterListener(manager, listener, sensor, samplingPeriodUs);
|
||||
}
|
||||
|
||||
public static void unregisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor) {
|
||||
mManager.UnregisterListener(manager, listener, sensor);
|
||||
}
|
||||
|
||||
private synchronized void RegisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor, int samplingPeriodUs) {
|
||||
manager.registerListener(listener, sensor, samplingPeriodUs, null);
|
||||
}
|
||||
|
||||
private synchronized void UnregisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor) {
|
||||
manager.unregisterListener(listener, sensor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,469 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.graphics.Insets;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.os.Build;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.PointerIcon;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import android.view.ScaleGestureDetector;
|
||||
|
||||
/**
|
||||
SDLSurface. This is what we draw on, so we need to know when it's created
|
||||
in order to do anything useful.
|
||||
|
||||
Because of this, that's where we set up the SDL thread
|
||||
*/
|
||||
public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
|
||||
View.OnApplyWindowInsetsListener, View.OnKeyListener, View.OnTouchListener,
|
||||
SensorEventListener, ScaleGestureDetector.OnScaleGestureListener {
|
||||
|
||||
private static native void auroraNativeSetSurfaceReady(boolean ready);
|
||||
|
||||
// Sensors
|
||||
protected SensorManager mSensorManager;
|
||||
protected Display mDisplay;
|
||||
|
||||
// Keep track of the surface size to normalize touch events
|
||||
protected float mWidth, mHeight;
|
||||
|
||||
// Is SurfaceView ready for rendering
|
||||
protected boolean mIsSurfaceReady;
|
||||
|
||||
// Is on-screen keyboard visible
|
||||
protected boolean mKeyboardVisible;
|
||||
|
||||
// Pinch events
|
||||
private final ScaleGestureDetector scaleGestureDetector;
|
||||
|
||||
// Startup
|
||||
protected SDLSurface(Context context) {
|
||||
super(context);
|
||||
getHolder().addCallback(this);
|
||||
|
||||
scaleGestureDetector = new ScaleGestureDetector(context, this);
|
||||
|
||||
setFocusable(true);
|
||||
setFocusableInTouchMode(true);
|
||||
requestFocus();
|
||||
setOnApplyWindowInsetsListener(this);
|
||||
setOnKeyListener(this);
|
||||
setOnTouchListener(this);
|
||||
|
||||
mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
|
||||
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
|
||||
|
||||
setOnGenericMotionListener(SDLActivity.getMotionListener());
|
||||
|
||||
// Some arbitrary defaults to avoid a potential division by zero
|
||||
mWidth = 1.0f;
|
||||
mHeight = 1.0f;
|
||||
|
||||
mIsSurfaceReady = false;
|
||||
}
|
||||
|
||||
protected void handlePause() {
|
||||
enableSensor(Sensor.TYPE_ACCELEROMETER, false);
|
||||
}
|
||||
|
||||
protected void handleResume() {
|
||||
setFocusable(true);
|
||||
setFocusableInTouchMode(true);
|
||||
requestFocus();
|
||||
setOnApplyWindowInsetsListener(this);
|
||||
setOnKeyListener(this);
|
||||
setOnTouchListener(this);
|
||||
enableSensor(Sensor.TYPE_ACCELEROMETER, true);
|
||||
}
|
||||
|
||||
protected Surface getNativeSurface() {
|
||||
return getHolder().getSurface();
|
||||
}
|
||||
|
||||
// Called when we have a valid drawing surface
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
Log.v("SDL", "surfaceCreated()");
|
||||
auroraNativeSetSurfaceReady(false);
|
||||
SDLActivity.onNativeSurfaceCreated();
|
||||
}
|
||||
|
||||
// Called when we lose the surface
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
Log.v("SDL", "surfaceDestroyed()");
|
||||
auroraNativeSetSurfaceReady(false);
|
||||
|
||||
// Transition to pause, if needed
|
||||
SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
|
||||
SDLActivity.handleNativeState();
|
||||
|
||||
mIsSurfaceReady = false;
|
||||
SDLActivity.onNativeSurfaceDestroyed();
|
||||
}
|
||||
|
||||
// Called when the surface is resized
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder,
|
||||
int format, int width, int height) {
|
||||
Log.v("SDL", "surfaceChanged()");
|
||||
|
||||
if (SDLActivity.mSingleton == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
int nDeviceWidth = width;
|
||||
int nDeviceHeight = height;
|
||||
float density = 1.0f;
|
||||
try
|
||||
{
|
||||
DisplayMetrics realMetrics = new DisplayMetrics();
|
||||
mDisplay.getRealMetrics( realMetrics );
|
||||
nDeviceWidth = realMetrics.widthPixels;
|
||||
nDeviceHeight = realMetrics.heightPixels;
|
||||
// Use densityDpi instead of density to more closely match what the UI scale is
|
||||
density = (float)realMetrics.densityDpi / 160.0f;
|
||||
} catch(Exception ignored) {
|
||||
}
|
||||
|
||||
synchronized(SDLActivity.getContext()) {
|
||||
// In case we're waiting on a size change after going fullscreen, send a notification.
|
||||
SDLActivity.getContext().notifyAll();
|
||||
}
|
||||
|
||||
Log.v("SDL", "Window size: " + width + "x" + height);
|
||||
Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
|
||||
SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, density, mDisplay.getRefreshRate());
|
||||
SDLActivity.onNativeResize();
|
||||
|
||||
// Prevent a screen distortion glitch,
|
||||
// for instance when the device is in Landscape and a Portrait App is resumed.
|
||||
boolean skip = false;
|
||||
int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
|
||||
|
||||
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
|
||||
if (mWidth > mHeight) {
|
||||
skip = true;
|
||||
}
|
||||
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
|
||||
if (mWidth < mHeight) {
|
||||
skip = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Special Patch for Square Resolution: Black Berry Passport
|
||||
if (skip) {
|
||||
double min = Math.min(mWidth, mHeight);
|
||||
double max = Math.max(mWidth, mHeight);
|
||||
|
||||
if (max / min < 1.20) {
|
||||
Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
|
||||
skip = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't skip if we might be multi-window or have popup dialogs
|
||||
if (skip) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
skip = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (skip) {
|
||||
Log.v("SDL", "Skip .. Surface is not ready.");
|
||||
mIsSurfaceReady = false;
|
||||
return;
|
||||
}
|
||||
|
||||
/* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
|
||||
SDLActivity.onNativeSurfaceChanged();
|
||||
|
||||
/* Surface is ready */
|
||||
mIsSurfaceReady = true;
|
||||
auroraNativeSetSurfaceReady(true);
|
||||
|
||||
SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
|
||||
SDLActivity.handleNativeState();
|
||||
}
|
||||
|
||||
// Window inset
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
if (Build.VERSION.SDK_INT >= 30 /* Android 11 (R) */) {
|
||||
Insets combined = insets.getInsets(WindowInsets.Type.systemBars() |
|
||||
WindowInsets.Type.systemGestures() |
|
||||
WindowInsets.Type.mandatorySystemGestures() |
|
||||
WindowInsets.Type.tappableElement() |
|
||||
WindowInsets.Type.displayCutout());
|
||||
|
||||
SDLActivity.onNativeInsetsChanged(combined.left, combined.right, combined.top, combined.bottom);
|
||||
|
||||
if (insets.isVisible(WindowInsets.Type.ime())) {
|
||||
if (!mKeyboardVisible) {
|
||||
mKeyboardVisible = true;
|
||||
SDLActivity.onNativeScreenKeyboardShown();
|
||||
}
|
||||
} else {
|
||||
if (mKeyboardVisible) {
|
||||
mKeyboardVisible = false;
|
||||
SDLActivity.onNativeScreenKeyboardHidden();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass these to any child views in case they need them
|
||||
return insets;
|
||||
}
|
||||
|
||||
// Key events
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
return SDLActivity.handleKeyEvent(v, keyCode, event, null);
|
||||
}
|
||||
|
||||
private float getNormalizedX(float x)
|
||||
{
|
||||
if (mWidth <= 1) {
|
||||
return 0.5f;
|
||||
} else {
|
||||
return (x / (mWidth - 1));
|
||||
}
|
||||
}
|
||||
|
||||
private float getNormalizedY(float y)
|
||||
{
|
||||
if (mHeight <= 1) {
|
||||
return 0.5f;
|
||||
} else {
|
||||
return (y / (mHeight - 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Touch events
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
/* Ref: http://developer.android.com/training/gestures/multi.html */
|
||||
int touchDevId = event.getDeviceId();
|
||||
final int pointerCount = event.getPointerCount();
|
||||
int action = event.getActionMasked();
|
||||
int pointerId;
|
||||
int i = 0;
|
||||
float x,y,p;
|
||||
|
||||
if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN)
|
||||
i = event.getActionIndex();
|
||||
|
||||
do {
|
||||
int toolType = event.getToolType(i);
|
||||
|
||||
if (toolType == MotionEvent.TOOL_TYPE_MOUSE) {
|
||||
int buttonState = event.getButtonState();
|
||||
boolean relative = false;
|
||||
|
||||
// We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
|
||||
// if we are. We'll leverage our existing mouse motion listener
|
||||
SDLGenericMotionListener_API14 motionListener = SDLActivity.getMotionListener();
|
||||
x = motionListener.getEventX(event, i);
|
||||
y = motionListener.getEventY(event, i);
|
||||
relative = motionListener.inRelativeMode();
|
||||
|
||||
SDLActivity.onNativeMouse(buttonState, action, x, y, relative);
|
||||
} else if (toolType == MotionEvent.TOOL_TYPE_STYLUS || toolType == MotionEvent.TOOL_TYPE_ERASER) {
|
||||
pointerId = event.getPointerId(i);
|
||||
x = event.getX(i);
|
||||
y = event.getY(i);
|
||||
p = event.getPressure(i);
|
||||
if (p > 1.0f) {
|
||||
// may be larger than 1.0f on some devices
|
||||
// see the documentation of getPressure(i)
|
||||
p = 1.0f;
|
||||
}
|
||||
|
||||
// BUTTON_STYLUS_PRIMARY is 2^5, so shift by 4, and apply SDL_PEN_INPUT_DOWN/SDL_PEN_INPUT_ERASER_TIP
|
||||
int buttonState = (event.getButtonState() >> 4) | (1 << (toolType == MotionEvent.TOOL_TYPE_STYLUS ? 0 : 30));
|
||||
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
|
||||
buttonState |= 0x08;
|
||||
}
|
||||
|
||||
SDLActivity.onNativePen(pointerId, SDLActivity.getMotionListener().getPenDeviceType(event.getDevice()), buttonState, action, x, y, p);
|
||||
} else { // MotionEvent.TOOL_TYPE_FINGER or MotionEvent.TOOL_TYPE_UNKNOWN
|
||||
pointerId = event.getPointerId(i);
|
||||
x = getNormalizedX(event.getX(i));
|
||||
y = getNormalizedY(event.getY(i));
|
||||
p = event.getPressure(i);
|
||||
if (p > 1.0f) {
|
||||
// may be larger than 1.0f on some devices
|
||||
// see the documentation of getPressure(i)
|
||||
p = 1.0f;
|
||||
}
|
||||
|
||||
SDLActivity.onNativeTouch(touchDevId, pointerId, action, x, y, p);
|
||||
}
|
||||
|
||||
// Non-primary up/down
|
||||
if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN)
|
||||
break;
|
||||
} while (++i < pointerCount);
|
||||
|
||||
scaleGestureDetector.onTouchEvent(event);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sensor events
|
||||
protected void enableSensor(int sensortype, boolean enabled) {
|
||||
// TODO: This uses getDefaultSensor - what if we have >1 accels?
|
||||
if (enabled) {
|
||||
SDLSensorManager.registerListener(mSensorManager, this,
|
||||
mSensorManager.getDefaultSensor(sensortype),
|
||||
SensorManager.SENSOR_DELAY_GAME);
|
||||
} else {
|
||||
SDLSensorManager.unregisterListener(mSensorManager, this,
|
||||
mSensorManager.getDefaultSensor(sensortype));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
|
||||
|
||||
// Since we may have an orientation set, we won't receive onConfigurationChanged events.
|
||||
// We thus should check here.
|
||||
int newRotation;
|
||||
|
||||
float x, y;
|
||||
switch (mDisplay.getRotation()) {
|
||||
case Surface.ROTATION_0:
|
||||
default:
|
||||
x = event.values[0];
|
||||
y = event.values[1];
|
||||
newRotation = 0;
|
||||
break;
|
||||
case Surface.ROTATION_90:
|
||||
x = -event.values[1];
|
||||
y = event.values[0];
|
||||
newRotation = 90;
|
||||
break;
|
||||
case Surface.ROTATION_180:
|
||||
x = -event.values[0];
|
||||
y = -event.values[1];
|
||||
newRotation = 180;
|
||||
break;
|
||||
case Surface.ROTATION_270:
|
||||
x = event.values[1];
|
||||
y = -event.values[0];
|
||||
newRotation = 270;
|
||||
break;
|
||||
}
|
||||
|
||||
if (newRotation != SDLActivity.mCurrentRotation) {
|
||||
SDLActivity.mCurrentRotation = newRotation;
|
||||
SDLActivity.onNativeRotationChanged(newRotation);
|
||||
}
|
||||
|
||||
SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
|
||||
y / SensorManager.GRAVITY_EARTH,
|
||||
event.values[2] / SensorManager.GRAVITY_EARTH);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent android internal NullPointerException (https://github.com/libsdl-org/SDL/issues/13306)
|
||||
@Override
|
||||
public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
|
||||
try {
|
||||
return super.onResolvePointerIcon(event, pointerIndex);
|
||||
} catch (NullPointerException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Captured pointer events for API 26.
|
||||
@Override
|
||||
public boolean onCapturedPointerEvent(MotionEvent event)
|
||||
{
|
||||
int action = event.getActionMasked();
|
||||
int pointerCount = event.getPointerCount();
|
||||
|
||||
for (int i = 0; i < pointerCount; i++) {
|
||||
float x, y;
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_SCROLL:
|
||||
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, i);
|
||||
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, i);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
x = event.getX(i);
|
||||
y = event.getY(i);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, true);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_BUTTON_PRESS:
|
||||
case MotionEvent.ACTION_BUTTON_RELEASE:
|
||||
|
||||
// Change our action value to what SDL's code expects.
|
||||
if (action == MotionEvent.ACTION_BUTTON_PRESS) {
|
||||
action = MotionEvent.ACTION_DOWN;
|
||||
} else { /* MotionEvent.ACTION_BUTTON_RELEASE */
|
||||
action = MotionEvent.ACTION_UP;
|
||||
}
|
||||
|
||||
x = event.getX(i);
|
||||
y = event.getY(i);
|
||||
int button = event.getButtonState();
|
||||
|
||||
SDLActivity.onNativeMouse(button, action, x, y, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector) {
|
||||
float scale = detector.getScaleFactor();
|
||||
SDLActivity.onNativePinchUpdate(scale);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScaleBegin(ScaleGestureDetector detector) {
|
||||
SDLActivity.onNativePinchStart();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScaleEnd(ScaleGestureDetector detector) {
|
||||
SDLActivity.onNativePinchEnd();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
APP_DIR="$ROOT_DIR/platforms/android/app/src/main/jniLibs"
|
||||
ANDROID_HOME_DIR="${ANDROID_HOME:-$HOME/Android/Sdk}"
|
||||
ANDROID_NDK_VER="${ANDROID_NDK_VERSION:-}"
|
||||
ANDROID_STAGE_ABIS="${ANDROID_STAGE_ABIS:-arm64-v8a x86_64}"
|
||||
ANDROID_STAGE_STRIP="${ANDROID_STAGE_STRIP:-1}"
|
||||
STRIP_TOOL=""
|
||||
|
||||
if [[ -z "$ANDROID_NDK_VER" ]] && [[ -d "$ANDROID_HOME_DIR/ndk" ]]; then
|
||||
ANDROID_NDK_VER="$(ls -1 "$ANDROID_HOME_DIR/ndk" | sort -V | tail -n 1)"
|
||||
fi
|
||||
|
||||
if [[ -n "$ANDROID_NDK_VER" ]]; then
|
||||
case "$(uname -s)" in
|
||||
Darwin) HOST_TAG="darwin-x86_64" ;;
|
||||
Linux) HOST_TAG="linux-x86_64" ;;
|
||||
*) HOST_TAG="" ;;
|
||||
esac
|
||||
|
||||
PREBUILT_DIR="$ANDROID_HOME_DIR/ndk/$ANDROID_NDK_VER/toolchains/llvm/prebuilt"
|
||||
if [[ -n "$HOST_TAG" && -x "$PREBUILT_DIR/$HOST_TAG/bin/llvm-strip" ]]; then
|
||||
STRIP_TOOL="$PREBUILT_DIR/$HOST_TAG/bin/llvm-strip"
|
||||
else
|
||||
for candidate in "$PREBUILT_DIR"/*/bin/llvm-strip; do
|
||||
if [[ -x "$candidate" ]]; then
|
||||
STRIP_TOOL="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
copy_lib() {
|
||||
local abi="$1"
|
||||
local src="$2"
|
||||
local dst_dir="$APP_DIR/$abi"
|
||||
local dst="$dst_dir/libmain.so"
|
||||
local tmp="$dst_dir/.libmain.so.$$"
|
||||
if [[ ! -f "$src" ]]; then
|
||||
echo "Missing native library for $abi: $src" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$dst_dir"
|
||||
cp -f "$src" "$tmp"
|
||||
if [[ "$ANDROID_STAGE_STRIP" != "0" ]] && [[ -n "$STRIP_TOOL" ]]; then
|
||||
"$STRIP_TOOL" --strip-unneeded "$tmp"
|
||||
mv -f "$tmp" "$dst"
|
||||
echo "Stripped and staged $src -> $dst"
|
||||
else
|
||||
mv -f "$tmp" "$dst"
|
||||
echo "Staged $src -> $dst (strip disabled or strip tool unavailable)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Drop any previously staged ABI directories to avoid stale APK contents.
|
||||
rm -rf "$APP_DIR/x86" "$APP_DIR/arm64-v8a" "$APP_DIR/x86_64"
|
||||
|
||||
for abi in $ANDROID_STAGE_ABIS; do
|
||||
case "$abi" in
|
||||
arm64-v8a)
|
||||
src="$ROOT_DIR/build/android-arm64/libmain.so"
|
||||
triple="aarch64-linux-android"
|
||||
;;
|
||||
x86_64)
|
||||
src="$ROOT_DIR/build/android-x86_64/libmain.so"
|
||||
triple="x86_64-linux-android"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported ABI '$abi'. Supported ABIs: arm64-v8a x86_64" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
copy_lib "$abi" "$src"
|
||||
if [[ -n "$STRIP_TOOL" ]]; then
|
||||
stl="$(dirname "$STRIP_TOOL")/../sysroot/usr/lib/$triple/libc++_shared.so"
|
||||
if [[ -f "$stl" ]]; then
|
||||
cp -f "$stl" "$APP_DIR/$abi/libc++_shared.so"
|
||||
echo "Staged $stl -> $APP_DIR/$abi/libc++_shared.so"
|
||||
else
|
||||
echo "Missing libc++_shared.so for $abi at $stl" >&2
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Cannot stage libc++_shared.so for $abi (NDK not found)" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Stage bundled mod packages into the app's assets source dir.
|
||||
MODS_STAGING_DIR="$ROOT_DIR/platforms/android/app/src/main/bundled_mods"
|
||||
rm -rf "$MODS_STAGING_DIR"
|
||||
mkdir -p "$MODS_STAGING_DIR"
|
||||
for abi in $ANDROID_STAGE_ABIS; do
|
||||
case "$abi" in
|
||||
arm64-v8a) build_dir="$ROOT_DIR/build/android-arm64" ;;
|
||||
x86_64) build_dir="$ROOT_DIR/build/android-x86_64" ;;
|
||||
esac
|
||||
[[ -d "$build_dir/bundled_mods" ]] || continue
|
||||
for pkg in "$build_dir/bundled_mods"/*.dusk; do
|
||||
[[ -f "$pkg" ]] || continue
|
||||
name="$(basename "$pkg")"
|
||||
if [[ ! -f "$MODS_STAGING_DIR/$name" ]]; then
|
||||
cp -f "$pkg" "$MODS_STAGING_DIR/$name"
|
||||
echo "Staged bundled mod $pkg"
|
||||
else
|
||||
stage_dir="$build_dir/mods/${name%.dusk}/${name%.dusk}_stage"
|
||||
(cd "$stage_dir" && zip -q -r "$MODS_STAGING_DIR/$name" lib)
|
||||
echo "Appended $abi libraries to bundled mod $name"
|
||||
fi
|
||||
done
|
||||
done
|
||||
@@ -1,16 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
SRC_DEFAULT="$ROOT_DIR/build/android-arm64/_deps/sdl-src/android-project/app/src/main/java/org/libsdl/app"
|
||||
SRC_DIR="${1:-$SRC_DEFAULT}"
|
||||
DST_DIR="$ROOT_DIR/platforms/android/app/src/main/java/org/libsdl/app"
|
||||
|
||||
if [[ ! -d "$SRC_DIR" ]]; then
|
||||
echo "SDL Java source directory not found: $SRC_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DST_DIR"
|
||||
cp -f "$SRC_DIR"/*.java "$DST_DIR"/
|
||||
echo "Synced SDL Java sources from $SRC_DIR to $DST_DIR"
|
||||
@@ -23,12 +23,12 @@ BEGIN
|
||||
BEGIN
|
||||
VALUE "CompanyName", "@DUSK_COMPANY_NAME@\0"
|
||||
VALUE "FileDescription", "@DUSK_FILE_DESCRIPTION@\0"
|
||||
VALUE "FileVersion", "@DUSK_VERSION_STRING@\0"
|
||||
VALUE "FileVersion", "@BOREALIS_APP_VERSION@\0"
|
||||
VALUE "InternalName", "dusklight\0"
|
||||
VALUE "LegalCopyright", "@DUSK_COPYRIGHT@\0"
|
||||
VALUE "OriginalFilename", "dusklight.exe\0"
|
||||
VALUE "ProductName", "@DUSK_PRODUCT_NAME@\0"
|
||||
VALUE "ProductVersion", "@DUSK_VERSION_STRING@\0"
|
||||
VALUE "ProductVersion", "@BOREALIS_APP_VERSION@\0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
||||
+1
-6
@@ -5,7 +5,7 @@
|
||||
#
|
||||
# Usage (from a mod project):
|
||||
# add_subdirectory(<dusk>/sdk dusk-sdk EXCLUDE_FROM_ALL)
|
||||
# add_mod(my_mod FEATURES game webgpu SOURCES ... MOD_JSON mod.json)
|
||||
# add_mod(my_mod FEATURES fmt game webgpu SOURCES ... MOD_JSON mod.json)
|
||||
#
|
||||
# On platforms where mods link against the game binary (Windows/Apple/Android), a
|
||||
# version-independent link stub is downloaded automatically unless DUSK_GAME_EXE is set.
|
||||
@@ -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")
|
||||
|
||||
|
||||
+17
-1
@@ -20,7 +20,23 @@ extern "C" {
|
||||
#ifdef __cplusplus
|
||||
#define MOD_EXTERN_C extern "C"
|
||||
#else
|
||||
#define MOD_EXTERN_C
|
||||
#define MOD_EXTERN_C extern
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
#define MOD_DECLARE_SERVICE( \
|
||||
service_type, variable, service_id_value, major_value, minor_value) \
|
||||
MOD_EXTERN_C const service_type* variable; \
|
||||
template <> \
|
||||
struct mods::ServiceTraits<service_type> { \
|
||||
static constexpr const char* id = service_id_value; \
|
||||
static constexpr uint16_t major_version = major_value; \
|
||||
static constexpr uint16_t minor_version = minor_value; \
|
||||
}
|
||||
#else
|
||||
#define MOD_DECLARE_SERVICE( \
|
||||
service_type, variable, service_id_value, major_value, minor_value) \
|
||||
MOD_EXTERN_C const service_type* variable
|
||||
#endif
|
||||
|
||||
#define MOD_ABI_VERSION 1u
|
||||
|
||||
+35
-179
@@ -1,210 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#if !defined(DUSK_BUILDING_GAME) && !defined(DUSK_MOD_FEATURE_GAME)
|
||||
#error "DEFINE_HOOK requires add_mod(... FEATURES game)"
|
||||
#if defined(_MSC_VER)
|
||||
#pragma message("warning: <mods/hook.hpp> is deprecated; include <mods/svc/hook.hpp> instead")
|
||||
#else
|
||||
#warning "<mods/hook.hpp> is deprecated; include <mods/svc/hook.hpp> instead"
|
||||
#endif
|
||||
|
||||
#include <mods/svc/hook.h>
|
||||
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <mods/svc/hook.hpp>
|
||||
|
||||
namespace mods {
|
||||
|
||||
template <class T>
|
||||
T arg(void* argsRaw, int n) noexcept {
|
||||
void** args = static_cast<void**>(argsRaw);
|
||||
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(args[n]);
|
||||
}
|
||||
|
||||
template <class T>
|
||||
std::remove_reference_t<T>& arg_ref(void* argsRaw, int n) noexcept {
|
||||
void** args = static_cast<void**>(argsRaw);
|
||||
return *static_cast<std::add_pointer_t<std::remove_reference_t<T>>>(args[n]);
|
||||
}
|
||||
|
||||
/*
|
||||
* Trampoline generator + per-target state. Tag makes each hooked target's statics distinct; the
|
||||
* target address comes from the declaration's metadata record, resolved by the host at mod
|
||||
* initialization.
|
||||
*/
|
||||
template <class Tag, class R, class... A>
|
||||
struct HookImpl {
|
||||
static inline R (*g_orig)(A...) = nullptr;
|
||||
static inline const HookService* hooks = nullptr;
|
||||
static inline void* target = nullptr;
|
||||
|
||||
static bool dispatch_pre(void* args, void* retval) {
|
||||
if (hooks == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int skipOriginal = 0;
|
||||
const ModResult result = hooks->dispatch_pre(mod_ctx, target, args, retval, &skipOriginal);
|
||||
return result == MOD_OK && skipOriginal != 0;
|
||||
}
|
||||
|
||||
static void dispatch_post(void* args, void* retval) {
|
||||
if (hooks != nullptr) {
|
||||
hooks->dispatch_post(mod_ctx, target, args, retval);
|
||||
}
|
||||
}
|
||||
|
||||
static R trampoline(A... args) {
|
||||
if constexpr (sizeof...(A) == 0) {
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
const bool skipOriginal = dispatch_pre(nullptr, nullptr);
|
||||
if (!skipOriginal) {
|
||||
g_orig(args...);
|
||||
}
|
||||
dispatch_post(nullptr, nullptr);
|
||||
} else {
|
||||
R result{};
|
||||
const bool skipOriginal =
|
||||
dispatch_pre(nullptr, static_cast<void*>(std::addressof(result)));
|
||||
if (!skipOriginal) {
|
||||
result = g_orig(args...);
|
||||
}
|
||||
dispatch_post(nullptr, static_cast<void*>(std::addressof(result)));
|
||||
return result;
|
||||
}
|
||||
} else {
|
||||
void* ptrs[] = {static_cast<void*>(std::addressof(args))...};
|
||||
if constexpr (std::is_void_v<R>) {
|
||||
const bool skipOriginal = dispatch_pre(static_cast<void*>(ptrs), nullptr);
|
||||
if (!skipOriginal) {
|
||||
g_orig(args...);
|
||||
}
|
||||
dispatch_post(static_cast<void*>(ptrs), nullptr);
|
||||
} else {
|
||||
R result{};
|
||||
const bool skipOriginal = dispatch_pre(
|
||||
static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
|
||||
if (!skipOriginal) {
|
||||
result = g_orig(args...);
|
||||
}
|
||||
dispatch_post(static_cast<void*>(ptrs), static_cast<void*>(std::addressof(result)));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
template <auto Target>
|
||||
using TargetTag = std::integral_constant<decltype(Target), Target>;
|
||||
template <FixedString Name>
|
||||
struct NameTag {};
|
||||
} // namespace detail
|
||||
|
||||
/*
|
||||
* Typed base for a hook on a function named at compile time (&daAlink_c::execute, &free_fn).
|
||||
* Instantiate through DEFINE_HOOK, which pairs it with the metadata record the host resolves.
|
||||
*/
|
||||
template <auto Target>
|
||||
struct Hook;
|
||||
|
||||
template <class C, class R, class... A, R (C::*Target)(A...)>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, C*, A...> {};
|
||||
|
||||
template <class C, class R, class... A, R (C::*Target)(A...) const>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, const C*, A...> {};
|
||||
|
||||
template <class R, class... A, R (*Target)(A...)>
|
||||
struct Hook<Target> : HookImpl<detail::TargetTag<Target>, R, A...> {};
|
||||
|
||||
/*
|
||||
* Typed base for a hook on a function by its symbol name, for targets you can't name in C++:
|
||||
* file-local statics, private members, or symbols without a header. The signature is written
|
||||
* free-style with the receiver first and is *not* compiler-checked. Instantiate through
|
||||
* DEFINE_HOOK_SYMBOL.
|
||||
*/
|
||||
template <FixedString Name, class Sig>
|
||||
struct NamedHook;
|
||||
|
||||
template <FixedString Name, class R, class... A>
|
||||
struct NamedHook<Name, R(A...)> : HookImpl<detail::NameTag<Name>, R, A...> {};
|
||||
|
||||
/*
|
||||
* Declare a hook target. The declaration emits a metadata record that the host resolves at mod
|
||||
* initialization. Every hook target must be declared.
|
||||
*
|
||||
* DEFINE_HOOK(&daAlink_c::execute, LinkExecute);
|
||||
* DEFINE_HOOK_SYMBOL("daAlink_hookshotAtHitCallBack",
|
||||
* void(fopAc_ac_c*, dCcD_GObjInf*, fopAc_ac_c*, dCcD_GObjInf*), HookshotHit);
|
||||
*
|
||||
* mods::hook_add_pre<LinkExecute>(svc_hook, on_link_execute);
|
||||
*
|
||||
* DEFINE_HOOK_SYMBOL names may be the platform mangled name (dlopen convention, no Mach-O
|
||||
* leading underscore) or the demangled qualified display name; overloaded display names are
|
||||
* ambiguous and need the mangled form.
|
||||
*/
|
||||
#define DEFINE_HOOK(target, alias) \
|
||||
[[maybe_unused]] static const void* const mod_meta_hook_##alias = \
|
||||
&::mods::detail::HookRecordFor<(target), ::mods::FixedString{#target}>::Holder::record; \
|
||||
struct alias : ::mods::Hook<(target)> { \
|
||||
static void* resolved_target() { \
|
||||
return ::mods::detail::HookRecordFor<(target), \
|
||||
::mods::FixedString{#target}>::Holder::record.resolved; \
|
||||
} \
|
||||
}
|
||||
|
||||
#define DEFINE_HOOK_SYMBOL(name, sig, alias) \
|
||||
MOD_META_RECORD static constinit auto mod_meta_hook_##alias = \
|
||||
::mods::detail::make_hook_name_record<::mods::FixedString{name}>(); \
|
||||
struct alias : ::mods::NamedHook<::mods::FixedString{name}, sig> { \
|
||||
static void* resolved_target() { return mod_meta_hook_##alias.resolved; } \
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_install(const HookService* hooks) {
|
||||
if (hooks == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
return hook::install<Entry>(hooks);
|
||||
}
|
||||
|
||||
Entry::hooks = hooks;
|
||||
if (Entry::target == nullptr) {
|
||||
void* resolved = Entry::resolved_target();
|
||||
if (resolved == nullptr) {
|
||||
return MOD_UNAVAILABLE;
|
||||
}
|
||||
Entry::target = resolved;
|
||||
}
|
||||
return hooks->install(mod_ctx, Entry::target, reinterpret_cast<void*>(Entry::trampoline),
|
||||
reinterpret_cast<void**>(&Entry::g_orig));
|
||||
template <class Entry>
|
||||
ModResult hook_install() {
|
||||
return hook::install<Entry>();
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_add_pre(
|
||||
const HookService* hooks, HookPreFn callback, const HookOptions* options = nullptr) {
|
||||
const ModResult installed = hook_install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
return hook::add_pre<Entry>(hooks, callback, options);
|
||||
}
|
||||
|
||||
return hooks->add_pre(mod_ctx, Entry::target, callback, options);
|
||||
template <class Entry>
|
||||
ModResult hook_add_pre(HookPreFn callback, const HookOptions* options = nullptr) {
|
||||
return hook::add_pre<Entry>(callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_add_post(
|
||||
const HookService* hooks, HookPostFn callback, const HookOptions* options = nullptr) {
|
||||
const ModResult installed = hook_install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
return hook::add_post<Entry>(hooks, callback, options);
|
||||
}
|
||||
|
||||
return hooks->add_post(mod_ctx, Entry::target, callback, options);
|
||||
template <class Entry>
|
||||
ModResult hook_add_post(HookPostFn callback, const HookOptions* options = nullptr) {
|
||||
return hook::add_post<Entry>(callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_replace(
|
||||
const HookService* hooks, HookReplaceFn callback, const HookOptions* options = nullptr) {
|
||||
const ModResult installed = hook_install<Entry>(hooks);
|
||||
if (installed != MOD_OK) {
|
||||
return installed;
|
||||
}
|
||||
return hook::replace<Entry>(hooks, callback, options);
|
||||
}
|
||||
|
||||
return hooks->replace(mod_ctx, Entry::target, callback, options);
|
||||
template <class Entry>
|
||||
ModResult hook_replace(HookReplaceFn callback, const HookOptions* options = nullptr) {
|
||||
return hook::replace<Entry>(callback, options);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_uninstall(const HookService* hooks) {
|
||||
return hook::uninstall<Entry>(hooks);
|
||||
}
|
||||
|
||||
template <class Entry>
|
||||
ModResult hook_uninstall() {
|
||||
return hook::uninstall<Entry>();
|
||||
}
|
||||
|
||||
} // namespace mods
|
||||
|
||||
@@ -241,6 +241,48 @@ consteval auto make_hook_mem_names() {
|
||||
return r;
|
||||
}
|
||||
|
||||
#if defined(__GNUC__) && !defined(__clang__) && defined(__ELF__)
|
||||
/* https://gcc.gnu.org/bugzilla/show_bug.cgi?id=41091 prevents inline static template members from
|
||||
* sharing an explicit ELF section with ordinary variables. GCC can instead constant-evaluate a
|
||||
* file-local record at each DEFINE_HOOK. */
|
||||
template <auto Target>
|
||||
void materialize_hook_mem(unsigned char* outPmf) {
|
||||
const auto target = Target;
|
||||
std::memcpy(outPmf, &target, sizeof(target));
|
||||
}
|
||||
|
||||
template <auto Target, FixedString Disp>
|
||||
consteval auto make_local_hook_record() {
|
||||
using F = decltype(Target);
|
||||
if constexpr (std::is_member_function_pointer_v<F>) {
|
||||
constexpr auto names = make_hook_mem_names<Target, Disp>();
|
||||
static_assert(sizeof(F) <= MOD_META_HOOK_MEM_EXT_CAPACITY,
|
||||
"unsupported pointer-to-member representation");
|
||||
if constexpr (sizeof(F) > MOD_META_HOOK_MEM_CAPACITY) {
|
||||
HookMemExtRecord<names.len> record = {
|
||||
{sizeof(HookMemExtRecord<names.len>), MOD_META_HOOK_MEM_EXT, 0}, sizeof(F),
|
||||
materialize_hook_mem<Target>, nullptr, {}};
|
||||
for (size_t i = 0; i < names.len; ++i) {
|
||||
record.names[i] = names.chars[i];
|
||||
}
|
||||
return record;
|
||||
} else {
|
||||
HookMemRecord<F, names.len> record = {
|
||||
{sizeof(HookMemRecord<F, names.len>), MOD_META_HOOK_MEM, 0}, 0, {Target}, nullptr,
|
||||
{}};
|
||||
for (size_t i = 0; i < names.len; ++i) {
|
||||
record.names[i] = names.chars[i];
|
||||
}
|
||||
return record;
|
||||
}
|
||||
} else {
|
||||
static_assert(std::is_pointer_v<F> && std::is_function_v<std::remove_pointer_t<F>>,
|
||||
"hook target must be a function or member function");
|
||||
return HookFnRecord<F>{{sizeof(HookFnRecord<F>), MOD_META_HOOK_FN, 0}, 0, Target, nullptr};
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/*
|
||||
* MSVC constant-evaluates a compact pointer-to-member only when every other operand in the
|
||||
* initializer is a literal: no consteval calls, constexpr-object copies, or default member
|
||||
|
||||
@@ -40,13 +40,13 @@ inline ModResult set_error(ModError* outError, ModResult code, const char* messa
|
||||
}; \
|
||||
}
|
||||
|
||||
// Declares `static const service_type* variable`, filled in by the host before mod_initialize.
|
||||
// Defines `const service_type* variable`, filled in by the host before mod_initialize.
|
||||
// Required imports are guaranteed non-null (the mod fails to load otherwise); optional imports
|
||||
// must be checked against nullptr before use. The unversioned macros use the latest minor version;
|
||||
// set an explicit version to target an older minor version for backwards compatibility.
|
||||
#define IMPORT_SERVICE_EX( \
|
||||
service_type, variable, service_id_value, major_value, min_minor_value, flags_value) \
|
||||
static const service_type* variable = nullptr; \
|
||||
const service_type* variable = nullptr; \
|
||||
MOD_META_RECORD static constinit ModMetaImport mod_meta_import_##variable = { \
|
||||
{sizeof(ModMetaImport), MOD_META_IMPORT, static_cast<uint8_t>(flags_value)}, \
|
||||
static_cast<uint16_t>(major_value), \
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#define CAMERA_SERVICE_ID "dev.twilitrealm.dusklight.camera"
|
||||
#define CAMERA_SERVICE_MAJOR 1u
|
||||
#define CAMERA_SERVICE_MINOR 0u
|
||||
#define CAMERA_SERVICE_MINOR 1u
|
||||
|
||||
/*
|
||||
* Snapshot of a game camera for the frame currently being recorded.
|
||||
@@ -42,6 +46,37 @@ typedef struct CameraInfo {
|
||||
|
||||
#define CAMERA_INFO_INIT {sizeof(CameraInfo)}
|
||||
|
||||
/* 0 is never a valid handle. */
|
||||
typedef uint64_t CameraOperatorHandle;
|
||||
|
||||
typedef struct CameraOperatorState {
|
||||
uint32_t struct_size;
|
||||
|
||||
/* Host inputs. */
|
||||
uint64_t frame_counter;
|
||||
uint64_t ticks;
|
||||
float aspect;
|
||||
|
||||
/* Initial camera state and callback output. */
|
||||
float eye[3];
|
||||
float center[3];
|
||||
float fovy;
|
||||
float bank_degrees;
|
||||
} CameraOperatorState;
|
||||
|
||||
/* Return true to use state for the current frame. Game thread only. */
|
||||
typedef bool (*CameraOperateFn)(ModContext* ctx, CameraOperatorState* state, void* user_data);
|
||||
|
||||
typedef struct CameraOperatorDesc {
|
||||
uint32_t struct_size;
|
||||
const char* debug_name;
|
||||
int32_t priority;
|
||||
CameraOperateFn operate;
|
||||
void* user_data;
|
||||
} CameraOperatorDesc;
|
||||
|
||||
#define CAMERA_OPERATOR_DESC_INIT {sizeof(CameraOperatorDesc), NULL, 0, NULL, NULL}
|
||||
|
||||
typedef struct CameraService {
|
||||
ServiceHeader header;
|
||||
|
||||
@@ -51,15 +86,18 @@ typedef struct CameraService {
|
||||
* perspective camera.
|
||||
*/
|
||||
ModResult (*get_camera)(ModContext* ctx, const void* game_view, CameraInfo* out_info);
|
||||
|
||||
/* Minor version 1 */
|
||||
|
||||
/*
|
||||
* Register an operator for the main camera. Operators run by descending priority, then
|
||||
* registration order, until one returns true. debug_name and operate must be set; debug_name
|
||||
* is copied. out_handle must not be NULL.
|
||||
*/
|
||||
ModResult (*register_camera_operator)(
|
||||
ModContext* ctx, const CameraOperatorDesc* desc, CameraOperatorHandle* out_handle);
|
||||
ModResult (*unregister_camera_operator)(ModContext* ctx, CameraOperatorHandle handle);
|
||||
} CameraService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<CameraService> {
|
||||
static constexpr const char* id = CAMERA_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = CAMERA_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = CAMERA_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(
|
||||
CameraService, svc_camera, CAMERA_SERVICE_ID, CAMERA_SERVICE_MAJOR, CAMERA_SERVICE_MINOR);
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#define CONFIG_SERVICE_ID "dev.twilitrealm.dusklight.config"
|
||||
#define CONFIG_SERVICE_MAJOR 1u
|
||||
#define CONFIG_SERVICE_MINOR 0u
|
||||
@@ -96,13 +100,5 @@ typedef struct ConfigService {
|
||||
ModResult (*unsubscribe)(ModContext* ctx, ConfigSubscriptionHandle handle);
|
||||
} ConfigService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<ConfigService> {
|
||||
static constexpr const char* id = CONFIG_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = CONFIG_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = CONFIG_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(
|
||||
ConfigService, svc_config, CONFIG_SERVICE_ID, CONFIG_SERVICE_MAJOR, CONFIG_SERVICE_MINOR);
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
/*
|
||||
* The mod SDK imports this service automatically for mods built with FEATURES game; service-only
|
||||
* and asset-only mods do not require it.
|
||||
@@ -19,13 +23,4 @@ typedef struct GameService {
|
||||
ServiceHeader header;
|
||||
} GameService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<GameService> {
|
||||
static constexpr const char* id = GAME_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = GAME_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = GAME_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(GameService, svc_game, GAME_SERVICE_ID, GAME_SERVICE_MAJOR, GAME_SERVICE_MINOR);
|
||||
|
||||
+72
-12
@@ -1,6 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <mods/api.h>
|
||||
#include <mods/svc/window.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
#if !defined(DUSK_BUILDING_GAME) && !defined(DUSK_MOD_FEATURE_WEBGPU)
|
||||
#error "mods/svc/gfx.h requires add_mod(... FEATURES webgpu)"
|
||||
@@ -28,7 +33,7 @@
|
||||
|
||||
#define GFX_SERVICE_ID "dev.twilitrealm.dusklight.gfx"
|
||||
#define GFX_SERVICE_MAJOR 1u
|
||||
#define GFX_SERVICE_MINOR 0u
|
||||
#define GFX_SERVICE_MINOR 1u
|
||||
|
||||
/* Maximum size for push_draw payload */
|
||||
#define GFX_INLINE_DRAW_PAYLOAD_SIZE 128u
|
||||
@@ -37,6 +42,7 @@
|
||||
typedef uint64_t GfxDrawTypeHandle;
|
||||
typedef uint64_t GfxStageHookHandle;
|
||||
typedef uint64_t GfxComputeTypeHandle;
|
||||
typedef uint64_t GfxPresentTargetHandle;
|
||||
|
||||
/* A suballocation in one of the shared per-frame streaming buffers. */
|
||||
typedef struct GfxRange {
|
||||
@@ -56,11 +62,13 @@ typedef struct GfxDeviceInfo {
|
||||
WGPUTextureFormat depth_format; /* scene depth target format */
|
||||
uint32_t sample_count; /* scene pass MSAA sample count */
|
||||
bool uses_reversed_z; /* true means depth 1.0 is near */
|
||||
WGPUInstance instance; /* borrowed; added in GfxService 1.1 */
|
||||
WGPUAdapter adapter; /* borrowed; added in GfxService 1.1 */
|
||||
} GfxDeviceInfo;
|
||||
|
||||
#define GFX_DEVICE_INFO_INIT \
|
||||
{sizeof(GfxDeviceInfo), NULL, NULL, WGPUTextureFormat_Undefined, WGPUTextureFormat_Undefined, \
|
||||
1u, false}
|
||||
1u, false, NULL, NULL}
|
||||
|
||||
/*
|
||||
* Passed to GfxDrawFn on the render worker thread; valid only during the call. The pass pipeline,
|
||||
@@ -168,6 +176,48 @@ typedef struct GfxComputeTypeDesc {
|
||||
|
||||
#define GFX_COMPUTE_TYPE_DESC_INIT {sizeof(GfxComputeTypeDesc), NULL, NULL, NULL}
|
||||
|
||||
/*
|
||||
* Invoked on the render worker while the frame encoder is open. The target texture and view have
|
||||
* been acquired by the host and are borrowed for the callback. Record all target work on encoder,
|
||||
* leave no pass open, and do not finish, submit, or present it. The host submits the shared command
|
||||
* buffer and presents the target after submission. The streaming buffers contain data appended on
|
||||
* the game thread before push_present.
|
||||
*/
|
||||
typedef struct GfxPresentContext {
|
||||
uint32_t struct_size;
|
||||
WGPUDevice device;
|
||||
WGPUQueue queue;
|
||||
WGPUCommandEncoder encoder;
|
||||
WGPUTexture target_texture;
|
||||
WGPUTextureView target_view;
|
||||
WGPUTextureFormat target_format;
|
||||
uint32_t target_width;
|
||||
uint32_t target_height;
|
||||
WGPUBuffer vertex_buffer;
|
||||
WGPUBuffer index_buffer;
|
||||
WGPUBuffer uniform_buffer;
|
||||
WGPUBuffer storage_buffer;
|
||||
} GfxPresentContext;
|
||||
|
||||
typedef void (*GfxPresentFn)(ModContext* ctx, const GfxPresentContext* present_ctx,
|
||||
const void* payload, size_t payload_size, void* user_data);
|
||||
|
||||
typedef struct GfxPresentTargetDesc {
|
||||
uint32_t struct_size;
|
||||
const char* label; /* optional debug label */
|
||||
uint32_t width; /* required for raw surfaces; ignored for WindowService windows */
|
||||
uint32_t height;
|
||||
WGPUTextureUsage usage; /* 0 defaults to RenderAttachment */
|
||||
WGPUTextureFormat preferred_format;
|
||||
WGPUCompositeAlphaMode preferred_alpha_mode;
|
||||
GfxPresentFn render;
|
||||
void* user_data;
|
||||
} GfxPresentTargetDesc;
|
||||
|
||||
#define GFX_PRESENT_TARGET_DESC_INIT \
|
||||
{sizeof(GfxPresentTargetDesc), NULL, 0u, 0u, WGPUTextureUsage_None, \
|
||||
WGPUTextureFormat_Undefined, WGPUCompositeAlphaMode_Auto, NULL, NULL}
|
||||
|
||||
typedef struct GfxService {
|
||||
ServiceHeader header;
|
||||
|
||||
@@ -200,15 +250,25 @@ typedef struct GfxService {
|
||||
ModResult (*resolve_pass)(
|
||||
ModContext* ctx, const GfxResolveDesc* desc, GfxResolvedTargets* out_targets);
|
||||
ModResult (*create_pass)(ModContext* ctx, uint32_t width, uint32_t height);
|
||||
|
||||
/* Minor version 1 */
|
||||
|
||||
ModResult (*register_present_target)(ModContext* ctx, WGPUSurface surface,
|
||||
const GfxPresentTargetDesc* desc, GfxPresentTargetHandle* out_handle);
|
||||
ModResult (*register_window_present_target)(ModContext* ctx, WindowHandle window,
|
||||
const GfxPresentTargetDesc* desc, GfxPresentTargetHandle* out_handle);
|
||||
/* Raw-surface targets only; WindowService target resizes are managed automatically. */
|
||||
ModResult (*resize_present_target)(
|
||||
ModContext* ctx, GfxPresentTargetHandle handle, uint32_t width, uint32_t height);
|
||||
ModResult (*unregister_present_target)(ModContext* ctx, GfxPresentTargetHandle handle);
|
||||
/*
|
||||
* MOD_OK means the task was queued.
|
||||
* MOD_UNAVAILABLE means no task could be queued now (for example, a window has no pixel size).
|
||||
* MOD_ERROR means an earlier task found the surface lost or deterministically invalid;
|
||||
* unregister and recreate the target before pushing again.
|
||||
*/
|
||||
ModResult (*push_present)(
|
||||
ModContext* ctx, GfxPresentTargetHandle handle, const void* payload, size_t payload_size);
|
||||
} GfxService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<GfxService> {
|
||||
static constexpr const char* id = GFX_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = GFX_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = GFX_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(GfxService, svc_gfx, GFX_SERVICE_ID, GFX_SERVICE_MAJOR, GFX_SERVICE_MINOR);
|
||||
|
||||
+39
-25
@@ -2,21 +2,27 @@
|
||||
|
||||
#include <mods/api.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include <mods/service.hpp>
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Intercept game functions by address. Prefer the typed helpers in mods/hook.hpp
|
||||
* (hook_add_pre/hook_add_post/hook_replace over a &Class::method): they generate the
|
||||
* trampoline and hide install/dispatch, which are the low-level primitives those helpers
|
||||
* build. resolve() maps a symbol name to an address for targets you can't name at compile time
|
||||
* (file-local statics included).
|
||||
* Hooks allow intercepting calls to game functions, allowing you to:
|
||||
* - Modify arguments
|
||||
* - Perform your own work before (pre), after (post) or instead of (replace) the original call
|
||||
* - From a pre hook, conditionally skip the original call and return your own value
|
||||
*
|
||||
* Every call is game-thread-only. Install and removal must run with no hooked function on the
|
||||
* stack; the loader guarantees this by applying mod lifecycle changes between frames, which is
|
||||
* why hooking a function that never returns (the outermost loop) makes a mod un-unloadable.
|
||||
* In most cases, you'll want to instead use the C++ helpers in mods/svc/hook.hpp
|
||||
* (mods::hook::add_pre/add_post/replace). They generate the trampoline passed to
|
||||
* install and provide compile-time type checking.
|
||||
*
|
||||
* resolve() resolves an address by symbol name for targets you can't name at compile time
|
||||
* (file-local statics included).
|
||||
*/
|
||||
|
||||
#define HOOK_SERVICE_ID "dev.twilitrealm.dusklight.hook"
|
||||
#define HOOK_SERVICE_MAJOR 1u
|
||||
#define HOOK_SERVICE_MINOR 0u
|
||||
#define HOOK_SERVICE_MINOR 1u
|
||||
|
||||
/* Symbol flags reported by resolve() */
|
||||
typedef enum HookSymbolFlags {
|
||||
@@ -46,7 +52,7 @@ typedef enum HookReplacePolicy {
|
||||
/*
|
||||
* Hook callbacks. `args` is an array of pointers to the call's arguments (index 0 is `this`
|
||||
* for member functions); `retval` points at the return slot (NULL for void). Read and write
|
||||
* them through mods::arg<T> / arg_ref<T> from mods/hook.hpp. `userdata` is the pointer
|
||||
* them through mods::arg<T> / arg_ref<T> from mods/svc/hook.hpp. `userdata` is the pointer
|
||||
* from HookOptions. All run on the game thread, in the hooked call's own stack frame.
|
||||
*/
|
||||
typedef HookAction (*HookPreFn)(ModContext* ctx, void* args, void* retval, void* userdata);
|
||||
@@ -68,11 +74,18 @@ typedef struct HookService {
|
||||
ServiceHeader header;
|
||||
|
||||
/*
|
||||
* Install a trampoline detour on fn_addr and return the address to call the original through in
|
||||
* *out_original_fn. The typed helpers generate the trampoline and call this; mods normally
|
||||
* don't. The first mod to install a given target owns the live detour; later mods register as
|
||||
* candidates so a hook survives the owner unloading (the detour is handed off and every
|
||||
* original pointer is rewritten). Idempotent per (mod, out slot).
|
||||
* Install a hook on fn_addr.
|
||||
*
|
||||
* trampoline_fn must point to a function that matches the original function's signature and
|
||||
* dispatches pre- and post- hooks. This dispatch trampoline is normally generated at compile
|
||||
* time using C++ template instantiation (see mods/svc/hook.hpp).
|
||||
*
|
||||
* The first hook install on a target will implicitly install a detour (patched instructions
|
||||
* on the target that jump to the dispatch trampoline). When all hooks are uninstalled from a
|
||||
* target, the detour is completely uninstalled.
|
||||
*
|
||||
* The address that the dispatch trampoline should call the original function through is written
|
||||
* to out_original_fn.
|
||||
*/
|
||||
ModResult (*install)(
|
||||
ModContext* ctx, void* fn_addr, void* trampoline_fn, void** out_original_fn);
|
||||
@@ -112,15 +125,16 @@ typedef struct HookService {
|
||||
*/
|
||||
ModResult (*resolve)(
|
||||
ModContext* ctx, const char* symbol, void** out_addr, HookSymbolFlags* out_flags);
|
||||
|
||||
/* Minor version 1 */
|
||||
|
||||
/*
|
||||
* Uninstall the current mod's hook on fn_addr and unregister all callbacks.
|
||||
* If no other mods have a hook installed on the target, the detour is uninstalled entirely.
|
||||
*
|
||||
* original_fn_slot must match the out_original_fn passed to install.
|
||||
*/
|
||||
ModResult (*uninstall)(ModContext* ctx, void* fn_addr, void** original_fn_slot);
|
||||
} HookService;
|
||||
|
||||
#ifdef __cplusplus
|
||||
#include "mods/service.hpp"
|
||||
|
||||
template <>
|
||||
struct mods::ServiceTraits<HookService> {
|
||||
static constexpr const char* id = HOOK_SERVICE_ID;
|
||||
static constexpr uint16_t major_version = HOOK_SERVICE_MAJOR;
|
||||
static constexpr uint16_t minor_version = HOOK_SERVICE_MINOR;
|
||||
};
|
||||
#endif
|
||||
MOD_DECLARE_SERVICE(HookService, svc_hook, HOOK_SERVICE_ID, HOOK_SERVICE_MAJOR, HOOK_SERVICE_MINOR);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user