Merge with origin/main

This commit is contained in:
jdflyer
2026-08-07 13:57:48 -07:00
204 changed files with 4811 additions and 14798 deletions
+4 -7
View File
@@ -76,7 +76,7 @@ jobs:
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: dusklight-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}}
name: dusklight-${{env.APP_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}}
path: |
build/install/Dusklight-*.AppImage
build/install/debug.tar.*
@@ -145,7 +145,7 @@ jobs:
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: dusklight-${{env.DUSK_VERSION}}-${{matrix.artifact_name}}
name: dusklight-${{env.APP_VERSION}}-${{matrix.artifact_name}}
path: |
build/install/Dusklight.app
build/install/debug.tar.*
@@ -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
+3
View File
@@ -1,3 +1,6 @@
[submodule "extern/aurora"]
path = extern/aurora
url = https://github.com/encounter/aurora.git
[submodule "extern/borealis"]
path = extern/borealis
url = https://github.com/encounter/borealis.git
+33 -140
View File
@@ -5,10 +5,11 @@ if (NOT CMAKE_BUILD_TYPE)
"Build type options: Debug Release RelWithDebInfo MinSizeRel" FORCE)
endif ()
include(cmake/DetectVersion.cmake)
detect_version()
include(extern/borealis/cmake/DetectVersion.cmake)
borealis_detect_version()
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
project(dusklight LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING})
project(dusklight LANGUAGES C CXX VERSION ${BOREALIS_APP_VERSION})
if (APPLE)
enable_language(OBJC OBJCXX)
endif ()
@@ -74,6 +75,8 @@ set(AURORA_ENABLE_RMLUI ON CACHE BOOL "Enable RmlUi UI support" FORCE)
add_subdirectory(extern/aurora EXCLUDE_FROM_ALL)
target_compile_definitions(aurora_mtx PRIVATE MTX_USE_PS=1)
add_subdirectory(extern/borealis EXCLUDE_FROM_ALL)
add_subdirectory(libs/freeverb)
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
@@ -85,8 +88,6 @@ endif ()
option(DUSK_BUILD_WARNINGS "Enable compiler warnings (off by default)")
option(DUSK_SELECTED_OPT "If on, selected parts of the project will be compiled with optimizations on Debug, intending to make the game run at 30 FPS. Note for MSVC: you will need to remove '/RTC1' from your debug flags in CMake.")
option(DUSK_MOVIE_SUPPORT "If on, compile against libjpeg-turbo to enable THP file decoding" ON)
option(DUSK_ENABLE_UPDATE_CHECKER "Enable update checking support" ON)
option(DUSK_ENABLE_SENTRY_NATIVE "Enable sentry-native crash reporting support" OFF)
option(DUSK_PACKAGE_INSTALL "Install Dusklight with a Linux-native file structure" OFF)
option(DUSK_GFX_DEBUG_GROUPS "Report debug groups to the native graphics API" ${DUSK_GFX_DEBUG_GROUPS_DEFAULT})
option(DUSK_ENABLE_CODE_MODS "Enable code mods" OFF)
@@ -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
View File
@@ -28,12 +28,12 @@
"cacheVariables": {
"CMAKE_C_COMPILER_LAUNCHER": "sccache",
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache",
"DUSK_ENABLE_SENTRY_NATIVE": {
"BOREALIS_ENABLE_SENTRY": {
"type": "BOOL",
"value": true
},
"DUSK_SENTRY_DSN": "$env{SENTRY_DSN}",
"DUSK_SENTRY_ENVIRONMENT": "production",
"BOREALIS_SENTRY_DSN": "$env{SENTRY_DSN}",
"BOREALIS_SENTRY_ENVIRONMENT": "production",
"Rust_RUSTUP_INSTALL_MISSING_TARGET": {
"type": "BOOL",
"value": true
@@ -448,7 +448,7 @@
"ci"
],
"cacheVariables": {
"DUSK_ENABLE_SENTRY_NATIVE": {
"BOREALIS_ENABLE_SENTRY": {
"type": "BOOL",
"value": false
}
+1 -1
View File
@@ -23,5 +23,5 @@ cp -r platforms/freedesktop/{16x16,32x32,48x48,64x64,128x128,256x256,512x512,102
cp platforms/freedesktop/dev.twilitrealm.dusk.desktop build/appdir/usr/share/applications
cd build/install
VERSION="$DUSK_VERSION" NO_STRIP=1 "$linuxdeploy" \
VERSION="$APP_VERSION" NO_STRIP=1 "$linuxdeploy" \
-l "$lib_dir/libusb-1.0.so" --appdir "$build_dir/appdir" --output appimage
-121
View File
@@ -1,121 +0,0 @@
# Version detection shared by the main build and the mod SDK (sdk/CMakeLists.txt)
include_guard(GLOBAL)
get_filename_component(_DUSK_VERSION_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN")
set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment")
set(DUSK_VERSION_OVERRIDE "" CACHE STRING "Override version string (skips git detection and format validation)")
macro(detect_version)
if (DUSK_VERSION_OVERRIDE)
set(DUSK_WC_DESCRIBE "${DUSK_VERSION_OVERRIDE}")
set(DUSK_VERSION_STRING "0.0.0.0")
set(DUSK_SHORT_VERSION_STRING "0.0.0")
set(DUSK_VERSION_CODE "1")
set(DUSK_WC_REVISION "")
set(DUSK_WC_BRANCH "")
set(DUSK_WC_DATE "")
message(STATUS "Dusklight version overridden to ${DUSK_WC_DESCRIBE}")
else ()
# obtain revision info from git
find_package(Git)
if (GIT_FOUND)
# make sure version information gets re-run when the current Git HEAD changes
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD
OUTPUT_VARIABLE dusk_git_head_filename
OUTPUT_STRIP_TRAILING_WHITESPACE)
get_filename_component(dusk_git_head_filename "${dusk_git_head_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_filename}")
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --symbolic-full-name HEAD
OUTPUT_VARIABLE dusk_git_head_symbolic
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT}
COMMAND ${GIT_EXECUTABLE} rev-parse --git-path ${dusk_git_head_symbolic}
OUTPUT_VARIABLE dusk_git_head_symbolic_filename
OUTPUT_STRIP_TRAILING_WHITESPACE)
get_filename_component(dusk_git_head_symbolic_filename "${dusk_git_head_symbolic_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_symbolic_filename}")
# defines DUSK_WC_REVISION
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
OUTPUT_VARIABLE DUSK_WC_REVISION
OUTPUT_STRIP_TRAILING_WHITESPACE)
# defines DUSK_WC_DESCRIBE
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} describe --tags --long --dirty --match "v*"
OUTPUT_VARIABLE DUSK_WC_DESCRIBE
OUTPUT_STRIP_TRAILING_WHITESPACE)
# remove the git hash, then collapse a clean "-0" suffix only
string(REGEX REPLACE "-[^-]+(-dirty|)$" "\\1" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
string(REGEX REPLACE "-0$" "" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
# defines DUSK_WC_BRANCH
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
OUTPUT_VARIABLE DUSK_WC_BRANCH
OUTPUT_STRIP_TRAILING_WHITESPACE)
# defines DUSK_WC_DATE
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} log -1 --format=%ad
OUTPUT_VARIABLE DUSK_WC_DATE
OUTPUT_STRIP_TRAILING_WHITESPACE)
else ()
message(STATUS "Unable to find git, commit information will not be available")
endif ()
if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$")
set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
set(_ver_major ${CMAKE_MATCH_1})
set(_ver_minor ${CMAKE_MATCH_2})
set(_ver_patch ${CMAKE_MATCH_3})
set(DUSK_VERSION_TWEAK "0")
if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$")
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$")
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
endif ()
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}")
if (DUSK_VERSION_TWEAK GREATER 999)
set(_tweak 999)
else ()
set(_tweak ${DUSK_VERSION_TWEAK})
endif ()
# encoding: major*1e7 + minor*1e5 + patch*1e3 + tweak; collision-free for major<210, minor<100, patch<100, tweak<=999
math(EXPR DUSK_VERSION_CODE
"${_ver_major} * 10000000 + ${_ver_minor} * 100000 + ${_ver_patch} * 1000 + ${_tweak}")
else ()
set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION")
set(DUSK_VERSION_STRING "0.0.0.0")
set(DUSK_SHORT_VERSION_STRING "0.0.0")
set(DUSK_VERSION_CODE "1")
endif ()
endif ()
# Add version information to CI environment variables
if (DEFINED ENV{GITHUB_ENV})
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION=${DUSK_WC_DESCRIBE}\n")
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION_CODE=${DUSK_VERSION_CODE}\n")
endif ()
message(STATUS "Dusklight version set to ${DUSK_WC_DESCRIBE}")
endmacro()
# Sets PLATFORM_NAME and configures version.h into the caller's binary dir.
macro(configure_version_header)
if (CMAKE_SYSTEM_NAME STREQUAL Windows)
set(PLATFORM_NAME win32)
elseif (CMAKE_SYSTEM_NAME STREQUAL Darwin)
if (IOS)
set(PLATFORM_NAME ios)
elseif (TVOS)
set(PLATFORM_NAME tvos)
else ()
set(PLATFORM_NAME macos)
endif ()
else ()
string(TOLOWER CMAKE_SYSTEM_NAME PLATFORM_NAME)
endif ()
configure_file(${_DUSK_VERSION_ROOT}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
endmacro()
+4
View File
@@ -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
View File
@@ -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 ()
+25 -4
View File
@@ -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
View File
@@ -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.
+1 -1
Vendored Submodule
+1
Submodule extern/borealis added at 6fd955e7e6
+9 -18
View File
@@ -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
)
+19 -28
View File
@@ -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"
+2 -2
View File
@@ -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*);
+21 -5
View File
@@ -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);
+2 -2
View File
@@ -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 = {
+2 -8
View File
@@ -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] = {
+2 -2
View File
@@ -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,
+3 -3
View File
@@ -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();
+2 -2
View File
@@ -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,
+3 -3
View File
@@ -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
+2 -2
View File
@@ -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;
+2 -2
View File
@@ -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*);
+1 -1
View File
@@ -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;
+3 -3
View File
@@ -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;
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
};
+5 -5
View File
@@ -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;
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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,
+5 -5
View File
@@ -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;
+1 -1
View File
@@ -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;
+7 -7
View File
@@ -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;
+2 -2
View File
@@ -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) {
+8 -8
View File
@@ -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);
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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;
+4 -4
View File
@@ -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);
}
+3 -3
View File
@@ -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) {
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+6 -6
View File
@@ -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);
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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;
+6 -6
View File
@@ -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;
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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)));
+1 -1
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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;
+20
View File
@@ -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
)
+7
View File
@@ -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."
}
+35
View File
@@ -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);
}
+9
View File
@@ -0,0 +1,9 @@
#pragma once
struct WindowEvent;
namespace window_demo {
void log_window_event(const WindowEvent* event);
} // namespace window_demo
+229
View File
@@ -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
View File
@@ -1,6 +1,6 @@
# Android Shell
This directory contains a minimal SDLActivity-based Android app wrapper for Dusklight.
This directory contains Dusklight's Android shell built on top of Borealis.
## Prerequisites
@@ -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.
+22 -64
View File
@@ -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
View File
@@ -1,4 +1 @@
# Keep SDL activity and related JNI bridge methods.
-keep class org.libsdl.app.** { *; }
-keep class dev.twilitrealm.dusk.DuskHttpClient { *; }
-keep class dev.twilitrealm.dusk.DuskHttpClient$Response { *; }
-keep class dev.twilitrealm.dusk.DuskActivity { *; }
@@ -26,8 +26,8 @@
android:resource="@xml/game_mode_config" />
<provider
android:name="dev.twilitrealm.dusk.DuskDocumentsProvider"
android:authorities="dev.twilitrealm.dusk.documents"
android:name="dev.encounter.borealis.BorealisDocumentsProvider"
android:authorities="${applicationId}.documents"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
@@ -1,102 +1,23 @@
package dev.twilitrealm.dusk;
import android.app.ActionBar;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.OpenableColumns;
import android.provider.Settings;
import android.util.Log;
import android.view.Display;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.View;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import org.libsdl.app.SDLActivity;
import org.libsdl.app.SDLSurface;
import dev.encounter.borealis.BorealisActivity;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class DuskActivity extends SDLActivity {
public class DuskActivity extends BorealisActivity {
private static final String TAG = "DuskActivity";
private static final float DEFAULT_SURFACE_FRAME_RATE = 60.0f;
private static final int FOLDER_DIALOG_REQUEST_CODE = 0x4455;
private static final int MANAGE_STORAGE_REQUEST_CODE = 0x4456;
private static final String EXTERNAL_STORAGE_AUTHORITY =
"com.android.externalstorage.documents";
private long folderDialogUserdata = 0;
private boolean awaitingManageStoragePermission = false;
private static native void nativeFolderDialogResult(long userdata, String path, String error);
private static String[] splitArgs(String raw) {
List<String> out = new ArrayList<>();
StringBuilder current = new StringBuilder();
boolean inSingle = false;
boolean inDouble = false;
boolean escaped = false;
for (int i = 0; i < raw.length(); ++i) {
char c = raw.charAt(i);
if (escaped) {
current.append(c);
escaped = false;
continue;
}
if (c == '\\' && !inSingle) {
escaped = true;
continue;
}
if (c == '"' && !inSingle) {
inDouble = !inDouble;
continue;
}
if (c == '\'' && !inDouble) {
inSingle = !inSingle;
continue;
}
if (!inSingle && !inDouble && Character.isWhitespace(c)) {
if (current.length() > 0) {
out.add(current.toString());
current.setLength(0);
}
continue;
}
current.append(c);
}
if (escaped) {
current.append('\\');
}
if (current.length() > 0) {
out.add(current.toString());
}
return out.toArray(new String[0]);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
extractBundledMods();
super.onCreate(savedInstanceState);
hideSystemBars();
}
// Bundled mod packages ship as APK assets, which the native loader cannot read directly;
@@ -143,459 +64,23 @@ public class DuskActivity extends SDLActivity {
file.delete();
}
@Override
protected SDLSurface createSDLSurface(Context context) {
return new DuskSurface(context);
}
@Override
protected void onResume() {
super.onResume();
hideSystemBars();
if (awaitingManageStoragePermission) {
resumeFolderDialogAfterPermissionGrant();
}
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
hideSystemBars();
}
}
private void hideSystemBars() {
Window window = getWindow();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.setDecorFitsSystemWindows(false);
WindowInsetsController ctrl = window.getDecorView().getWindowInsetsController();
if (ctrl != null) {
ctrl.setSystemBarsBehavior(
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
ctrl.hide(WindowInsets.Type.systemBars());
}
} else {
View decorView = window.getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(uiOptions);
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.hide();
}
}
}
@Override
protected String[] getLibraries() {
// SDL3 is statically linked into libmain.so in this build.
return new String[] {
"main"
};
}
public void setPreferredSurfaceFrameRate(float frameRate) {
runOnUiThread(() -> {
if (mSurface instanceof DuskSurface) {
((DuskSurface)mSurface).setPreferredFrameRate(frameRate);
}
});
}
private static final class DuskSurface extends SDLSurface {
private float preferredFrameRate = DEFAULT_SURFACE_FRAME_RATE;
DuskSurface(Context context) {
super(context);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
super.surfaceChanged(holder, format, width, height);
setTargetFrameRate(holder);
}
void setPreferredFrameRate(float frameRate) {
preferredFrameRate = frameRate;
setTargetFrameRate(getHolder());
}
private void setTargetFrameRate(SurfaceHolder holder) {
if (!mIsSurfaceReady || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
return;
}
Surface surface = holder != null ? holder.getSurface() : getHolder().getSurface();
if (surface == null || !surface.isValid()) {
return;
}
float targetFrameRate = getMaxSupportedFrameRate();
if (preferredFrameRate > 0.0f) {
targetFrameRate = preferredFrameRate;
}
if (targetFrameRate <= 0.0f) {
return;
}
try {
surface.setFrameRate(
targetFrameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
Log.v(TAG, "Requested surface frame rate " + targetFrameRate + " fps");
} catch (RuntimeException e) {
Log.w(TAG, "Failed to request surface frame rate", e);
}
}
private float getMaxSupportedFrameRate() {
if (mDisplay == null) {
return 0.0f;
}
float maxFrameRate = mDisplay.getRefreshRate();
Display.Mode[] modes = mDisplay.getSupportedModes();
if (modes == null) {
return maxFrameRate;
}
for (Display.Mode mode : modes) {
maxFrameRate = Math.max(maxFrameRate, mode.getRefreshRate());
}
return maxFrameRate;
}
}
@Override
protected String[] getArguments() {
String[] arguments = super.getArguments();
if (arguments.length > 0) {
return arguments;
}
Intent intent = getIntent();
if (intent != null) {
String[] argv = intent.getStringArrayExtra("dusk_argv");
if (argv != null && argv.length > 0) {
return argv;
}
String rawArgs = intent.getStringExtra("dusk_args");
if (rawArgs != null) {
String trimmed = rawArgs.trim();
if (!trimmed.isEmpty()) {
return splitArgs(trimmed);
}
}
if (intent == null) {
return arguments;
}
return new String[0];
String[] argv = intent.getStringArrayExtra("dusk_argv");
if (argv != null && argv.length > 0) {
return argv;
}
String rawArgs = intent.getStringExtra("dusk_args");
return rawArgs == null ? arguments : splitArguments(rawArgs.trim());
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
persistUriPermissions(data);
}
if (requestCode == FOLDER_DIALOG_REQUEST_CODE) {
finishFolderDialog(resultCode, data);
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
public boolean showFolderDialog(long userdata) {
if (userdata == 0 || folderDialogUserdata != 0) {
return false;
}
folderDialogUserdata = userdata;
if (requiresManageStoragePermission() && !hasManageStoragePermission()) {
if (!requestManageStoragePermission()) {
finishFolderDialogWithError("Unable to request Android file access permission");
return false;
}
return true;
}
openFolderDialog();
return true;
}
private void openFolderDialog() {
runOnUiThread(() -> {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
try {
startActivityForResult(intent, FOLDER_DIALOG_REQUEST_CODE);
} catch (ActivityNotFoundException e) {
Log.w(TAG, "Unable to open folder dialog.", e);
finishFolderDialog(Activity.RESULT_CANCELED, null);
}
});
}
private boolean requiresManageStoragePermission() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;
}
private boolean hasManageStoragePermission() {
return !requiresManageStoragePermission() || Environment.isExternalStorageManager();
}
private boolean requestManageStoragePermission() {
if (!requiresManageStoragePermission()) {
return true;
}
awaitingManageStoragePermission = true;
runOnUiThread(() -> {
if (tryStartManageStorageIntent(
new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
.setData(Uri.parse("package:" + getPackageName()))) ||
tryStartManageStorageIntent(
new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)))
{
return;
}
finishFolderDialogWithError("Unable to request Android file access permission");
});
return true;
}
private boolean tryStartManageStorageIntent(Intent intent) {
try {
startActivityForResult(intent, MANAGE_STORAGE_REQUEST_CODE);
return true;
} catch (ActivityNotFoundException e) {
Log.w(TAG, "Unable to open all-files access settings.", e);
return false;
}
}
private void resumeFolderDialogAfterPermissionGrant() {
awaitingManageStoragePermission = false;
if (folderDialogUserdata == 0) {
return;
}
if (hasManageStoragePermission()) {
openFolderDialog();
return;
}
finishFolderDialogWithError(
"Allow \"All files access\" for Dusklight before choosing a custom data folder");
}
private void finishFolderDialogWithError(String error) {
long userdata = folderDialogUserdata;
folderDialogUserdata = 0;
awaitingManageStoragePermission = false;
if (userdata != 0) {
nativeFolderDialogResult(userdata, null, error);
}
}
private void finishFolderDialog(int resultCode, Intent data) {
long userdata = folderDialogUserdata;
folderDialogUserdata = 0;
if (userdata == 0) {
return;
}
if (resultCode == RESULT_OK && data != null && data.getData() != null) {
String path = getRealPathForUri(data.getData());
if (path != null && !path.isEmpty()) {
nativeFolderDialogResult(userdata, path, null);
} else {
nativeFolderDialogResult(
userdata, null, "Selected folder is not available as a filesystem path");
}
return;
}
nativeFolderDialogResult(userdata, null, null);
}
private String getRealPathForUri(Uri uri) {
if (uri == null) {
return null;
}
String scheme = uri.getScheme();
if ("file".equals(scheme)) {
return uri.getPath();
}
if (!"content".equals(scheme) ||
!EXTERNAL_STORAGE_AUTHORITY.equals(uri.getAuthority()) ||
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
{
return null;
}
try {
return getExternalStoragePathForDocumentId(getExternalStorageDocumentId(uri));
} catch (IllegalArgumentException e) {
Log.w(TAG, "Unable to resolve URI: " + uri, e);
return null;
}
}
private static String getExternalStorageDocumentId(Uri uri) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && isTreeDocumentUri(uri)) {
return DocumentsContract.getTreeDocumentId(uri);
}
return DocumentsContract.getDocumentId(uri);
}
private static boolean isTreeDocumentUri(Uri uri) {
List<String> segments = uri.getPathSegments();
return segments.size() >= 2 && "tree".equals(segments.get(0));
}
private String getExternalStoragePathForDocumentId(String documentId) {
if (documentId == null || documentId.isEmpty()) {
return null;
}
if (documentId.startsWith("raw:")) {
return documentId.substring("raw:".length());
}
String[] parts = documentId.split(":", 2);
String volumeId = parts[0];
String relativePath = parts.length > 1 ? parts[1] : "";
File root = getExternalStorageRoot(volumeId);
if (root == null) {
return null;
}
return relativePath.isEmpty()
? root.getAbsolutePath()
: new File(root, relativePath).getAbsolutePath();
}
private File getExternalStorageRoot(String volumeId) {
if ("primary".equalsIgnoreCase(volumeId)) {
return Environment.getExternalStorageDirectory();
}
if ("home".equalsIgnoreCase(volumeId)) {
return new File(
Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOCUMENTS);
}
File[] externalFilesDirs = getExternalFilesDirs(null);
if (externalFilesDirs != null) {
for (File externalFilesDir : externalFilesDirs) {
File root = getStorageRootForExternalFilesDir(externalFilesDir);
if (root != null && volumeId.equalsIgnoreCase(root.getName())) {
return root;
}
}
}
File fallback = new File("/storage", volumeId);
return fallback.exists() ? fallback : null;
}
private File getStorageRootForExternalFilesDir(File externalFilesDir) {
if (externalFilesDir == null) {
return null;
}
String path = externalFilesDir.getAbsolutePath();
int androidDir = path.indexOf("/Android/");
if (androidDir <= 0) {
return null;
}
return new File(path.substring(0, androidDir));
}
private void persistUriPermissions(Intent data) {
if (data == null) {
return;
}
int permissionFlags =
data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (permissionFlags == 0) {
return;
}
Uri uri = data.getData();
if (uri != null) {
persistUriPermission(uri, permissionFlags);
}
ClipData clipData = data.getClipData();
if (clipData == null) {
return;
}
for (int i = 0; i < clipData.getItemCount(); ++i) {
Uri itemUri = clipData.getItemAt(i).getUri();
if (itemUri != null) {
persistUriPermission(itemUri, permissionFlags);
}
}
}
private void persistUriPermission(Uri uri, int permissionFlags) {
if ((permissionFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
persistUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION, "read");
}
if ((permissionFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
persistUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, "write");
}
}
private void persistUriPermission(Uri uri, int permissionFlag, String permissionName) {
try {
getContentResolver().takePersistableUriPermission(uri, permissionFlag);
} catch (SecurityException | IllegalArgumentException e) {
Log.w(TAG, "Unable to persist " + permissionName + " URI permission for " + uri, e);
}
}
public String getDisplayNameForUri(String uriString) {
if (uriString == null || uriString.isEmpty()) {
return "";
}
Uri uri = Uri.parse(uriString);
if ("content".equals(uri.getScheme())) {
try (Cursor cursor = getContentResolver().query(
uri, new String[] { OpenableColumns.DISPLAY_NAME }, null, null, null))
{
if (cursor != null && cursor.moveToFirst()) {
int displayNameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (displayNameColumn >= 0) {
String displayName = cursor.getString(displayNameColumn);
if (displayName != null && !displayName.isEmpty()) {
return displayName;
}
}
}
} catch (SecurityException | IllegalArgumentException e) {
Log.w(TAG, "Unable to query display name for " + uri, e);
}
} else if ("file".equals(uri.getScheme())) {
String path = uri.getPath();
if (path != null && !path.isEmpty()) {
String name = new File(path).getName();
if (!name.isEmpty()) {
return name;
}
}
}
String lastSegment = uri.getLastPathSegment();
return lastSegment != null ? lastSegment : "";
}
}
@@ -1,467 +0,0 @@
package dev.twilitrealm.dusk;
import android.content.ContentResolver;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.webkit.MimeTypeMap;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class DuskDocumentsProvider extends DocumentsProvider {
public static final String AUTHORITY = "dev.twilitrealm.dusk.documents";
private static final String ROOT_ID = "dusk";
private static final String ROOT_DOCUMENT_ID = "root";
private static final String LOCATION_DESCRIPTOR_NAME = "data_location.json";
private static final String DIRECTORY_MIME_TYPE = Document.MIME_TYPE_DIR;
private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
Root.COLUMN_ROOT_ID,
Root.COLUMN_FLAGS,
Root.COLUMN_TITLE,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_ICON,
Root.COLUMN_AVAILABLE_BYTES,
Root.COLUMN_SUMMARY
};
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_FLAGS,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_SIZE
};
@Override
public boolean onCreate() {
if (!isCustomDataPathEnabled()) {
ensureUserDirectories();
}
return true;
}
@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
if (isCustomDataPathEnabled()) {
return result;
}
final File root = getRootDirectory();
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, ROOT_ID);
row.add(Root.COLUMN_FLAGS,
Root.FLAG_LOCAL_ONLY |
Root.FLAG_SUPPORTS_CREATE |
Root.FLAG_SUPPORTS_IS_CHILD);
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.app_name));
row.add(Root.COLUMN_DOCUMENT_ID, ROOT_DOCUMENT_ID);
row.add(Root.COLUMN_ICON, R.mipmap.icon);
row.add(Root.COLUMN_AVAILABLE_BYTES, root.getFreeSpace());
row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.documents_provider_summary));
return result;
}
@Override
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
includeDocument(result, documentId, getFileForDocumentId(documentId));
return result;
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
throws FileNotFoundException
{
return queryChildDocumentsInternal(parentDocumentId, projection);
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, Bundle queryArgs)
throws FileNotFoundException
{
return queryChildDocumentsInternal(parentDocumentId, projection);
}
private Cursor queryChildDocumentsInternal(String parentDocumentId, String[] projection)
throws FileNotFoundException
{
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
final File parent = getFileForDocumentId(parentDocumentId);
final File[] files = parent.listFiles();
result.setNotificationUri(getContext().getContentResolver(), getChildDocumentsUri(parentDocumentId));
if (files == null) {
return result;
}
for (File file : files) {
includeDocument(result, getDocumentIdForFile(file), file);
}
return result;
}
@Override
public boolean isChildDocument(String parentDocumentId, String documentId) {
try {
final File parent = getFileForDocumentId(parentDocumentId);
final File child = getFileForDocumentId(documentId);
return isInside(parent, child);
} catch (FileNotFoundException e) {
return false;
}
}
@Override
public String createDocument(String parentDocumentId, String mimeType, String displayName)
throws FileNotFoundException
{
final File parent = getFileForDocumentId(parentDocumentId);
if (!parent.isDirectory()) {
throw new FileNotFoundException("Parent is not a directory: " + parentDocumentId);
}
final String safeDisplayName = sanitizeDisplayName(displayName);
final File file = buildUniqueFile(parent, safeDisplayName);
final boolean created;
if (DIRECTORY_MIME_TYPE.equals(mimeType)) {
created = file.mkdir();
} else {
try {
created = file.createNewFile();
} catch (IOException e) {
throw asFileNotFound("Unable to create document", e);
}
}
if (!created) {
throw new FileNotFoundException("Unable to create document: " + displayName);
}
notifyChildrenChanged(parentDocumentId);
return getDocumentIdForFile(file);
}
@Override
public String renameDocument(String documentId, String displayName) throws FileNotFoundException {
final File file = getFileForDocumentId(documentId);
if (ROOT_DOCUMENT_ID.equals(documentId)) {
throw new FileNotFoundException("Cannot rename root document");
}
final File target = buildUniqueFile(file.getParentFile(), sanitizeDisplayName(displayName));
final String parentDocumentId = getDocumentIdForFile(file.getParentFile());
if (!file.renameTo(target)) {
throw new FileNotFoundException("Unable to rename document: " + documentId);
}
notifyDocumentChanged(documentId);
notifyDocumentChanged(getDocumentIdForFile(target));
notifyChildrenChanged(parentDocumentId);
return getDocumentIdForFile(target);
}
@Override
public void deleteDocument(String documentId) throws FileNotFoundException {
if (ROOT_DOCUMENT_ID.equals(documentId)) {
throw new FileNotFoundException("Cannot delete root document");
}
final File file = getFileForDocumentId(documentId);
final String parentDocumentId = getDocumentIdForFile(file.getParentFile());
deleteRecursively(file);
notifyDocumentChanged(documentId);
notifyChildrenChanged(parentDocumentId);
}
@Override
public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal)
throws FileNotFoundException
{
return ParcelFileDescriptor.open(getFileForDocumentId(documentId), modeToParcelMode(mode));
}
@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, android.graphics.Point sizeHint,
CancellationSignal signal) throws FileNotFoundException
{
throw new FileNotFoundException("Thumbnails are not supported");
}
private void includeDocument(MatrixCursor result, String documentId, File file) throws FileNotFoundException {
final MatrixCursor.RowBuilder row = result.newRow();
final boolean isDirectory = file.isDirectory();
final String displayName = ROOT_DOCUMENT_ID.equals(documentId)
? getContext().getString(R.string.documents_provider_root_name)
: file.getName();
int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME;
if (isDirectory) {
flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
} else if (file.canWrite()) {
flags |= Document.FLAG_SUPPORTS_WRITE;
}
if (ROOT_DOCUMENT_ID.equals(documentId)) {
flags &= ~(Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME);
}
row.add(Document.COLUMN_DOCUMENT_ID, documentId);
row.add(Document.COLUMN_DISPLAY_NAME, displayName);
row.add(Document.COLUMN_FLAGS, flags);
row.add(Document.COLUMN_MIME_TYPE, isDirectory ? DIRECTORY_MIME_TYPE : getMimeType(file));
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
row.add(Document.COLUMN_SIZE, isDirectory ? null : file.length());
}
private File getRootDirectory() throws FileNotFoundException {
if (isCustomDataPathEnabled()) {
throw new FileNotFoundException(
"Dusk DocumentsProvider is disabled while a custom data path is configured");
}
final File root = getContext().getFilesDir();
if (root == null) {
throw new FileNotFoundException("Dusklight files directory is unavailable");
}
return root;
}
private File getFileForDocumentId(String documentId) throws FileNotFoundException {
final File root = getRootDirectory();
if (ROOT_DOCUMENT_ID.equals(documentId)) {
return root;
}
if (!documentId.startsWith(ROOT_DOCUMENT_ID + "/")) {
throw new FileNotFoundException("Invalid document id: " + documentId);
}
final String relativePath = documentId.substring(ROOT_DOCUMENT_ID.length() + 1);
final File file = new File(root, relativePath);
if (!isInside(root, file)) {
throw new FileNotFoundException("Document escapes Dusklight files directory: " + documentId);
}
if (!file.exists()) {
throw new FileNotFoundException("Document does not exist: " + documentId);
}
return file;
}
private String getDocumentIdForFile(File file) throws FileNotFoundException {
final File root = getRootDirectory();
if (sameFile(root, file)) {
return ROOT_DOCUMENT_ID;
}
if (!isInside(root, file)) {
throw new FileNotFoundException("File escapes Dusklight files directory: " + file);
}
final String rootPath = canonicalPath(root);
final String filePath = canonicalPath(file);
return ROOT_DOCUMENT_ID + "/" + filePath.substring(rootPath.length() + 1);
}
private void ensureUserDirectories() {
final File root = getContext().getFilesDir();
if (root == null) {
return;
}
new File(root, "texture_replacements").mkdirs();
new File(root, "USA/Card A").mkdirs();
new File(root, "EUR/Card A").mkdirs();
}
private boolean isCustomDataPathEnabled() {
if (getContext() == null) {
return false;
}
final File filesDir = getContext().getFilesDir();
if (filesDir == null) {
return false;
}
final File descriptor = new File(filesDir, LOCATION_DESCRIPTOR_NAME);
if (!descriptor.isFile()) {
return false;
}
try {
final JSONObject json = new JSONObject(readText(descriptor));
return "custom".equals(json.optString("mode", "default"));
} catch (IOException | JSONException e) {
return false;
}
}
private static String readText(File file) throws IOException {
try (FileInputStream input = new FileInputStream(file);
ByteArrayOutputStream output = new ByteArrayOutputStream())
{
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
return output.toString(StandardCharsets.UTF_8.name());
}
}
private static String[] resolveRootProjection(String[] projection) {
return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
}
private static String[] resolveDocumentProjection(String[] projection) {
return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
}
private static String sanitizeDisplayName(String displayName) throws FileNotFoundException {
if (displayName == null) {
throw new FileNotFoundException("Document name is empty");
}
final String sanitized = displayName.trim();
if (sanitized.isEmpty() || ".".equals(sanitized) || "..".equals(sanitized) ||
sanitized.contains("/") || sanitized.contains("\\"))
{
throw new FileNotFoundException("Invalid document name: " + displayName);
}
return sanitized;
}
private static File buildUniqueFile(File parent, String displayName) {
File file = new File(parent, displayName);
if (!file.exists()) {
return file;
}
final int dot = displayName.lastIndexOf('.');
final String baseName = dot > 0 ? displayName.substring(0, dot) : displayName;
final String extension = dot > 0 ? displayName.substring(dot) : "";
for (int i = 1; i < 100; ++i) {
file = new File(parent, baseName + " (" + i + ")" + extension);
if (!file.exists()) {
return file;
}
}
return new File(parent, baseName + " (" + System.currentTimeMillis() + ")" + extension);
}
private static int modeToParcelMode(String mode) {
if ("r".equals(mode)) {
return ParcelFileDescriptor.MODE_READ_ONLY;
}
if ("w".equals(mode) || "wt".equals(mode)) {
return ParcelFileDescriptor.MODE_WRITE_ONLY |
ParcelFileDescriptor.MODE_CREATE |
ParcelFileDescriptor.MODE_TRUNCATE;
}
if ("wa".equals(mode)) {
return ParcelFileDescriptor.MODE_WRITE_ONLY |
ParcelFileDescriptor.MODE_CREATE |
ParcelFileDescriptor.MODE_APPEND;
}
if ("rw".equals(mode)) {
return ParcelFileDescriptor.MODE_READ_WRITE |
ParcelFileDescriptor.MODE_CREATE;
}
if ("rwt".equals(mode)) {
return ParcelFileDescriptor.MODE_READ_WRITE |
ParcelFileDescriptor.MODE_CREATE |
ParcelFileDescriptor.MODE_TRUNCATE;
}
return ParcelFileDescriptor.MODE_READ_ONLY;
}
private static String getMimeType(File file) {
final int dot = file.getName().lastIndexOf('.');
if (dot >= 0) {
final String extension = file.getName().substring(dot + 1).toLowerCase();
final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mimeType != null) {
return mimeType;
}
}
return "application/octet-stream";
}
private Uri getChildDocumentsUri(String parentDocumentId) {
return DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId);
}
private void notifyChildrenChanged(String parentDocumentId) {
final ContentResolver resolver = getContext().getContentResolver();
resolver.notifyChange(getChildDocumentsUri(parentDocumentId), null, false);
}
private void notifyDocumentChanged(String documentId) {
final ContentResolver resolver = getContext().getContentResolver();
resolver.notifyChange(DocumentsContract.buildDocumentUri(AUTHORITY, documentId), null, false);
}
private static void deleteRecursively(File file) throws FileNotFoundException {
if (file.isDirectory()) {
final File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
deleteRecursively(child);
}
}
}
if (!file.delete()) {
throw new FileNotFoundException("Unable to delete document: " + file);
}
}
private static boolean isInside(File parent, File child) {
try {
final String parentPath = canonicalPath(parent);
final String childPath = canonicalPath(child);
return childPath.equals(parentPath) || childPath.startsWith(parentPath + File.separator);
} catch (FileNotFoundException e) {
return false;
}
}
private static boolean sameFile(File a, File b) {
try {
return canonicalPath(a).equals(canonicalPath(b));
} catch (FileNotFoundException e) {
return false;
}
}
private static String canonicalPath(File file) throws FileNotFoundException {
try {
return file.getCanonicalPath();
} catch (IOException e) {
throw asFileNotFound("Unable to resolve path", e);
}
}
private static FileNotFoundException asFileNotFound(String message, IOException cause) {
final FileNotFoundException exception = new FileNotFoundException(message + ": " + cause.getMessage());
exception.initCause(cause);
return exception;
}
}
@@ -1,237 +0,0 @@
package dev.twilitrealm.dusk;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
public final class DuskHttpClient {
public static final int ERROR_NONE = 0;
public static final int ERROR_INVALID_URL = 1;
public static final int ERROR_UNSUPPORTED_SCHEME = 2;
public static final int ERROR_TIMEOUT = 3;
public static final int ERROR_TOO_LARGE = 4;
public static final int ERROR_NETWORK = 5;
private static final int MAX_REDIRECTS = 5;
public static final class Response {
public int error;
public String message;
public int statusCode;
public String[] headerNames;
public String[] headerValues;
public byte[] body;
Response(int error, String message, int statusCode, String[] headerNames,
String[] headerValues, byte[] body) {
this.error = error;
this.message = message;
this.statusCode = statusCode;
this.headerNames = headerNames != null ? headerNames : new String[0];
this.headerValues = headerValues != null ? headerValues : new String[0];
this.body = body != null ? body : new byte[0];
}
}
private DuskHttpClient() {
}
public static Response get(String url, String[] headerNames, String[] headerValues,
int timeoutMs, long maxBodyBytes) {
if (url == null || url.isEmpty()) {
return fail(ERROR_INVALID_URL, "URL is empty");
}
try {
URL currentUrl = new URL(url);
if (!isHttps(currentUrl)) {
return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported");
}
for (int redirect = 0; redirect <= MAX_REDIRECTS; ++redirect) {
HttpsURLConnection connection =
(HttpsURLConnection) currentUrl.openConnection();
try {
connection.setRequestMethod("GET");
connection.setConnectTimeout(timeoutMs);
connection.setReadTimeout(timeoutMs);
connection.setUseCaches(false);
connection.setInstanceFollowRedirects(false);
applyHeaders(connection, headerNames, headerValues);
int statusCode = connection.getResponseCode();
if (isRedirect(statusCode)) {
String location = connection.getHeaderField("Location");
if (location == null || location.isEmpty()) {
return fail(ERROR_NETWORK, "Redirect response did not include Location",
statusCode, connection, new byte[0]);
}
URL nextUrl = new URL(currentUrl, location);
if (!isHttps(nextUrl)) {
return fail(ERROR_UNSUPPORTED_SCHEME,
"Only https:// redirects are supported", statusCode,
connection, new byte[0]);
}
currentUrl = nextUrl;
continue;
}
byte[] body = readBody(connection, statusCode, maxBodyBytes);
return success(statusCode, connection, body);
} catch (ResponseTooLargeException e) {
return fail(ERROR_TOO_LARGE, "Response body exceeded the configured limit",
safeStatusCode(connection), connection, e.partialBody);
} finally {
connection.disconnect();
}
}
return fail(ERROR_NETWORK, "Too many redirects");
} catch (MalformedURLException e) {
return fail(ERROR_INVALID_URL, "Failed to parse URL");
} catch (SocketTimeoutException e) {
return fail(ERROR_TIMEOUT, "Request timed out");
} catch (IOException e) {
String message = e.getMessage();
return fail(ERROR_NETWORK, message != null ? message : e.toString());
} catch (ClassCastException e) {
return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported");
}
}
private static void applyHeaders(HttpsURLConnection connection, String[] names,
String[] values) {
if (names == null || values == null) {
return;
}
int count = Math.min(names.length, values.length);
for (int i = 0; i < count; ++i) {
if (names[i] != null && values[i] != null) {
connection.setRequestProperty(names[i], values[i]);
}
}
}
private static boolean isHttps(URL url) {
return "https".equalsIgnoreCase(url.getProtocol());
}
private static boolean isRedirect(int statusCode) {
return statusCode == HttpURLConnection.HTTP_MOVED_PERM ||
statusCode == HttpURLConnection.HTTP_MOVED_TEMP ||
statusCode == HttpURLConnection.HTTP_SEE_OTHER ||
statusCode == 307 ||
statusCode == 308;
}
private static byte[] readBody(HttpsURLConnection connection, int statusCode,
long maxBodyBytes) throws IOException,
ResponseTooLargeException {
InputStream stream = statusCode >= HttpURLConnection.HTTP_BAD_REQUEST ?
connection.getErrorStream() : connection.getInputStream();
if (stream == null) {
return new byte[0];
}
try (InputStream bodyStream = stream;
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buffer = new byte[8192];
long total = 0;
while (true) {
int read = bodyStream.read(buffer);
if (read < 0) {
return out.toByteArray();
}
if (read == 0) {
continue;
}
if (read > maxBodyBytes || total > maxBodyBytes - read) {
throw new ResponseTooLargeException(out.toByteArray());
}
out.write(buffer, 0, read);
total += read;
}
}
}
private static int safeStatusCode(HttpsURLConnection connection) {
try {
return connection.getResponseCode();
} catch (IOException e) {
return 0;
}
}
private static Response success(int statusCode, HttpsURLConnection connection, byte[] body) {
HeaderLists headers = readHeaders(connection);
return new Response(ERROR_NONE, "", statusCode, headers.names, headers.values, body);
}
private static Response fail(int error, String message) {
return new Response(error, message, 0, null, null, null);
}
private static Response fail(int error, String message, int statusCode,
HttpsURLConnection connection, byte[] body) {
HeaderLists headers = readHeaders(connection);
return new Response(error, message, statusCode, headers.names, headers.values, body);
}
private static HeaderLists readHeaders(HttpsURLConnection connection) {
List<String> names = new ArrayList<>();
List<String> values = new ArrayList<>();
Map<String, List<String>> headerFields = connection.getHeaderFields();
if (headerFields == null) {
return new HeaderLists(new String[0], new String[0]);
}
for (Map.Entry<String, List<String>> entry : headerFields.entrySet()) {
String name = entry.getKey();
if (name == null) {
continue;
}
List<String> entryValues = entry.getValue();
if (entryValues == null || entryValues.isEmpty()) {
names.add(name);
values.add("");
continue;
}
for (String value : entryValues) {
names.add(name);
values.add(value != null ? value : "");
}
}
return new HeaderLists(names.toArray(new String[0]), values.toArray(new String[0]));
}
private static final class HeaderLists {
final String[] names;
final String[] values;
HeaderLists(String[] names, String[] values) {
this.names = names;
this.values = values;
}
}
private static final class ResponseTooLargeException extends Exception {
final byte[] partialBody;
ResponseTooLargeException(byte[] partialBody) {
this.partialBody = partialBody;
}
}
}
@@ -1,21 +0,0 @@
package org.libsdl.app;
import android.hardware.usb.UsbDevice;
interface HIDDevice
{
public int getId();
public int getVendorId();
public int getProductId();
public String getSerialNumber();
public int getVersion();
public String getManufacturerName();
public String getProductName();
public UsbDevice getDevice();
public boolean open();
public int writeReport(byte[] report, boolean feature);
public boolean readReport(byte[] report, boolean feature);
public void setFrozen(boolean frozen);
public void close();
public void shutdown();
}
@@ -1,829 +0,0 @@
package org.libsdl.app;
import android.content.Context;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothGattService;
import android.hardware.usb.UsbDevice;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.os.*;
//import com.android.internal.util.HexDump;
import java.lang.Runnable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice {
private static final String TAG = "hidapi";
private HIDDeviceManager mManager;
private BluetoothDevice mDevice;
private int mDeviceId;
private BluetoothGatt mGatt;
private boolean mIsRegistered = false;
private boolean mIsConnected = false;
private boolean mIsChromebook = false;
private boolean mIsReconnecting = false;
private boolean mHasEnabledNotifications = false;
private boolean mHasSeenInputUpdate = false;
private boolean mFrozen = false;
private LinkedList<GattOperation> mOperations;
GattOperation mCurrentOperation = null;
private Handler mHandler;
private int mProductId = -1;
private int mReportId = 0;
private UUID mInputCharacteristic;
private static final int D0G_BLE2_PID = 0x1106;
private static final int TRITON_BLE_PID = 0x1303;
private static final int TRANSPORT_AUTO = 0;
private static final int TRANSPORT_BREDR = 1;
private static final int TRANSPORT_LE = 2;
private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000;
static final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3");
static final UUID inputCharacteristicD0G = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3");
static final UUID inputCharacteristicTriton_0x45 = UUID.fromString("100F6C7A-1735-4313-B402-38567131E5F3");
static final UUID inputCharacteristicTriton_0x47 = UUID.fromString("100F6C7C-1735-4313-B402-38567131E5F3");
static final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3");
static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 };
private HashMap<Integer, BluetoothGattCharacteristic> mOutputReportChars = new HashMap<Integer, BluetoothGattCharacteristic>();
static class GattOperation {
private enum Operation {
CHR_READ,
CHR_WRITE,
ENABLE_NOTIFICATION
}
Operation mOp;
UUID mUuid;
byte[] mValue;
BluetoothGatt mGatt;
boolean mResult = true;
int mDelayMs = 0;
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
mGatt = gatt;
mOp = operation;
mUuid = uuid;
}
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, int delayMs) {
mGatt = gatt;
mOp = operation;
mUuid = uuid;
mDelayMs = delayMs;
}
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
mGatt = gatt;
mOp = operation;
mUuid = uuid;
mValue = value;
}
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value, int delayMs) {
mGatt = gatt;
mOp = operation;
mUuid = uuid;
mValue = value;
mDelayMs = delayMs;
}
public void run() {
// This is executed in main thread
BluetoothGattCharacteristic chr;
switch (mOp) {
case CHR_READ:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Reading characteristic " + chr.getUuid());
if (!mGatt.readCharacteristic(chr)) {
Log.e(TAG, "Unable to read characteristic " + mUuid.toString());
mResult = false;
break;
}
mResult = true;
break;
case CHR_WRITE:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
chr.setValue(mValue);
if (!mGatt.writeCharacteristic(chr)) {
Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
mResult = false;
break;
}
mResult = true;
break;
case ENABLE_NOTIFICATION:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Writing descriptor of " + chr.getUuid());
if (chr != null) {
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if (cccd != null) {
int properties = chr.getProperties();
byte[] value;
if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
} else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) {
value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
} else {
Log.e(TAG, "Unable to start notifications on input characteristic");
mResult = false;
return;
}
mGatt.setCharacteristicNotification(chr, true);
cccd.setValue(value);
if (!mGatt.writeDescriptor(cccd)) {
Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
mResult = false;
return;
}
mResult = true;
}
}
}
}
public boolean finish() {
return mResult;
}
public int getDelayMs() { return mDelayMs; }
private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
BluetoothGattService valveService = mGatt.getService(steamControllerService);
if (valveService == null)
return null;
return valveService.getCharacteristic(uuid);
}
static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) {
return new GattOperation(gatt, Operation.CHR_READ, uuid);
}
static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) {
return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value);
}
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid);
}
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid, int delayMs) {
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid, delayMs);
}
}
HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) {
mManager = manager;
mDevice = device;
mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier());
mIsRegistered = false;
mIsChromebook = SDLActivity.isChromebook();
mOperations = new LinkedList<GattOperation>();
mHandler = new Handler(Looper.getMainLooper());
mGatt = connectGatt();
mHasEnabledNotifications = false;
mHasSeenInputUpdate = false;
// final HIDDeviceBLESteamController finalThis = this;
// mHandler.postDelayed(new Runnable() {
// @Override
// void run() {
// finalThis.checkConnectionForChromebookIssue();
// }
// }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
}
String getIdentifier() {
return String.format("SteamController.%s", mDevice.getAddress());
}
BluetoothGatt getGatt() {
return mGatt;
}
// Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
// of TRANSPORT_LE. Let's force ourselves to connect low energy.
private BluetoothGatt connectGatt(boolean managed) {
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
try {
return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
} catch (Exception e) {
return mDevice.connectGatt(mManager.getContext(), managed, this);
}
} else {
return mDevice.connectGatt(mManager.getContext(), managed, this);
}
}
private BluetoothGatt connectGatt() {
return connectGatt(false);
}
protected int getConnectionState() {
Context context = mManager.getContext();
if (context == null) {
// We are lacking any context to get our Bluetooth information. We'll just assume disconnected.
return BluetoothProfile.STATE_DISCONNECTED;
}
BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
if (btManager == null) {
// This device doesn't support Bluetooth. We should never be here, because how did
// we instantiate a device to start with?
return BluetoothProfile.STATE_DISCONNECTED;
}
return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
}
void reconnect() {
if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
mGatt.disconnect();
mGatt = connectGatt();
}
}
protected void checkConnectionForChromebookIssue() {
if (!mIsChromebook) {
// We only do this on Chromebooks, because otherwise it's really annoying to just attempt
// over and over.
return;
}
int connectionState = getConnectionState();
switch (connectionState) {
case BluetoothProfile.STATE_CONNECTED:
if (!mIsConnected) {
// We are in the Bad Chromebook Place. We can force a disconnect
// to try to recover.
Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
}
else if (!isRegistered()) {
if (mGatt.getServices().size() > 0) {
Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover.");
probeService(this);
}
else {
Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
}
}
else {
Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!");
return;
}
break;
case BluetoothProfile.STATE_DISCONNECTED:
Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
case BluetoothProfile.STATE_CONNECTING:
Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer.");
break;
}
final HIDDeviceBLESteamController finalThis = this;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
finalThis.checkConnectionForChromebookIssue();
}
}, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
}
private boolean isRegistered() {
return mIsRegistered;
}
private void setRegistered() {
mIsRegistered = true;
}
private boolean probeService(HIDDeviceBLESteamController controller) {
if (isRegistered()) {
return true;
}
if (!mIsConnected) {
return false;
}
Log.v(TAG, "probeService controller=" + controller);
for (BluetoothGattService service : mGatt.getServices()) {
if (service.getUuid().equals(steamControllerService)) {
Log.v(TAG, "Found Valve steam controller service " + service.getUuid());
for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
if (chr.getUuid().equals(inputCharacteristicTriton_0x45)) {
Log.v(TAG, "Found Triton input characteristic 0x45");
mProductId = TRITON_BLE_PID;
mReportId = 0x45;
mInputCharacteristic = chr.getUuid();
} else if (chr.getUuid().equals(inputCharacteristicTriton_0x47)) {
Log.v(TAG, "Found Triton input characteristic 0x47");
mProductId = TRITON_BLE_PID;
mReportId = 0x47;
mInputCharacteristic = chr.getUuid();
} else if (chr.getUuid().equals(inputCharacteristicD0G)) {
Log.v(TAG, "Found D0G input characteristic");
mProductId = D0G_BLE2_PID;
mReportId = 0x03;
mInputCharacteristic = chr.getUuid();
} else {
Pattern reportPattern = Pattern.compile("100F6C([0-9A-Z]{2})", Pattern.CASE_INSENSITIVE);
Matcher matcher = reportPattern.matcher(chr.getUuid().toString());
if (matcher.find()) {
try {
int reportId = Integer.parseInt(matcher.group(1), 16);
reportId -= 0x35;
if (reportId >= 0x80) {
// This is a Triton output report characteristic that we need to care about.
Log.v(TAG, "Found Triton output report 0x" + Integer.toString(reportId, 16));
mOutputReportChars.put(reportId, chr);
}
}
catch (NumberFormatException nfe) {
Log.w(TAG, "Could not parse report characteristic " + chr.getUuid().toString() + ": " + nfe.toString());
}
}
}
}
for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
if (chr.getUuid().equals(mInputCharacteristic)) {
// Start notifications
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if (cccd != null) {
enableNotification(chr.getUuid());
}
}
}
return true;
}
}
if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) {
Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us.");
mIsConnected = false;
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
}
return false;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
private void finishCurrentGattOperation() {
GattOperation op = null;
synchronized (mOperations) {
if (mCurrentOperation != null) {
op = mCurrentOperation;
mCurrentOperation = null;
}
}
if (op != null) {
boolean result = op.finish(); // TODO: Maybe in main thread as well?
// Our operation failed, let's add it back to the beginning of our queue.
if (!result) {
mOperations.addFirst(op);
}
}
executeNextGattOperation();
}
private void executeNextGattOperation() {
synchronized (mOperations) {
if (mCurrentOperation != null)
return;
if (mOperations.isEmpty())
return;
mCurrentOperation = mOperations.removeFirst();
}
Runnable gattOperationRunnable = new Runnable() {
@Override
public void run() {
synchronized (mOperations) {
if (mCurrentOperation == null) {
Log.e(TAG, "Current operation null in executor?");
return;
}
mCurrentOperation.run();
// now wait for the GATT callback and when it comes, finish this operation
}
}
};
if (mCurrentOperation.getDelayMs() == 0) {
// Run in main thread
mHandler.post(gattOperationRunnable);
}
else {
// If we have a delay on this operation, wait before we post it.
mHandler.postDelayed(gattOperationRunnable, mCurrentOperation.getDelayMs());
}
}
private void queueGattOperation(GattOperation op) {
synchronized (mOperations) {
mOperations.add(op);
}
executeNextGattOperation();
}
private void enableNotification(UUID chrUuid) {
// Add a 500ms delay to notification write for Amazon Fire TV devices, as otherwise if we do this too quickly after connecting
// it will return success and then silently drop the operation on the floor.
GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid, 500);
queueGattOperation(op);
// Amazon Fire devices can also silently timeout on writeDescriptor, so
// set up a little delayed check that will attempt to write a second time.
//
// While this only seems to be needed on Amazon Fire TV devices at present, it
// doesn't hurt to have a retry on other devices as well.
//
final HIDDeviceBLESteamController finalThis = this;
final UUID finalUuid = chrUuid;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (!finalThis.mHasEnabledNotifications) {
if (finalThis.mHasSeenInputUpdate) {
// Amazon Five devices may have enabled notifications on the input characteristic and not given us a callback. If we've seen
// input reports, though, somewhat by definition notifications are enabled.
Log.w(TAG, "WriteDescriptor has never returned, but we've seen input reports. Moving on with controller initialization.");
finalThis.mHasEnabledNotifications = true;
finalThis.enableValveMode();
return;
}
// Give one more try.
GattOperation retry = HIDDeviceBLESteamController.GattOperation.enableNotification(finalThis.mGatt, finalUuid, 500);
finalThis.queueGattOperation(retry);
}
}
}, 1000);
}
void writeCharacteristic(UUID uuid, byte[] value) {
GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value);
queueGattOperation(op);
}
void readCharacteristic(UUID uuid) {
GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid);
queueGattOperation(op);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
////////////// BluetoothGattCallback overridden methods
//////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onConnectionStateChange(BluetoothGatt g, int status, int newState) {
//Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
mIsReconnecting = false;
if (newState == 2) {
mIsConnected = true;
// Run directly, without GattOperation
if (!isRegistered()) {
mHandler.post(new Runnable() {
@Override
public void run() {
mGatt.discoverServices();
}
});
}
}
else if (newState == 0) {
mIsConnected = false;
}
// Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
//Log.v(TAG, "onServicesDiscovered status=" + status);
if (status == 0) {
if (gatt.getServices().size() == 0) {
Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack.");
mIsReconnecting = true;
mIsConnected = false;
gatt.disconnect();
mGatt = connectGatt(false);
} else {
if (getProductId() == TRITON_BLE_PID) {
// Android will not properly play well with Data Length Extensions without manually requesting a large MTU,
// and Triton controllers require DLE support.
//
// 517 is basically a "magic number" as far as Android's bluetooth code is concerned, so do not change
// this value. It is functionally "please enable data length extensions" on some Android builds.
mGatt.requestMtu(517);
}
probeService(this);
}
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
//Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid());
if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) {
mManager.HIDDeviceReportResponse(getId(), characteristic.getValue());
}
finishCurrentGattOperation();
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
//Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
if (characteristic.getUuid().equals(reportCharacteristic)) {
// Only register controller with the native side once it has been fully configured
if (!isRegistered()) {
Log.v(TAG, "Registering Steam Controller with ID: " + getId());
mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true, mReportId);
setRegistered();
}
}
finishCurrentGattOperation();
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
// Enable this for verbose logging of controller input reports
//Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue()));
if (characteristic.getUuid().equals(mInputCharacteristic) && !mFrozen) {
mHasSeenInputUpdate = true;
mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
}
}
@Override
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
//Log.v(TAG, "onDescriptorRead status=" + status);
}
private void enableValveMode()
{
BluetoothGattService valveService = mGatt.getService(steamControllerService);
if (valveService == null)
return;
BluetoothGattCharacteristic reportChr = valveService.getCharacteristic(reportCharacteristic);
if (reportChr != null) {
if (getProductId() == TRITON_BLE_PID) {
// For Triton we just mark things registered.
Log.v(TAG, "Registering Triton Steam Controller with ID: " + getId());
mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true, mReportId);
setRegistered();
} else {
// For the original controller, we need to manually enter Valve mode.
Log.v(TAG, "Writing report characteristic to enter valve mode");
reportChr.setValue(enterValveMode);
mGatt.writeCharacteristic(reportChr);
}
}
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
BluetoothGattCharacteristic chr = descriptor.getCharacteristic();
//Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid());
if (chr.getUuid().equals(mInputCharacteristic)) {
mHasEnabledNotifications = true;
enableValveMode();
}
finishCurrentGattOperation();
}
@Override
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
//Log.v(TAG, "onReliableWriteCompleted status=" + status);
}
@Override
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
//Log.v(TAG, "onReadRemoteRssi status=" + status);
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
//Log.v(TAG, "onMtuChanged status=" + status);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////// Public API
//////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public int getId() {
return mDeviceId;
}
@Override
public int getVendorId() {
// Valve Corporation
final int VALVE_USB_VID = 0x28DE;
return VALVE_USB_VID;
}
@Override
public int getProductId() {
if (mProductId > 0) {
// We've already set a product ID.
return mProductId;
}
if (mDevice.getName().startsWith("Steam Ctrl")) {
// We're a newer Triton device
mProductId = TRITON_BLE_PID;
} else {
// We're an OG Steam Controller
mProductId = D0G_BLE2_PID;
}
return mProductId;
}
@Override
public String getSerialNumber() {
// This will be read later via feature report by Steam
return "12345";
}
@Override
public int getVersion() {
return 0;
}
@Override
public String getManufacturerName() {
return "Valve Corporation";
}
@Override
public String getProductName() {
return "Steam Controller";
}
@Override
public UsbDevice getDevice() {
return null;
}
@Override
public boolean open() {
return true;
}
@Override
public int writeReport(byte[] report, boolean feature) {
if (!isRegistered()) {
Log.e(TAG, "Attempted writeReport before Steam Controller is registered!");
if (mIsConnected) {
probeService(this);
}
return -1;
}
if (feature) {
// We need to skip the first byte, as that doesn't go over the air
byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
//Log.v(TAG, "writeFeatureReport " + HexDump.dumpHexString(actual_report));
writeCharacteristic(reportCharacteristic, actual_report);
return report.length;
} else {
// If we're an original-recipe Steam Controller we just write to the characteristic directly.
if (getProductId() == D0G_BLE2_PID) {
//Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report));
writeCharacteristic(reportCharacteristic, report);
return report.length;
}
// If we're a Triton, we need to find the correct report characteristic.
if (report.length > 0) {
int reportId = report[0] & 0xFF;
BluetoothGattCharacteristic targetedReportCharacteristic = mOutputReportChars.get(reportId);
if (targetedReportCharacteristic != null) {
byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
//Log.v(TAG, "writeOutputReport 0x" + Integer.toString(reportId, 16) + " " + HexDump.dumpHexString(report));
writeCharacteristic(targetedReportCharacteristic.getUuid(), actual_report);
return report.length;
} else {
Log.w(TAG, "Got report write request for unknown report type 0x" + Integer.toString(reportId, 16));
}
}
}
return -1;
}
@Override
public boolean readReport(byte[] report, boolean feature) {
if (!isRegistered()) {
Log.e(TAG, "Attempted readReport before Steam Controller is registered!");
if (mIsConnected) {
probeService(this);
}
return false;
}
if (feature) {
readCharacteristic(reportCharacteristic);
return true;
} else {
// Not implemented
return false;
}
}
@Override
public void close() {
}
@Override
public void setFrozen(boolean frozen) {
mFrozen = frozen;
}
@Override
public void shutdown() {
close();
BluetoothGatt g = mGatt;
if (g != null) {
g.disconnect();
g.close();
mGatt = null;
}
mManager = null;
mIsRegistered = false;
mIsConnected = false;
mOperations.clear();
}
}
@@ -1,698 +0,0 @@
package org.libsdl.app;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.os.Build;
import android.util.Log;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.hardware.usb.*;
import android.os.Handler;
import android.os.Looper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
public class HIDDeviceManager {
private static final String TAG = "hidapi";
private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION";
private static HIDDeviceManager sManager;
private static int sManagerRefCount = 0;
static public HIDDeviceManager acquire(Context context) {
if (sManagerRefCount == 0) {
sManager = new HIDDeviceManager(context);
}
++sManagerRefCount;
return sManager;
}
static public void release(HIDDeviceManager manager) {
if (manager == sManager) {
--sManagerRefCount;
if (sManagerRefCount == 0) {
sManager.close();
sManager = null;
}
}
}
private Context mContext;
private HashMap<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>();
private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>();
private int mNextDeviceId = 0;
private SharedPreferences mSharedPreferences = null;
private boolean mIsChromebook = false;
private UsbManager mUsbManager;
private Handler mHandler;
private BluetoothManager mBluetoothManager;
private List<BluetoothDevice> mLastBluetoothDevices;
private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDeviceAttached(usbDevice);
} else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDeviceDetached(usbDevice);
} else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false));
}
}
};
private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// Bluetooth device was connected. If it was a Steam Controller, handle it
if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Log.d(TAG, "Bluetooth device connected: " + device);
if (isSteamController(device)) {
connectBluetoothDevice(device);
}
}
// Bluetooth device was disconnected, remove from controller manager (if any)
if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Log.d(TAG, "Bluetooth device disconnected: " + device);
disconnectBluetoothDevice(device);
}
}
};
private HIDDeviceManager(final Context context) {
mContext = context;
HIDDeviceRegisterCallback();
mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE);
mIsChromebook = SDLActivity.isChromebook();
// if (shouldClear) {
// SharedPreferences.Editor spedit = mSharedPreferences.edit();
// spedit.clear();
// spedit.apply();
// }
// else
{
mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0);
}
}
Context getContext() {
return mContext;
}
int getDeviceIDForIdentifier(String identifier) {
SharedPreferences.Editor spedit = mSharedPreferences.edit();
int result = mSharedPreferences.getInt(identifier, 0);
if (result == 0) {
result = mNextDeviceId++;
spedit.putInt("next_device_id", mNextDeviceId);
}
spedit.putInt(identifier, result);
spedit.apply();
return result;
}
private void initializeUSB() {
mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE);
if (mUsbManager == null) {
return;
}
/*
// Logging
for (UsbDevice device : mUsbManager.getDeviceList().values()) {
Log.i(TAG,"Path: " + device.getDeviceName());
Log.i(TAG,"Manufacturer: " + device.getManufacturerName());
Log.i(TAG,"Product: " + device.getProductName());
Log.i(TAG,"ID: " + device.getDeviceId());
Log.i(TAG,"Class: " + device.getDeviceClass());
Log.i(TAG,"Protocol: " + device.getDeviceProtocol());
Log.i(TAG,"Vendor ID " + device.getVendorId());
Log.i(TAG,"Product ID: " + device.getProductId());
Log.i(TAG,"Interface count: " + device.getInterfaceCount());
Log.i(TAG,"---------------------------------------");
// Get interface details
for (int index = 0; index < device.getInterfaceCount(); index++) {
UsbInterface mUsbInterface = device.getInterface(index);
Log.i(TAG," ***** *****");
Log.i(TAG," Interface index: " + index);
Log.i(TAG," Interface ID: " + mUsbInterface.getId());
Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass());
Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass());
Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol());
Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount());
// Get endpoint details
for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++)
{
UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi);
Log.i(TAG," ++++ ++++ ++++");
Log.i(TAG," Endpoint index: " + epi);
Log.i(TAG," Attributes: " + mEndpoint.getAttributes());
Log.i(TAG," Direction: " + mEndpoint.getDirection());
Log.i(TAG," Number: " + mEndpoint.getEndpointNumber());
Log.i(TAG," Interval: " + mEndpoint.getInterval());
Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize());
Log.i(TAG," Type: " + mEndpoint.getType());
}
}
}
Log.i(TAG," No more devices connected.");
*/
// Register for USB broadcasts and permission completions
IntentFilter filter = new IntentFilter();
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION);
if (Build.VERSION.SDK_INT >= 33) { /* Android 13.0 (TIRAMISU) */
mContext.registerReceiver(mUsbBroadcast, filter, Context.RECEIVER_EXPORTED);
} else {
mContext.registerReceiver(mUsbBroadcast, filter);
}
for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
handleUsbDeviceAttached(usbDevice);
}
}
UsbManager getUSBManager() {
return mUsbManager;
}
private void shutdownUSB() {
try {
mContext.unregisterReceiver(mUsbBroadcast);
} catch (Exception e) {
// We may not have registered, that's okay
}
}
private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) {
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
return true;
}
if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) {
return true;
}
return false;
}
private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) {
final int XB360_IFACE_SUBCLASS = 93;
final int XB360_IFACE_PROTOCOL = 1; // Wired
final int XB360W_IFACE_PROTOCOL = 129; // Wireless
final int[] SUPPORTED_VENDORS = {
0x0079, // GPD Win 2
0x044f, // Thrustmaster
0x045e, // Microsoft
0x046d, // Logitech
0x056e, // Elecom
0x06a3, // Saitek
0x0738, // Mad Catz
0x07ff, // Mad Catz
0x0e6f, // PDP
0x0f0d, // Hori
0x1038, // SteelSeries
0x11c9, // Nacon
0x12ab, // Unknown
0x1430, // RedOctane
0x146b, // BigBen
0x1532, // Razer Sabertooth
0x15e4, // Numark
0x162e, // Joytech
0x1689, // Razer Onza
0x1949, // Lab126, Inc.
0x1bad, // Harmonix
0x20d6, // PowerA
0x24c6, // PowerA
0x2c22, // Qanba
0x2dc8, // 8BitDo
0x3537, // GameSir
0x37d7, // Flydigi
0x9886, // ASTRO Gaming
};
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
(usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL ||
usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) {
int vendor_id = usbDevice.getVendorId();
for (int supportedVid : SUPPORTED_VENDORS) {
if (vendor_id == supportedVid) {
return true;
}
}
}
return false;
}
private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) {
final int XB1_IFACE_SUBCLASS = 71;
final int XB1_IFACE_PROTOCOL = 208;
final int[] SUPPORTED_VENDORS = {
0x03f0, // HP
0x044f, // Thrustmaster
0x045e, // Microsoft
0x0738, // Mad Catz
0x0b05, // ASUS
0x0e6f, // PDP
0x0f0d, // Hori
0x10f5, // Turtle Beach
0x1532, // Razer Wildcat
0x20d6, // PowerA
0x24c6, // PowerA
0x294b, // Snakebyte
0x2dc8, // 8BitDo
0x2e24, // Hyperkin
0x2e95, // SCUF
0x3285, // Nacon
0x3537, // GameSir
0x366c, // ByoWave
};
if (usbInterface.getId() == 0 &&
usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
int vendor_id = usbDevice.getVendorId();
for (int supportedVid : SUPPORTED_VENDORS) {
if (vendor_id == supportedVid) {
return true;
}
}
}
return false;
}
private void handleUsbDeviceAttached(UsbDevice usbDevice) {
connectHIDDeviceUSB(usbDevice);
}
private void handleUsbDeviceDetached(UsbDevice usbDevice) {
List<Integer> devices = new ArrayList<Integer>();
for (HIDDevice device : mDevicesById.values()) {
if (usbDevice.equals(device.getDevice())) {
devices.add(device.getId());
}
}
for (int id : devices) {
HIDDevice device = mDevicesById.get(id);
mDevicesById.remove(id);
device.shutdown();
HIDDeviceDisconnected(id);
}
}
private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) {
for (HIDDevice device : mDevicesById.values()) {
if (usbDevice.equals(device.getDevice())) {
boolean opened = false;
if (permission_granted) {
opened = device.open();
}
HIDDeviceOpenResult(device.getId(), opened);
}
}
}
private void connectHIDDeviceUSB(UsbDevice usbDevice) {
synchronized (this) {
int interface_mask = 0;
for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) {
UsbInterface usbInterface = usbDevice.getInterface(interface_index);
if (isHIDDeviceInterface(usbDevice, usbInterface)) {
// Check to see if we've already added this interface
// This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive
int interface_id = usbInterface.getId();
if ((interface_mask & (1 << interface_id)) != 0) {
continue;
}
interface_mask |= (1 << interface_id);
HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index);
int id = device.getId();
mDevicesById.put(id, device);
HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol(), false, 0);
}
}
}
}
private void initializeBluetooth() {
Log.d(TAG, "Initializing Bluetooth");
if (Build.VERSION.SDK_INT >= 31 /* Android 12 */ &&
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH_CONNECT");
return;
}
if (Build.VERSION.SDK_INT <= 30 /* Android 11.0 (R) */ &&
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH");
return;
}
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE");
return;
}
// Find bonded bluetooth controllers and create SteamControllers for them
mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
if (mBluetoothManager == null) {
// This device doesn't support Bluetooth.
return;
}
BluetoothAdapter btAdapter = mBluetoothManager.getAdapter();
if (btAdapter == null) {
// This device has Bluetooth support in the codebase, but has no available adapters.
return;
}
// Get our bonded devices.
for (BluetoothDevice device : btAdapter.getBondedDevices()) {
Log.d(TAG, "Bluetooth device available: " + device);
if (isSteamController(device)) {
connectBluetoothDevice(device);
}
}
// NOTE: These don't work on Chromebooks, to my undying dismay.
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
if (Build.VERSION.SDK_INT >= 33) { /* Android 13.0 (TIRAMISU) */
mContext.registerReceiver(mBluetoothBroadcast, filter, Context.RECEIVER_EXPORTED);
} else {
mContext.registerReceiver(mBluetoothBroadcast, filter);
}
if (mIsChromebook) {
mHandler = new Handler(Looper.getMainLooper());
mLastBluetoothDevices = new ArrayList<BluetoothDevice>();
// final HIDDeviceManager finalThis = this;
// mHandler.postDelayed(new Runnable() {
// @Override
// public void run() {
// finalThis.chromebookConnectionHandler();
// }
// }, 5000);
}
}
private void shutdownBluetooth() {
try {
mContext.unregisterReceiver(mBluetoothBroadcast);
} catch (Exception e) {
// We may not have registered, that's okay
}
}
// Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly.
// This function provides a sort of dummy version of that, watching for changes in the
// connected devices and attempting to add controllers as things change.
void chromebookConnectionHandler() {
if (!mIsChromebook) {
return;
}
ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>();
ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>();
List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
for (BluetoothDevice bluetoothDevice : currentConnected) {
if (!mLastBluetoothDevices.contains(bluetoothDevice)) {
connected.add(bluetoothDevice);
}
}
for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) {
if (!currentConnected.contains(bluetoothDevice)) {
disconnected.add(bluetoothDevice);
}
}
mLastBluetoothDevices = currentConnected;
for (BluetoothDevice bluetoothDevice : disconnected) {
disconnectBluetoothDevice(bluetoothDevice);
}
for (BluetoothDevice bluetoothDevice : connected) {
connectBluetoothDevice(bluetoothDevice);
}
final HIDDeviceManager finalThis = this;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
finalThis.chromebookConnectionHandler();
}
}, 10000);
}
boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) {
Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice);
synchronized (this) {
if (mBluetoothDevices.containsKey(bluetoothDevice)) {
Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect");
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
device.reconnect();
return false;
}
HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice);
int id = device.getId();
mBluetoothDevices.put(bluetoothDevice, device);
mDevicesById.put(id, device);
// The Steam Controller will mark itself connected once initialization is complete
}
return true;
}
void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) {
synchronized (this) {
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
if (device == null)
return;
int id = device.getId();
mBluetoothDevices.remove(bluetoothDevice);
mDevicesById.remove(id);
device.shutdown();
HIDDeviceDisconnected(id);
}
}
boolean isSteamController(BluetoothDevice bluetoothDevice) {
// Sanity check. If you pass in a null device, by definition it is never a Steam Controller.
if (bluetoothDevice == null) {
return false;
}
// If the device has no local name, we really don't want to try an equality check against it.
if (bluetoothDevice.getName() == null) {
return false;
}
// Steam Controllers will always support Bluetooth Low Energy
if ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) == 0) {
return false;
}
// Match on the name either the original Steam Controller or the new second-generation one advertise with.
return bluetoothDevice.getName().equals("SteamController") || bluetoothDevice.getName().startsWith("Steam Ctrl");
}
private void close() {
shutdownUSB();
shutdownBluetooth();
synchronized (this) {
for (HIDDevice device : mDevicesById.values()) {
device.shutdown();
}
mDevicesById.clear();
mBluetoothDevices.clear();
HIDDeviceReleaseCallback();
}
}
public void setFrozen(boolean frozen) {
synchronized (this) {
for (HIDDevice device : mDevicesById.values()) {
device.setFrozen(frozen);
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
private HIDDevice getDevice(int id) {
synchronized (this) {
HIDDevice result = mDevicesById.get(id);
if (result == null) {
Log.v(TAG, "No device for id: " + id);
Log.v(TAG, "Available devices: " + mDevicesById.keySet());
}
return result;
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
////////// JNI interface functions
//////////////////////////////////////////////////////////////////////////////////////////////////////
boolean initialize(boolean usb, boolean bluetooth) {
Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")");
if (usb) {
initializeUSB();
}
if (bluetooth) {
initializeBluetooth();
}
return true;
}
boolean openDevice(int deviceID) {
Log.v(TAG, "openDevice deviceID=" + deviceID);
HIDDevice device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return false;
}
// Look to see if this is a USB device and we have permission to access it
UsbDevice usbDevice = device.getDevice();
if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) {
HIDDeviceOpenPending(deviceID);
try {
final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31
int flags;
if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
flags = FLAG_MUTABLE;
} else {
flags = 0;
}
Intent intent = new Intent(HIDDeviceManager.ACTION_USB_PERMISSION);
intent.setPackage(mContext.getPackageName());
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, intent, flags));
} catch (Exception e) {
Log.v(TAG, "Couldn't request permission for USB device " + usbDevice);
HIDDeviceOpenResult(deviceID, false);
}
return false;
}
try {
return device.open();
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return false;
}
int writeReport(int deviceID, byte[] report, boolean feature) {
try {
//Log.v(TAG, "writeReport deviceID=" + deviceID + " length=" + report.length);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return -1;
}
return device.writeReport(report, feature);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return -1;
}
boolean readReport(int deviceID, byte[] report, boolean feature) {
try {
//Log.v(TAG, "readReport deviceID=" + deviceID);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return false;
}
return device.readReport(report, feature);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return false;
}
void closeDevice(int deviceID) {
try {
Log.v(TAG, "closeDevice deviceID=" + deviceID);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return;
}
device.close();
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////// Native methods
//////////////////////////////////////////////////////////////////////////////////////////////////////
private native void HIDDeviceRegisterCallback();
private native void HIDDeviceReleaseCallback();
native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol, boolean bBluetooth, int reportID);
native void HIDDeviceOpenPending(int deviceID);
native void HIDDeviceOpenResult(int deviceID, boolean opened);
native void HIDDeviceDisconnected(int deviceID);
native void HIDDeviceInputReport(int deviceID, byte[] report);
native void HIDDeviceReportResponse(int deviceID, byte[] report);
}
@@ -1,354 +0,0 @@
package org.libsdl.app;
import android.hardware.usb.*;
import android.os.Build;
import android.util.Log;
import java.util.Arrays;
import java.util.Locale;
class HIDDeviceUSB implements HIDDevice {
private static final String TAG = "hidapi";
protected HIDDeviceManager mManager;
protected UsbDevice mDevice;
protected int mInterfaceIndex;
protected int mInterface;
protected int mDeviceId;
protected UsbDeviceConnection mConnection;
protected UsbEndpoint mInputEndpoint;
protected UsbEndpoint mOutputEndpoint;
protected InputThread mInputThread;
protected boolean mRunning;
protected boolean mFrozen;
protected boolean mClaimed;
public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) {
mManager = manager;
mDevice = usbDevice;
mInterfaceIndex = interface_index;
mInterface = mDevice.getInterface(mInterfaceIndex).getId();
mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier());
mRunning = false;
mClaimed = false;
}
String getIdentifier() {
return String.format(Locale.ENGLISH, "%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex);
}
@Override
public int getId() {
return mDeviceId;
}
@Override
public int getVendorId() {
return mDevice.getVendorId();
}
@Override
public int getProductId() {
return mDevice.getProductId();
}
@Override
public String getSerialNumber() {
String result = null;
try {
result = mDevice.getSerialNumber();
}
catch (SecurityException exception) {
//Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage());
}
if (result == null) {
result = "";
}
return result;
}
@Override
public int getVersion() {
return 0;
}
@Override
public String getManufacturerName() {
String result;
result = mDevice.getManufacturerName();
if (result == null) {
result = String.format("%x", getVendorId());
}
return result;
}
@Override
public String getProductName() {
String result;
result = mDevice.getProductName();
if (result == null) {
result = String.format("%x", getProductId());
}
return result;
}
@Override
public UsbDevice getDevice() {
return mDevice;
}
String getDeviceName() {
return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")";
}
@Override
public boolean open() {
mConnection = mManager.getUSBManager().openDevice(mDevice);
if (mConnection == null) {
Log.w(TAG, "Unable to open USB device " + getDeviceName());
return false;
}
// Force claim our interface
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
if (!mConnection.claimInterface(iface, true)) {
Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName());
close();
return false;
}
mClaimed = true;
// Find the endpoints
for (int j = 0; j < iface.getEndpointCount(); j++) {
UsbEndpoint endpt = iface.getEndpoint(j);
switch (endpt.getDirection()) {
case UsbConstants.USB_DIR_IN:
if (mInputEndpoint == null) {
mInputEndpoint = endpt;
}
break;
case UsbConstants.USB_DIR_OUT:
if (mOutputEndpoint == null) {
mOutputEndpoint = endpt;
}
break;
}
}
// Make sure the required endpoints were present. The original Steam Controller and the wireless dongle for it do NOT
// actually have -- or require -- output endpoints, so we need to accept only an input one for them or else we'll fall
// back to the Android system gamepad functionality (and lose our paddles et al).
if (mInputEndpoint == null) {
Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName());
mConnection.releaseInterface(iface);
close();
return false;
}
// Start listening for input
mRunning = true;
mInputThread = new InputThread();
mInputThread.start();
return true;
}
@Override
public int writeReport(byte[] report, boolean feature) {
if (mConnection == null) {
Log.w(TAG, "writeReport() called with no device connection");
return -1;
}
if (!mClaimed) {
Log.w(TAG, "writeReport() called but some other process currently owns the USB device");
return -1;
}
if (feature) {
int res = -1;
int offset = 0;
int length = report.length;
boolean skipped_report_id = false;
byte report_number = report[0];
if (report_number == 0x0) {
++offset;
--length;
skipped_report_id = true;
}
res = mConnection.controlTransfer(
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT,
0x09/*HID set_report*/,
(3/*HID feature*/ << 8) | report_number,
mInterface,
report, offset, length,
1000/*timeout millis*/);
if (res < 0) {
Log.w(TAG, "writeFeatureReport() returned " + res + " on device " + getDeviceName());
return -1;
}
if (skipped_report_id) {
++length;
}
return length;
} else {
if (mOutputEndpoint == null)
{
Log.e(TAG, "Tried to write an output report to an interface with no output endpoint!");
return -1;
}
int res = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000);
if (res != report.length) {
Log.w(TAG, "writeOutputReport() returned " + res + " on device " + getDeviceName());
}
return res;
}
}
@Override
public boolean readReport(byte[] report, boolean feature) {
int res = -1;
int offset = 0;
int length = report.length;
boolean skipped_report_id = false;
byte report_number = report[0];
if (mConnection == null) {
Log.w(TAG, "readReport() called with no device connection");
return false;
}
if (!mClaimed) {
if (feature) {
return false;
}
return true;
}
if (report_number == 0x0) {
/* Offset the return buffer by 1, so that the report ID
will remain in byte 0. */
++offset;
--length;
skipped_report_id = true;
}
res = mConnection.controlTransfer(
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN,
0x01/*HID get_report*/,
((feature ? 3/*HID feature*/ : 1/*HID Input*/) << 8) | report_number,
mInterface,
report, offset, length,
1000/*timeout millis*/);
if (res < 0) {
Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName());
return false;
}
if (skipped_report_id) {
++res;
++length;
}
byte[] data;
if (res == length) {
data = report;
} else {
data = Arrays.copyOfRange(report, 0, res);
}
mManager.HIDDeviceReportResponse(mDeviceId, data);
return true;
}
@Override
public void close() {
mRunning = false;
if (mInputThread != null) {
while (mInputThread.isAlive()) {
mInputThread.interrupt();
try {
mInputThread.join();
} catch (InterruptedException e) {
// Keep trying until we're done
}
}
mInputThread = null;
}
if (mConnection != null) {
if (mClaimed) {
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
mConnection.releaseInterface(iface);
}
mConnection.close();
mConnection = null;
mClaimed = false;
}
}
@Override
public void shutdown() {
close();
mManager = null;
}
@Override
public void setFrozen(boolean frozen) {
mFrozen = frozen;
/* If we have a valid device connection and the claim state doesn't match what we want, try to correct that. */
if (mConnection != null && mClaimed == mFrozen) {
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
if (frozen) {
mClaimed = !mConnection.releaseInterface(iface);
if (mClaimed) {
Log.e(TAG, "Tried to release claim on USB device, but failed!");
}
} else {
mClaimed = mConnection.claimInterface(iface, true);
if (!mClaimed) {
Log.e(TAG, "Tried to regain claim on USB device, but failed!");
}
}
}
}
protected class InputThread extends Thread {
@Override
public void run() {
int packetSize = mInputEndpoint.getMaxPacketSize();
byte[] packet = new byte[packetSize];
while (mRunning) {
int r;
try
{
r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000);
}
catch (Exception e)
{
Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e);
break;
}
if (r < 0) {
// Could be a timeout or an I/O error
}
if (r > 0) {
byte[] data;
if (r == packetSize) {
data = packet;
} else {
data = Arrays.copyOfRange(packet, 0, r);
}
if (!mFrozen) {
mManager.HIDDeviceInputReport(mDeviceId, data);
}
}
}
}
}
}
@@ -1,90 +0,0 @@
package org.libsdl.app;
import android.app.Activity;
import android.content.Context;
import java.lang.reflect.Method;
/**
SDL library initialization
*/
public class SDL {
// This function should be called first and sets up the native code
// so it can call into the Java classes
static public void setupJNI() {
SDLActivity.nativeSetupJNI();
SDLAudioManager.nativeSetupJNI();
SDLControllerManager.nativeSetupJNI();
}
// This function should be called each time the activity is started
static public void initialize() {
setContext(null);
SDLActivity.initialize();
SDLAudioManager.initialize();
SDLControllerManager.initialize();
}
// This function stores the current activity (SDL or not)
static public void setContext(Activity context) {
SDLAudioManager.setContext(context);
mContext = context;
}
static public Activity getContext() {
return mContext;
}
static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
loadLibrary(libraryName, mContext);
}
static void loadLibrary(String libraryName, Context context) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
if (libraryName == null) {
throw new NullPointerException("No library name provided.");
}
try {
// Let's see if we have ReLinker available in the project. This is necessary for
// some projects that have huge numbers of local libraries bundled, and thus may
// trip a bug in Android's native library loader which ReLinker works around. (If
// loadLibrary works properly, ReLinker will simply use the normal Android method
// internally.)
//
// To use ReLinker, just add it as a dependency. For more information, see
// https://github.com/KeepSafe/ReLinker for ReLinker's repository.
//
Class<?> relinkClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
Class<?> relinkListenerClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
Class<?> contextClass = context.getClassLoader().loadClass("android.content.Context");
Class<?> stringClass = context.getClassLoader().loadClass("java.lang.String");
// Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if
// they've changed during updates.
Method forceMethod = relinkClass.getDeclaredMethod("force");
Object relinkInstance = forceMethod.invoke(null);
Class<?> relinkInstanceClass = relinkInstance.getClass();
// Actually load the library!
Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass);
loadMethod.invoke(relinkInstance, context, libraryName, null, null);
}
catch (final Throwable e) {
// Fall back
try {
System.loadLibrary(libraryName);
}
catch (final UnsatisfiedLinkError ule) {
throw ule;
}
catch (final SecurityException se) {
throw se;
}
}
}
protected static Activity mContext;
}
File diff suppressed because it is too large Load Diff
@@ -1,126 +0,0 @@
package org.libsdl.app;
import android.content.Context;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioManager;
import android.os.Build;
import android.util.Log;
import java.util.Arrays;
import java.util.ArrayList;
class SDLAudioManager {
protected static final String TAG = "SDLAudio";
protected static Context mContext;
private static AudioDeviceCallback mAudioDeviceCallback;
static void initialize() {
mAudioDeviceCallback = null;
if(Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */)
{
mAudioDeviceCallback = new AudioDeviceCallback() {
@Override
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
for (AudioDeviceInfo deviceInfo : addedDevices) {
nativeAddAudioDevice(deviceInfo.isSink(), deviceInfo.getProductName().toString(), deviceInfo.getId());
}
}
@Override
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
for (AudioDeviceInfo deviceInfo : removedDevices) {
nativeRemoveAudioDevice(deviceInfo.isSink(), deviceInfo.getId());
}
}
};
}
}
static void setContext(Context context) {
mContext = context;
}
static void release(Context context) {
// no-op atm
}
// Audio
private static AudioDeviceInfo getInputAudioDeviceInfo(int deviceId) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) {
if (deviceInfo.getId() == deviceId) {
return deviceInfo;
}
}
}
return null;
}
private static AudioDeviceInfo getPlaybackAudioDeviceInfo(int deviceId) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
if (deviceInfo.getId() == deviceId) {
return deviceInfo;
}
}
}
return null;
}
static void registerAudioDeviceCallback() {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
// get an initial list now, before hotplug callbacks fire.
for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
if (dev.getType() == AudioDeviceInfo.TYPE_TELEPHONY) {
continue; // Device cannot be opened
}
nativeAddAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId());
}
for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) {
nativeAddAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId());
}
audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null);
}
}
static void unregisterAudioDeviceCallback() {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback);
}
}
/** This method is called by SDL using JNI. */
static void audioSetThreadPriority(boolean recording, int device_id) {
try {
/* Set thread name */
if (recording) {
Thread.currentThread().setName("SDLAudioC" + device_id);
} else {
Thread.currentThread().setName("SDLAudioP" + device_id);
}
/* Set thread priority */
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
} catch (Exception e) {
Log.v(TAG, "modify thread properties failed " + e.toString());
}
}
static native void nativeSetupJNI();
static native void nativeRemoveAudioDevice(boolean recording, int deviceId);
static native void nativeAddAudioDevice(boolean recording, String name, int deviceId);
}
File diff suppressed because it is too large Load Diff
@@ -1,66 +0,0 @@
package org.libsdl.app;
import android.content.*;
import android.text.InputType;
import android.view.*;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
/* This is a fake invisible editor view that receives the input and defines the
* pan&scan region
*/
public class SDLDummyEdit extends View implements View.OnKeyListener
{
InputConnection ic;
int input_type;
SDLDummyEdit(Context context) {
super(context);
setFocusableInTouchMode(true);
setFocusable(true);
setOnKeyListener(this);
}
void setInputType(int input_type) {
this.input_type = input_type;
}
@Override
public boolean onCheckIsTextEditor() {
return true;
}
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
return SDLActivity.handleKeyEvent(v, keyCode, event, ic);
}
//
@Override
public boolean onKeyPreIme (int keyCode, KeyEvent event) {
// As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event
// FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639
// FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not
// FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout
// FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
// FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :)
if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) {
SDLActivity.onNativeKeyboardFocusLost();
}
}
return super.onKeyPreIme(keyCode, event);
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
ic = new SDLInputConnection(this, true);
outAttrs.inputType = input_type;
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI |
EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */;
return ic;
}
}
@@ -1,136 +0,0 @@
package org.libsdl.app;
import android.content.*;
import android.os.Build;
import android.text.Editable;
import android.view.*;
import android.view.inputmethod.BaseInputConnection;
import android.widget.EditText;
class SDLInputConnection extends BaseInputConnection
{
protected EditText mEditText;
protected String mCommittedText = "";
SDLInputConnection(View targetView, boolean fullEditor) {
super(targetView, fullEditor);
mEditText = new EditText(SDL.getContext());
}
@Override
public Editable getEditable() {
return mEditText.getEditableText();
}
@Override
public boolean sendKeyEvent(KeyEvent event) {
/*
* This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard)
* However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses
* and so we need to generate them ourselves in commitText. To avoid duplicates on the handful of keys
* that still do, we empty this out.
*/
/*
* Return DOES still generate a key event, however. So rather than using it as the 'click a button' key
* as we do with physical keyboards, let's just use it to hide the keyboard.
*/
if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
if (SDLActivity.onNativeSoftReturnKey()) {
return true;
}
}
return super.sendKeyEvent(event);
}
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
if (!super.commitText(text, newCursorPosition)) {
return false;
}
updateText();
return true;
}
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
if (!super.setComposingText(text, newCursorPosition)) {
return false;
}
updateText();
return true;
}
@Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
// Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection
// and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
if (beforeLength > 0 && afterLength == 0) {
// backspace(s)
while (beforeLength-- > 0) {
nativeGenerateScancodeForUnichar('\b');
}
return true;
}
if (!super.deleteSurroundingText(beforeLength, afterLength)) {
return false;
}
updateText();
return true;
}
protected void updateText() {
final Editable content = getEditable();
if (content == null) {
return;
}
String text = content.toString();
int compareLength = Math.min(text.length(), mCommittedText.length());
int matchLength, offset;
/* Backspace over characters that are no longer in the string */
for (matchLength = 0; matchLength < compareLength; ) {
int codePoint = mCommittedText.codePointAt(matchLength);
if (codePoint != text.codePointAt(matchLength)) {
break;
}
matchLength += Character.charCount(codePoint);
}
/* FIXME: This doesn't handle graphemes, like '🌬️' */
for (offset = matchLength; offset < mCommittedText.length(); ) {
int codePoint = mCommittedText.codePointAt(offset);
nativeGenerateScancodeForUnichar('\b');
offset += Character.charCount(codePoint);
}
if (matchLength < text.length()) {
String pendingText = text.subSequence(matchLength, text.length()).toString();
if (!SDLActivity.dispatchingKeyEvent()) {
for (offset = 0; offset < pendingText.length(); ) {
int codePoint = pendingText.codePointAt(offset);
if (codePoint == '\n') {
if (SDLActivity.onNativeSoftReturnKey()) {
return;
}
}
/* Higher code points don't generate simulated scancodes */
if (codePoint > 0 && codePoint < 128) {
nativeGenerateScancodeForUnichar((char)codePoint);
}
offset += Character.charCount(codePoint);
}
}
SDLInputConnection.nativeCommitText(pendingText, 0);
}
mCommittedText = text;
}
public static native void nativeCommitText(String text, int newCursorPosition);
public static native void nativeGenerateScancodeForUnichar(char c);
}
@@ -1,32 +0,0 @@
package org.libsdl.app;
import android.hardware.Sensor;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
// This class coordinates synchronized access to sensor manager registration
//
// This prevents a java.util.ConcurrentModificationException exception on
// Android 16, specifically on the Samsung Tab S9 Ultra.
class SDLSensorManager
{
static private SDLSensorManager mManager = new SDLSensorManager();
public static void registerListener(SensorManager manager, SensorEventListener listener, Sensor sensor, int samplingPeriodUs) {
mManager.RegisterListener(manager, listener, sensor, samplingPeriodUs);
}
public static void unregisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor) {
mManager.UnregisterListener(manager, listener, sensor);
}
private synchronized void RegisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor, int samplingPeriodUs) {
manager.registerListener(listener, sensor, samplingPeriodUs, null);
}
private synchronized void UnregisterListener(SensorManager manager, SensorEventListener listener, Sensor sensor) {
manager.unregisterListener(listener, sensor);
}
}
@@ -1,469 +0,0 @@
package org.libsdl.app;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.graphics.Insets;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.ScaleGestureDetector;
/**
SDLSurface. This is what we draw on, so we need to know when it's created
in order to do anything useful.
Because of this, that's where we set up the SDL thread
*/
public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
View.OnApplyWindowInsetsListener, View.OnKeyListener, View.OnTouchListener,
SensorEventListener, ScaleGestureDetector.OnScaleGestureListener {
private static native void auroraNativeSetSurfaceReady(boolean ready);
// Sensors
protected SensorManager mSensorManager;
protected Display mDisplay;
// Keep track of the surface size to normalize touch events
protected float mWidth, mHeight;
// Is SurfaceView ready for rendering
protected boolean mIsSurfaceReady;
// Is on-screen keyboard visible
protected boolean mKeyboardVisible;
// Pinch events
private final ScaleGestureDetector scaleGestureDetector;
// Startup
protected SDLSurface(Context context) {
super(context);
getHolder().addCallback(this);
scaleGestureDetector = new ScaleGestureDetector(context, this);
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
setOnApplyWindowInsetsListener(this);
setOnKeyListener(this);
setOnTouchListener(this);
mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
setOnGenericMotionListener(SDLActivity.getMotionListener());
// Some arbitrary defaults to avoid a potential division by zero
mWidth = 1.0f;
mHeight = 1.0f;
mIsSurfaceReady = false;
}
protected void handlePause() {
enableSensor(Sensor.TYPE_ACCELEROMETER, false);
}
protected void handleResume() {
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
setOnApplyWindowInsetsListener(this);
setOnKeyListener(this);
setOnTouchListener(this);
enableSensor(Sensor.TYPE_ACCELEROMETER, true);
}
protected Surface getNativeSurface() {
return getHolder().getSurface();
}
// Called when we have a valid drawing surface
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.v("SDL", "surfaceCreated()");
auroraNativeSetSurfaceReady(false);
SDLActivity.onNativeSurfaceCreated();
}
// Called when we lose the surface
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.v("SDL", "surfaceDestroyed()");
auroraNativeSetSurfaceReady(false);
// Transition to pause, if needed
SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
SDLActivity.handleNativeState();
mIsSurfaceReady = false;
SDLActivity.onNativeSurfaceDestroyed();
}
// Called when the surface is resized
@Override
public void surfaceChanged(SurfaceHolder holder,
int format, int width, int height) {
Log.v("SDL", "surfaceChanged()");
if (SDLActivity.mSingleton == null) {
return;
}
mWidth = width;
mHeight = height;
int nDeviceWidth = width;
int nDeviceHeight = height;
float density = 1.0f;
try
{
DisplayMetrics realMetrics = new DisplayMetrics();
mDisplay.getRealMetrics( realMetrics );
nDeviceWidth = realMetrics.widthPixels;
nDeviceHeight = realMetrics.heightPixels;
// Use densityDpi instead of density to more closely match what the UI scale is
density = (float)realMetrics.densityDpi / 160.0f;
} catch(Exception ignored) {
}
synchronized(SDLActivity.getContext()) {
// In case we're waiting on a size change after going fullscreen, send a notification.
SDLActivity.getContext().notifyAll();
}
Log.v("SDL", "Window size: " + width + "x" + height);
Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, density, mDisplay.getRefreshRate());
SDLActivity.onNativeResize();
// Prevent a screen distortion glitch,
// for instance when the device is in Landscape and a Portrait App is resumed.
boolean skip = false;
int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
if (mWidth > mHeight) {
skip = true;
}
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
if (mWidth < mHeight) {
skip = true;
}
}
// Special Patch for Square Resolution: Black Berry Passport
if (skip) {
double min = Math.min(mWidth, mHeight);
double max = Math.max(mWidth, mHeight);
if (max / min < 1.20) {
Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
skip = false;
}
}
// Don't skip if we might be multi-window or have popup dialogs
if (skip) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
skip = false;
}
}
if (skip) {
Log.v("SDL", "Skip .. Surface is not ready.");
mIsSurfaceReady = false;
return;
}
/* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
SDLActivity.onNativeSurfaceChanged();
/* Surface is ready */
mIsSurfaceReady = true;
auroraNativeSetSurfaceReady(true);
SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
SDLActivity.handleNativeState();
}
// Window inset
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
if (Build.VERSION.SDK_INT >= 30 /* Android 11 (R) */) {
Insets combined = insets.getInsets(WindowInsets.Type.systemBars() |
WindowInsets.Type.systemGestures() |
WindowInsets.Type.mandatorySystemGestures() |
WindowInsets.Type.tappableElement() |
WindowInsets.Type.displayCutout());
SDLActivity.onNativeInsetsChanged(combined.left, combined.right, combined.top, combined.bottom);
if (insets.isVisible(WindowInsets.Type.ime())) {
if (!mKeyboardVisible) {
mKeyboardVisible = true;
SDLActivity.onNativeScreenKeyboardShown();
}
} else {
if (mKeyboardVisible) {
mKeyboardVisible = false;
SDLActivity.onNativeScreenKeyboardHidden();
}
}
}
// Pass these to any child views in case they need them
return insets;
}
// Key events
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
return SDLActivity.handleKeyEvent(v, keyCode, event, null);
}
private float getNormalizedX(float x)
{
if (mWidth <= 1) {
return 0.5f;
} else {
return (x / (mWidth - 1));
}
}
private float getNormalizedY(float y)
{
if (mHeight <= 1) {
return 0.5f;
} else {
return (y / (mHeight - 1));
}
}
// Touch events
@Override
public boolean onTouch(View v, MotionEvent event) {
/* Ref: http://developer.android.com/training/gestures/multi.html */
int touchDevId = event.getDeviceId();
final int pointerCount = event.getPointerCount();
int action = event.getActionMasked();
int pointerId;
int i = 0;
float x,y,p;
if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN)
i = event.getActionIndex();
do {
int toolType = event.getToolType(i);
if (toolType == MotionEvent.TOOL_TYPE_MOUSE) {
int buttonState = event.getButtonState();
boolean relative = false;
// We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
// if we are. We'll leverage our existing mouse motion listener
SDLGenericMotionListener_API14 motionListener = SDLActivity.getMotionListener();
x = motionListener.getEventX(event, i);
y = motionListener.getEventY(event, i);
relative = motionListener.inRelativeMode();
SDLActivity.onNativeMouse(buttonState, action, x, y, relative);
} else if (toolType == MotionEvent.TOOL_TYPE_STYLUS || toolType == MotionEvent.TOOL_TYPE_ERASER) {
pointerId = event.getPointerId(i);
x = event.getX(i);
y = event.getY(i);
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
// BUTTON_STYLUS_PRIMARY is 2^5, so shift by 4, and apply SDL_PEN_INPUT_DOWN/SDL_PEN_INPUT_ERASER_TIP
int buttonState = (event.getButtonState() >> 4) | (1 << (toolType == MotionEvent.TOOL_TYPE_STYLUS ? 0 : 30));
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
buttonState |= 0x08;
}
SDLActivity.onNativePen(pointerId, SDLActivity.getMotionListener().getPenDeviceType(event.getDevice()), buttonState, action, x, y, p);
} else { // MotionEvent.TOOL_TYPE_FINGER or MotionEvent.TOOL_TYPE_UNKNOWN
pointerId = event.getPointerId(i);
x = getNormalizedX(event.getX(i));
y = getNormalizedY(event.getY(i));
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
SDLActivity.onNativeTouch(touchDevId, pointerId, action, x, y, p);
}
// Non-primary up/down
if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN)
break;
} while (++i < pointerCount);
scaleGestureDetector.onTouchEvent(event);
return true;
}
// Sensor events
protected void enableSensor(int sensortype, boolean enabled) {
// TODO: This uses getDefaultSensor - what if we have >1 accels?
if (enabled) {
SDLSensorManager.registerListener(mSensorManager, this,
mSensorManager.getDefaultSensor(sensortype),
SensorManager.SENSOR_DELAY_GAME);
} else {
SDLSensorManager.unregisterListener(mSensorManager, this,
mSensorManager.getDefaultSensor(sensortype));
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// TODO
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
// Since we may have an orientation set, we won't receive onConfigurationChanged events.
// We thus should check here.
int newRotation;
float x, y;
switch (mDisplay.getRotation()) {
case Surface.ROTATION_0:
default:
x = event.values[0];
y = event.values[1];
newRotation = 0;
break;
case Surface.ROTATION_90:
x = -event.values[1];
y = event.values[0];
newRotation = 90;
break;
case Surface.ROTATION_180:
x = -event.values[0];
y = -event.values[1];
newRotation = 180;
break;
case Surface.ROTATION_270:
x = event.values[1];
y = -event.values[0];
newRotation = 270;
break;
}
if (newRotation != SDLActivity.mCurrentRotation) {
SDLActivity.mCurrentRotation = newRotation;
SDLActivity.onNativeRotationChanged(newRotation);
}
SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
y / SensorManager.GRAVITY_EARTH,
event.values[2] / SensorManager.GRAVITY_EARTH);
}
}
// Prevent android internal NullPointerException (https://github.com/libsdl-org/SDL/issues/13306)
@Override
public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
try {
return super.onResolvePointerIcon(event, pointerIndex);
} catch (NullPointerException e) {
return null;
}
}
// Captured pointer events for API 26.
@Override
public boolean onCapturedPointerEvent(MotionEvent event)
{
int action = event.getActionMasked();
int pointerCount = event.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
float x, y;
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, i);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, i);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
case MotionEvent.ACTION_MOVE:
x = event.getX(i);
y = event.getY(i);
SDLActivity.onNativeMouse(0, action, x, y, true);
return true;
case MotionEvent.ACTION_BUTTON_PRESS:
case MotionEvent.ACTION_BUTTON_RELEASE:
// Change our action value to what SDL's code expects.
if (action == MotionEvent.ACTION_BUTTON_PRESS) {
action = MotionEvent.ACTION_DOWN;
} else { /* MotionEvent.ACTION_BUTTON_RELEASE */
action = MotionEvent.ACTION_UP;
}
x = event.getX(i);
y = event.getY(i);
int button = event.getButtonState();
SDLActivity.onNativeMouse(button, action, x, y, true);
return true;
}
}
return false;
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scale = detector.getScaleFactor();
SDLActivity.onNativePinchUpdate(scale);
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
SDLActivity.onNativePinchStart();
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
SDLActivity.onNativePinchEnd();
}
}
-115
View File
@@ -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"
+2 -2
View File
@@ -23,12 +23,12 @@ BEGIN
BEGIN
VALUE "CompanyName", "@DUSK_COMPANY_NAME@\0"
VALUE "FileDescription", "@DUSK_FILE_DESCRIPTION@\0"
VALUE "FileVersion", "@DUSK_VERSION_STRING@\0"
VALUE "FileVersion", "@BOREALIS_APP_VERSION@\0"
VALUE "InternalName", "dusklight\0"
VALUE "LegalCopyright", "@DUSK_COPYRIGHT@\0"
VALUE "OriginalFilename", "dusklight.exe\0"
VALUE "ProductName", "@DUSK_PRODUCT_NAME@\0"
VALUE "ProductVersion", "@DUSK_VERSION_STRING@\0"
VALUE "ProductVersion", "@BOREALIS_APP_VERSION@\0"
END
END
BLOCK "VarFileInfo"
+1 -6
View File
@@ -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
View File
@@ -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
View File
@@ -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
+42
View File
@@ -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
+2 -2
View File
@@ -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), \
+49 -11
View File
@@ -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);
+6 -10
View File
@@ -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);
+5 -10
View File
@@ -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
View File
@@ -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
View File
@@ -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