mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-08-13 19:44:32 -04:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0da58ac91d | |||
| c29cafa8a2 | |||
| 5e669c8f0f | |||
| 3e625cf006 | |||
| c6f497b525 | |||
| 79d3c7c76a | |||
| 16dd1118d1 | |||
| e9918cc8ed | |||
| 058304dc26 | |||
| d55b44b5a5 | |||
| 7434a0f834 | |||
| 469575fd65 | |||
| 4883c11182 | |||
| 7ac2340312 | |||
| 3352ea3dde | |||
| 82e2fd4bea | |||
| fcd9dd0f70 | |||
| 8afe479d0f | |||
| c5933be631 | |||
| 928e9b4bc7 | |||
| 3ac0fcb156 | |||
| 6481d12a3d | |||
| 2c7ab755a0 | |||
| cf8ab52f55 | |||
| 960d016113 | |||
| 13b3b68fe5 | |||
| 4604304305 | |||
| 794bb130e2 | |||
| e02251a669 | |||
| 2c9f814ec6 | |||
| 76079b6294 | |||
| 519305b7d4 |
@@ -76,7 +76,7 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dusklight-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}}
|
||||
name: dusklight-${{env.APP_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}}
|
||||
path: |
|
||||
build/install/Dusklight-*.AppImage
|
||||
build/install/debug.tar.*
|
||||
@@ -145,7 +145,7 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dusklight-${{env.DUSK_VERSION}}-${{matrix.artifact_name}}
|
||||
name: dusklight-${{env.APP_VERSION}}-${{matrix.artifact_name}}
|
||||
path: |
|
||||
build/install/Dusklight.app
|
||||
build/install/debug.tar.*
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dusklight-${{env.DUSK_VERSION}}-android-${{matrix.artifact_arch}}
|
||||
name: dusklight-${{env.APP_VERSION}}-android-${{matrix.artifact_arch}}
|
||||
path: upload/
|
||||
|
||||
build-windows:
|
||||
@@ -283,7 +283,7 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: dusklight-${{env.DUSK_VERSION}}-win32-msvc-${{matrix.artifact_arch}}
|
||||
name: dusklight-${{env.APP_VERSION}}-win32-msvc-${{matrix.artifact_arch}}
|
||||
path: |
|
||||
build/install/*.exe
|
||||
build/install/*.dll
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
[submodule "extern/aurora"]
|
||||
path = extern/aurora
|
||||
url = https://github.com/encounter/aurora.git
|
||||
[submodule "extern/borealis"]
|
||||
path = extern/borealis
|
||||
url = https://github.com/encounter/borealis.git
|
||||
|
||||
+34
-140
@@ -5,10 +5,11 @@ if (NOT CMAKE_BUILD_TYPE)
|
||||
"Build type options: Debug Release RelWithDebInfo MinSizeRel" FORCE)
|
||||
endif ()
|
||||
|
||||
include(cmake/DetectVersion.cmake)
|
||||
detect_version()
|
||||
include(extern/borealis/cmake/DetectVersion.cmake)
|
||||
borealis_detect_version()
|
||||
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
|
||||
project(dusklight LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING})
|
||||
project(dusklight LANGUAGES C CXX VERSION ${BOREALIS_APP_VERSION})
|
||||
|
||||
if (APPLE)
|
||||
enable_language(OBJC OBJCXX)
|
||||
endif ()
|
||||
@@ -32,6 +33,7 @@ set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON)
|
||||
set(CMAKE_COLOR_DIAGNOSTICS ON)
|
||||
|
||||
# Folder-based instead of target-based organization
|
||||
# in Visual Studio and Xcode generators
|
||||
@@ -74,6 +76,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 +89,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 +152,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 +196,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 +225,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 +244,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 +279,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 +297,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 +323,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 +387,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 +468,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)
|
||||
@@ -611,6 +519,7 @@ if (DUSK_ENABLE_CODE_MODS AND CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR
|
||||
add_subdirectory(mods/ao_mod)
|
||||
add_subdirectory(mods/shadow_mod)
|
||||
add_subdirectory(mods/window_demo)
|
||||
add_subdirectory(mods/basic_cosmetics_mod)
|
||||
endif ()
|
||||
|
||||
if (APPLE)
|
||||
@@ -644,8 +553,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"
|
||||
@@ -678,21 +587,6 @@ if (APPLE)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||
find_library(APPKIT_FRAMEWORK AppKit REQUIRED)
|
||||
target_sources(dusklight PRIVATE src/dusk/file_select_macos.mm)
|
||||
set_source_files_properties(src/dusk/file_select_macos.mm PROPERTIES COMPILE_FLAGS -fobjc-arc)
|
||||
target_link_libraries(dusklight PRIVATE ${APPKIT_FRAMEWORK})
|
||||
endif ()
|
||||
|
||||
if (IOS)
|
||||
find_library(UIKIT_FRAMEWORK UIKit REQUIRED)
|
||||
find_library(UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK UniformTypeIdentifiers REQUIRED)
|
||||
target_sources(dusklight PRIVATE src/dusk/ios/FileSelectDialog.m)
|
||||
set_source_files_properties(src/dusk/ios/FileSelectDialog.m PROPERTIES COMPILE_FLAGS -fobjc-arc)
|
||||
target_link_libraries(dusklight PRIVATE ${UIKIT_FRAMEWORK} ${UNIFORM_TYPE_IDENTIFIERS_FRAMEWORK})
|
||||
endif ()
|
||||
|
||||
include(extern/aurora/cmake/AuroraCopyRuntimeDLLs.cmake)
|
||||
aurora_copy_runtime_dlls(dusklight)
|
||||
|
||||
|
||||
+4
-4
@@ -28,12 +28,12 @@
|
||||
"cacheVariables": {
|
||||
"CMAKE_C_COMPILER_LAUNCHER": "sccache",
|
||||
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache",
|
||||
"DUSK_ENABLE_SENTRY_NATIVE": {
|
||||
"BOREALIS_ENABLE_SENTRY": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
},
|
||||
"DUSK_SENTRY_DSN": "$env{SENTRY_DSN}",
|
||||
"DUSK_SENTRY_ENVIRONMENT": "production",
|
||||
"BOREALIS_SENTRY_DSN": "$env{SENTRY_DSN}",
|
||||
"BOREALIS_SENTRY_ENVIRONMENT": "production",
|
||||
"Rust_RUSTUP_INSTALL_MISSING_TARGET": {
|
||||
"type": "BOOL",
|
||||
"value": true
|
||||
@@ -448,7 +448,7 @@
|
||||
"ci"
|
||||
],
|
||||
"cacheVariables": {
|
||||
"DUSK_ENABLE_SENTRY_NATIVE": {
|
||||
"BOREALIS_ENABLE_SENTRY": {
|
||||
"type": "BOOL",
|
||||
"value": false
|
||||
}
|
||||
|
||||
@@ -23,5 +23,5 @@ cp -r platforms/freedesktop/{16x16,32x32,48x48,64x64,128x128,256x256,512x512,102
|
||||
cp platforms/freedesktop/dev.twilitrealm.dusk.desktop build/appdir/usr/share/applications
|
||||
|
||||
cd build/install
|
||||
VERSION="$DUSK_VERSION" NO_STRIP=1 "$linuxdeploy" \
|
||||
VERSION="$APP_VERSION" NO_STRIP=1 "$linuxdeploy" \
|
||||
-l "$lib_dir/libusb-1.0.so" --appdir "$build_dir/appdir" --output appimage
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
# Version detection shared by the main build and the mod SDK (sdk/CMakeLists.txt)
|
||||
include_guard(GLOBAL)
|
||||
|
||||
get_filename_component(_DUSK_VERSION_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
|
||||
|
||||
set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN")
|
||||
set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment")
|
||||
|
||||
set(DUSK_VERSION_OVERRIDE "" CACHE STRING "Override version string (skips git detection and format validation)")
|
||||
|
||||
macro(detect_version)
|
||||
if (DUSK_VERSION_OVERRIDE)
|
||||
set(DUSK_WC_DESCRIBE "${DUSK_VERSION_OVERRIDE}")
|
||||
set(DUSK_VERSION_STRING "0.0.0.0")
|
||||
set(DUSK_SHORT_VERSION_STRING "0.0.0")
|
||||
set(DUSK_VERSION_CODE "1")
|
||||
set(DUSK_WC_REVISION "")
|
||||
set(DUSK_WC_BRANCH "")
|
||||
set(DUSK_WC_DATE "")
|
||||
message(STATUS "Dusklight version overridden to ${DUSK_WC_DESCRIBE}")
|
||||
else ()
|
||||
# obtain revision info from git
|
||||
find_package(Git)
|
||||
if (GIT_FOUND)
|
||||
# make sure version information gets re-run when the current Git HEAD changes
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD
|
||||
OUTPUT_VARIABLE dusk_git_head_filename
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
get_filename_component(dusk_git_head_filename "${dusk_git_head_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_filename}")
|
||||
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --symbolic-full-name HEAD
|
||||
OUTPUT_VARIABLE dusk_git_head_symbolic
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT}
|
||||
COMMAND ${GIT_EXECUTABLE} rev-parse --git-path ${dusk_git_head_symbolic}
|
||||
OUTPUT_VARIABLE dusk_git_head_symbolic_filename
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
get_filename_component(dusk_git_head_symbolic_filename "${dusk_git_head_symbolic_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_symbolic_filename}")
|
||||
|
||||
# defines DUSK_WC_REVISION
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
|
||||
OUTPUT_VARIABLE DUSK_WC_REVISION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
# defines DUSK_WC_DESCRIBE
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} describe --tags --long --dirty --match "v*"
|
||||
OUTPUT_VARIABLE DUSK_WC_DESCRIBE
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
|
||||
# remove the git hash, then collapse a clean "-0" suffix only
|
||||
string(REGEX REPLACE "-[^-]+(-dirty|)$" "\\1" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
|
||||
string(REGEX REPLACE "-0$" "" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
|
||||
|
||||
# defines DUSK_WC_BRANCH
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
|
||||
OUTPUT_VARIABLE DUSK_WC_BRANCH
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
# defines DUSK_WC_DATE
|
||||
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} log -1 --format=%ad
|
||||
OUTPUT_VARIABLE DUSK_WC_DATE
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE)
|
||||
else ()
|
||||
message(STATUS "Unable to find git, commit information will not be available")
|
||||
endif ()
|
||||
|
||||
if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$")
|
||||
set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
|
||||
set(_ver_major ${CMAKE_MATCH_1})
|
||||
set(_ver_minor ${CMAKE_MATCH_2})
|
||||
set(_ver_patch ${CMAKE_MATCH_3})
|
||||
set(DUSK_VERSION_TWEAK "0")
|
||||
if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$")
|
||||
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
|
||||
elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$")
|
||||
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
|
||||
endif ()
|
||||
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}")
|
||||
if (DUSK_VERSION_TWEAK GREATER 999)
|
||||
set(_tweak 999)
|
||||
else ()
|
||||
set(_tweak ${DUSK_VERSION_TWEAK})
|
||||
endif ()
|
||||
# encoding: major*1e7 + minor*1e5 + patch*1e3 + tweak; collision-free for major<210, minor<100, patch<100, tweak<=999
|
||||
math(EXPR DUSK_VERSION_CODE
|
||||
"${_ver_major} * 10000000 + ${_ver_minor} * 100000 + ${_ver_patch} * 1000 + ${_tweak}")
|
||||
else ()
|
||||
set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION")
|
||||
set(DUSK_VERSION_STRING "0.0.0.0")
|
||||
set(DUSK_SHORT_VERSION_STRING "0.0.0")
|
||||
set(DUSK_VERSION_CODE "1")
|
||||
endif ()
|
||||
|
||||
endif ()
|
||||
|
||||
# Add version information to CI environment variables
|
||||
if (DEFINED ENV{GITHUB_ENV})
|
||||
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION=${DUSK_WC_DESCRIBE}\n")
|
||||
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION_CODE=${DUSK_VERSION_CODE}\n")
|
||||
endif ()
|
||||
message(STATUS "Dusklight version set to ${DUSK_WC_DESCRIBE}")
|
||||
endmacro()
|
||||
|
||||
# Sets PLATFORM_NAME and configures version.h into the caller's binary dir.
|
||||
macro(configure_version_header)
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL Windows)
|
||||
set(PLATFORM_NAME win32)
|
||||
elseif (CMAKE_SYSTEM_NAME STREQUAL Darwin)
|
||||
if (IOS)
|
||||
set(PLATFORM_NAME ios)
|
||||
elseif (TVOS)
|
||||
set(PLATFORM_NAME tvos)
|
||||
else ()
|
||||
set(PLATFORM_NAME macos)
|
||||
endif ()
|
||||
else ()
|
||||
string(TOLOWER CMAKE_SYSTEM_NAME PLATFORM_NAME)
|
||||
endif ()
|
||||
|
||||
configure_file(${_DUSK_VERSION_ROOT}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
|
||||
endmacro()
|
||||
+101
-8
@@ -209,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`)
|
||||
|
||||
@@ -219,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).
|
||||
@@ -340,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,
|
||||
@@ -543,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`.
|
||||
|
||||
---
|
||||
|
||||
## Hooking Game Functions
|
||||
@@ -845,6 +938,6 @@ const char* nativeDir = svc_host->native_dir(mod_ctx); // read-only
|
||||
```
|
||||
|
||||
Libraries loaded explicitly by the mod remain its responsibility: stop their threads and unload them during
|
||||
`mod_shutdown`. Do not write into `native_dir`; use `mod_dir` for writable state. Native library namespaces are
|
||||
process-wide on some platforms, so two mods cannot safely assume that incompatible libraries with the same filename
|
||||
will remain isolated.
|
||||
`mod_shutdown`. Do not write into `native_dir`; use `data_dir` for persistent storage or `mod_dir` for temporary
|
||||
(session) storage. Native library namespaces are process-wide on some platforms, so two mods cannot safely assume that
|
||||
incompatible libraries with the same filename will remain isolated.
|
||||
|
||||
Vendored
+1
-1
Submodule extern/aurora updated: 0bddb86249...8005d336ab
+1
Submodule extern/borealis added at 6fd955e7e6
+7
-18
@@ -1421,30 +1421,21 @@ set(DUSK_FILES
|
||||
src/dusk/achievements.cpp
|
||||
src/dusk/action_bindings.cpp
|
||||
src/dusk/action_bindings.h
|
||||
src/dusk/android_frame_rate.cpp
|
||||
src/dusk/android_frame_rate.hpp
|
||||
src/dusk/asserts.cpp
|
||||
src/dusk/autosave.cpp
|
||||
src/dusk/config.cpp
|
||||
src/dusk/config.hpp
|
||||
src/dusk/crash_handler.cpp
|
||||
src/dusk/crash_reporting.cpp
|
||||
src/dusk/data.cpp
|
||||
src/dusk/data.hpp
|
||||
src/dusk/discord.cpp
|
||||
src/dusk/discord.hpp
|
||||
src/dusk/discord_presence.cpp
|
||||
src/dusk/dvd_asset.cpp
|
||||
src/dusk/dvd_asset.hpp
|
||||
src/dusk/extras.c
|
||||
src/dusk/file_select.cpp
|
||||
src/dusk/file_select.hpp
|
||||
src/dusk/frame_interpolation.cpp
|
||||
src/dusk/game_clock.cpp
|
||||
src/dusk/gamepad_color.cpp
|
||||
src/dusk/globals.cpp
|
||||
src/dusk/gyro.cpp
|
||||
src/dusk/http/http.hpp
|
||||
src/dusk/imgui/ImGuiActorSpawner.cpp
|
||||
src/dusk/imgui/ImGuiBloomWindow.cpp
|
||||
src/dusk/imgui/ImGuiBloomWindow.hpp
|
||||
@@ -1502,7 +1493,13 @@ set(DUSK_FILES
|
||||
src/dusk/mods/svc/ui.hpp
|
||||
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 +1575,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
|
||||
)
|
||||
|
||||
@@ -228,7 +228,7 @@
|
||||
ninjaFlags = [ "dusklight" ];
|
||||
|
||||
cmakeFlags = [
|
||||
"-DDUSK_VERSION_OVERRIDE=${versionSuffix}"
|
||||
"-DBOREALIS_APP_VERSION_OVERRIDE=${versionSuffix}"
|
||||
"-DFETCHCONTENT_FULLY_DISCONNECTED=ON"
|
||||
"-DAURORA_DAWN_PROVIDER=package"
|
||||
"-DAURORA_DAWN_LINKAGE=static"
|
||||
|
||||
@@ -423,7 +423,7 @@ public:
|
||||
static DUSK_GAME_DATA daMidna_texData_s const m_texDataTable[21];
|
||||
static DUSK_GAME_DATA daMidna_anmData_s const m_anmDataTable[53];
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x568 */ request_of_phase_process_class mPhase;
|
||||
/* 0x570 */ J3DModel* mpModel;
|
||||
/* 0x574 */ J3DModel* mpShadowModel;
|
||||
|
||||
@@ -37,7 +37,7 @@ public:
|
||||
CPaneMgrAlpha* getNoDatBase() { return mNoDatBase; }
|
||||
void draw() { _draw(); }
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x04 */ JKRArchive* mArchive;
|
||||
/* 0x08 */ dDlst_FileInfo_c mFileInfo;
|
||||
/* 0x1C */ u8 field_0x1c[4];
|
||||
|
||||
@@ -13,36 +13,8 @@ class dFile_info_c;
|
||||
class J2DPicture;
|
||||
|
||||
#if TARGET_PC
|
||||
static PaneCache mSelDtPanes[] = {
|
||||
{MULTI_CHAR('tate_n0'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('tate_n1'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('ken_n0'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('ken_n1'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('fuku_n0'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('fuku_n1'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('fuku_n2'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('gray_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('b_base'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('b_base1'), 0.0f, 0.0f, false},
|
||||
};
|
||||
|
||||
static PaneCache fileSelPanes[] = {
|
||||
{MULTI_CHAR('w_uzu00'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('w_uzu01'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('w_uzu02'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('w_uzu03'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('w_uzu04'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('w_uzu05'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('w_uzu06'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('w_uzu07'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('w_uzu08'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('w_uzu09'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('w_er_msg'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('w_er_msE'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('w_er_msR'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('er_for0'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('er_for1'), 0.0f, 0.0f, false},
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
class dDlst_FileSel_c : public dDlst_base_c {
|
||||
|
||||
@@ -30,7 +30,7 @@ public:
|
||||
|
||||
void drawSelf() { mpKanteraIcon->draw(); }
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x04 */ dDlst_KanteraIcon_c* mpKanteraIcon;
|
||||
/* 0x08 */ CPaneMgr* mpParent;
|
||||
/* 0x0C */ CPaneMgr* mpGauge;
|
||||
|
||||
@@ -15,49 +15,6 @@ class dMenu_Fishing_c;
|
||||
class dMenu_Skill_c;
|
||||
class dMenu_Insect_c;
|
||||
class dSelect_cursor_c;
|
||||
|
||||
#if TARGET_PC
|
||||
struct PaneCache {
|
||||
u64 tag;
|
||||
f32 origTransX;
|
||||
f32 origTransY;
|
||||
bool cached;
|
||||
};
|
||||
|
||||
static PaneCache mpScreenPanes[] = {
|
||||
{MULTI_CHAR('sa_tex_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('op_tex_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('heart_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('wolf_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('item_0_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('item_1_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('item_2_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('fish_3_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('lett_4_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('maki_5_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('fuku_n0'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('fuku_n1'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('fuku_n2'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('tate_n0'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('tate_n1'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('ken_n0'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('ken_n1'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('kabu_6n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('t_t00'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('f_t00'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('itemn_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('infotxtn'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('sa_op_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('title_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('menu_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('w_er_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('center_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('info_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('lavel_n'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('modelbgn'), 0.0f, 0.0f, false},
|
||||
};
|
||||
#endif
|
||||
|
||||
class dMenu_Collect2D_c;
|
||||
class dMenu_Collect2DTop_c : public dDlst_base_c {
|
||||
public:
|
||||
@@ -172,7 +129,7 @@ public:
|
||||
J2DPicture* getBlackTex() { return mpBlackTex; }
|
||||
u8 getSubWindowOpenCheck() { return mSubWindowOpenCheck; }
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x004 */ JKRExpHeap* mpHeap;
|
||||
/* 0x008 */ JKRExpHeap* mpSubHeap;
|
||||
/* 0x00C */ void* field_0xc;
|
||||
|
||||
@@ -43,7 +43,7 @@ public:
|
||||
|
||||
u8 getStatus() { return mStatus; }
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x004 */ JKRExpHeap* mpHeap;
|
||||
/* 0x008 */ JKRArchive* mpArchive;
|
||||
/* 0x00C */ STControl* mpStick;
|
||||
|
||||
@@ -270,7 +270,7 @@ public:
|
||||
|
||||
static DUSK_GAME_DATA dMenu_Fmap_c* MyClass;
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x004 */ JKRExpHeap* mpHeap;
|
||||
/* 0x008 */ JKRExpHeap* mpTalkHeap;
|
||||
/* 0x00C */ STControl* mpStick;
|
||||
|
||||
@@ -60,7 +60,7 @@ public:
|
||||
|
||||
u8 getStatus() { return mStatus; }
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x04 */ JKRExpHeap* mpHeap;
|
||||
/* 0x08 */ JKRArchive* mpArchive;
|
||||
/* 0x0C */ STControl* mpStick;
|
||||
|
||||
@@ -64,7 +64,7 @@ public:
|
||||
|
||||
u8 getStatus() { return mStatus; }
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x004 */ JKRExpHeap* mpHeap;
|
||||
/* 0x008 */ JKRArchive* mpArchive;
|
||||
/* 0x00C */ STControl* mpStick;
|
||||
|
||||
@@ -117,7 +117,7 @@ public:
|
||||
return ((u8)mUseFlag & (u8)i_flag) != 0 ? true : false;
|
||||
}
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x004 */ J2DScreen* mpBackScreen;
|
||||
/* 0x008 */ J2DScreen* mpScreen;
|
||||
/* 0x00C */ J2DScreen* mpClipScreen;
|
||||
|
||||
@@ -84,7 +84,7 @@ public:
|
||||
void drawFlag0() { mDrawFlag = 0; }
|
||||
void setStatus(u8 i_status) { mStatus = i_status; }
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x004 */ JKRExpHeap* mpHeap;
|
||||
/* 0x008 */ STControl* mpStick;
|
||||
/* 0x00C */ CSTControl* mpCStick;
|
||||
|
||||
@@ -278,7 +278,7 @@ public:
|
||||
u8 getEndStatus() { return mEndStatus; }
|
||||
void setUseType(u8 type) { mUseType = type; }
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x0004 */ JKRArchive* mpArchive;
|
||||
/* 0x0008 */ mDoDvdThd_mountArchive_c* mpMount;
|
||||
/* 0x000C */ STControl* stick;
|
||||
|
||||
@@ -58,7 +58,7 @@ public:
|
||||
|
||||
u8 getStatus() { return mStatus; }
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x004 */ JKRExpHeap* mpHeap;
|
||||
/* 0x008 */ JKRArchive* mpArchive;
|
||||
/* 0x00C */ STControl* mpStick;
|
||||
|
||||
@@ -154,7 +154,7 @@ public:
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x004 */ item_params mItemParams[4];
|
||||
/* 0x074 */ JKRExpHeap* heap;
|
||||
/* 0x078 */ J2DScreen* mpScreen;
|
||||
|
||||
@@ -25,7 +25,7 @@ public:
|
||||
void setTextBoxPtr(J2DTextBox* p_textBox) { mpTextBoxPtr = p_textBox; }
|
||||
void resetType() { mType = 0x47; }
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x04 */ J2DTextBox* mpTextBoxPtr;
|
||||
/* 0x08 */ f32 mPosX;
|
||||
/* 0x0C */ f32 mPosY;
|
||||
@@ -53,7 +53,7 @@ public:
|
||||
|
||||
void setRupeeColor(u8 color) { mRupeeColor = color; }
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x004 */ COutFontSet_c* mpOfs[35];
|
||||
/* 0x090 */ J2DPicture* mpPane[70];
|
||||
/* 0x1A8 */ f32 mAlphaRatio;
|
||||
|
||||
@@ -12,34 +12,6 @@ class J2DTextBox;
|
||||
class JUTFont;
|
||||
class STControl;
|
||||
|
||||
#if TARGET_PC
|
||||
struct PaneCache {
|
||||
u64 tag;
|
||||
f32 origTransX;
|
||||
f32 origTransY;
|
||||
bool cached;
|
||||
};
|
||||
|
||||
static PaneCache l_tagName[] = {
|
||||
{MULTI_CHAR('m_00_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_00_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_00_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_00_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_00_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_01_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_01_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_01_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_01_3'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('m_01_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_02_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_02_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_02_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_02_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_02_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m03_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m03_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m03_2'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('m03_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m03_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_04_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_04_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_04_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_04_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_04_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_05_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_05_1'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('m_05_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_05_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_05_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_06_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_06_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_06_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_06_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_06_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_07_0'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('m_07_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_07_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_07_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_07_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_08_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_08_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_08_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_08_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_08_4'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('m_09_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_09_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_09_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_09_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_09_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_10_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_10_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_10_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_10_3'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('m_10_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_11_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_11_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_11_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_11_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m_11_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('m12_0'), 0.0f, 0.0f, false}, {MULTI_CHAR('m12_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('m12_2'), 0.0f, 0.0f, false},
|
||||
{MULTI_CHAR('m12_3'), 0.0f, 0.0f, false}, {MULTI_CHAR('m12_4'), 0.0f, 0.0f, false}, {MULTI_CHAR('p_end_2'), 0.0f, 0.0f, false}, {MULTI_CHAR('p_end_1'), 0.0f, 0.0f, false}, {MULTI_CHAR('p_end_0'), 0.0f, 0.0f, false},
|
||||
};
|
||||
|
||||
static PaneCache l_nameTagName[] = {
|
||||
{MULTI_CHAR('name_00'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_01'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_02'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_03'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_04'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_05'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_06'), 0.0f, 0.0f, false}, {MULTI_CHAR('name_07'), 0.0f, 0.0f, false},
|
||||
};
|
||||
|
||||
static PaneCache l_nameCurTagName[] = {
|
||||
{MULTI_CHAR('s__n_00'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_01'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_02'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_03'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_04'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_05'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_06'), 0.0f, 0.0f, false}, {MULTI_CHAR('s__n_07'), 0.0f, 0.0f, false},
|
||||
};
|
||||
#endif
|
||||
|
||||
class dNm_HIO_c {
|
||||
public:
|
||||
dNm_HIO_c();
|
||||
|
||||
@@ -68,7 +68,7 @@ public:
|
||||
JKRMemArchive* getArchive() const { return mArchive; }
|
||||
JKRHeap* getHeap() const { return mHeap; }
|
||||
|
||||
private:
|
||||
// private:
|
||||
/* 0x14 */ u8 mMountDirection;
|
||||
/* 0x18 */ s32 mEntryNumber;
|
||||
/* 0x1C */ JKRMemArchive* mArchive;
|
||||
@@ -130,8 +130,8 @@ private:
|
||||
}; // Size = 0x28
|
||||
|
||||
struct mDoDvdThdStack {
|
||||
u8 stack[4096];
|
||||
} ATTRIBUTE_ALIGN(16);
|
||||
ATTRIBUTE_ALIGN(16) u8 stack[4096];
|
||||
};
|
||||
|
||||
struct mDoDvdThd {
|
||||
static s32 main(void*);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define JASCALC_H
|
||||
|
||||
#include <types.h>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
|
||||
/**
|
||||
@@ -10,16 +11,31 @@
|
||||
*/
|
||||
struct JASCalc {
|
||||
static void imixcopy(const s16*, const s16*, s16*, u32);
|
||||
static void bcopyfast(const void* src, void* dest, u32 size);
|
||||
#if TARGET_PC
|
||||
static void bcopyfast(const void* src, void* dest, u32 size) {
|
||||
std::memcpy(dest, src, size);
|
||||
}
|
||||
#if TARGET_ANDROID
|
||||
static void _bcopy(const void* src, void* dest, u32 size);
|
||||
static void _bcopy(const void* src, void* dest, u32 size) {
|
||||
#else
|
||||
static void bcopy(const void* src, void* dest, u32 size);
|
||||
static void bcopy(const void* src, void* dest, u32 size) {
|
||||
#endif
|
||||
static void bzerofast(void* dest, u32 size);
|
||||
std::memcpy(dest, src, size);
|
||||
}
|
||||
static void bzerofast(void* dest, u32 size) {
|
||||
std::memset(dest, 0, size);
|
||||
}
|
||||
#if TARGET_ANDROID
|
||||
static void _bzero(void* dest, u32 size);
|
||||
static void _bzero(void* dest, u32 size) {
|
||||
#else
|
||||
static void bzero(void* dest, u32 size) {
|
||||
#endif
|
||||
std::memset(dest, 0, size);
|
||||
}
|
||||
#else
|
||||
static void bcopyfast(const void* src, void* dest, u32 size);
|
||||
static void bcopy(const void* src, void* dest, u32 size);
|
||||
static void bzerofast(void* dest, u32 size);
|
||||
static void bzero(void* dest, u32 size);
|
||||
#endif
|
||||
static f32 pow2(f32);
|
||||
|
||||
@@ -23,9 +23,9 @@ DUSK_GAME_DATA Vec J3DSys::mParentS;
|
||||
DUSK_GAME_DATA J3DTexCoordScaleInfo J3DSys::sTexCoordScaleTable[8];
|
||||
|
||||
#if TARGET_PC // Original game bug, array is too small.
|
||||
static u8 NullTexData[0x20] ATTRIBUTE_ALIGN(32) = {0};
|
||||
ATTRIBUTE_ALIGN(32) static u8 NullTexData[0x20] = {0};
|
||||
#else
|
||||
static u8 NullTexData[0x10] ATTRIBUTE_ALIGN(32) = {0};
|
||||
ATTRIBUTE_ALIGN(32) static u8 NullTexData[0x10] = {0};
|
||||
#endif
|
||||
|
||||
static Mtx j3dIdentityMtx = {
|
||||
|
||||
@@ -11,6 +11,7 @@ void JASCalc::imixcopy(const s16* s1, const s16* s2, s16* dst, u32 n) {
|
||||
}
|
||||
}
|
||||
|
||||
#if !TARGET_PC
|
||||
void JASCalc::bcopyfast(const void* src, void* dest, u32 size) {
|
||||
JUT_ASSERT(226, (reinterpret_cast<uintptr_t>(src) & 0x03) == 0);
|
||||
JUT_ASSERT(227, (reinterpret_cast<uintptr_t>(dest) & 0x03) == 0);
|
||||
@@ -33,11 +34,7 @@ void JASCalc::bcopyfast(const void* src, void* dest, u32 size) {
|
||||
}
|
||||
}
|
||||
|
||||
#if TARGET_ANDROID
|
||||
void JASCalc::_bcopy(const void* src, void* dest, u32 size) {
|
||||
#else
|
||||
void JASCalc::bcopy(const void* src, void* dest, u32 size) {
|
||||
#endif
|
||||
u32* usrc;
|
||||
u32* udest;
|
||||
|
||||
@@ -94,11 +91,7 @@ void JASCalc::bzerofast(void* dest, u32 size) {
|
||||
}
|
||||
}
|
||||
|
||||
#if TARGET_ANDROID
|
||||
void JASCalc::_bzero(void* dest, u32 size) {
|
||||
#else
|
||||
void JASCalc::bzero(void* dest, u32 size) {
|
||||
#endif
|
||||
u32* udest;
|
||||
u8* bdest = (u8*)dest;
|
||||
if ((size & 0x1f) == 0 && (reinterpret_cast<uintptr_t>(dest) & 0x1f) == 0) {
|
||||
@@ -139,6 +132,7 @@ void JASCalc::bzero(void* dest, u32 size) {
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#if AVOID_UB
|
||||
DUSK_GAME_DATA s16 const JASCalc::CUTOFF_TO_IIR_TABLE[129][4] = {
|
||||
|
||||
@@ -99,14 +99,14 @@ void JASDsp::invalChannelAll() {
|
||||
DCInvalidateRange(CH_BUF, sizeof(TChannel) * DSP_CHANNELS);
|
||||
}
|
||||
|
||||
DUSK_GAME_DATA u8 const ATTRIBUTE_ALIGN(32) JASDsp::DSPADPCM_FILTER[64] = {
|
||||
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA u8 const JASDsp::DSPADPCM_FILTER[64] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, 0x04, 0x00,
|
||||
0x10, 0x00, 0xF8, 0x00, 0x0E, 0x00, 0xFA, 0x00, 0x0C, 0x00, 0xFC, 0x00, 0x12, 0x00, 0xF6, 0x00,
|
||||
0x10, 0x68, 0xF7, 0x38, 0x12, 0xC0, 0xF7, 0x04, 0x14, 0x00, 0xF4, 0x00, 0x08, 0x00, 0xF8, 0x00,
|
||||
0x04, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0x04, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
DUSK_GAME_DATA u32 const ATTRIBUTE_ALIGN(32) JASDsp::DSPRES_FILTER[320] = {
|
||||
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA u32 const JASDsp::DSPRES_FILTER[320] = {
|
||||
0x0C3966AD,
|
||||
0x0D46FFDF,
|
||||
0x0B396696,
|
||||
|
||||
@@ -25,7 +25,7 @@ void DspHandShake(void*) {
|
||||
Dsp_Running_Start();
|
||||
}
|
||||
|
||||
static u8 jdsp[7936] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 jdsp[7936] = {
|
||||
0x02, 0x9F, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFF, 0x00, 0x00, 0x02, 0xFF, 0x00, 0x00,
|
||||
0x02, 0xFF, 0x00, 0x00, 0x02, 0xFF, 0x00, 0x00, 0x02, 0xFF, 0x00, 0x00, 0x02, 0x9F, 0x06, 0xA5,
|
||||
0x02, 0x9F, 0x00, 0x4E, 0x12, 0x05, 0x02, 0xBF, 0x00, 0x57, 0x81, 0x00, 0x00, 0x9F, 0x10, 0x00,
|
||||
@@ -524,9 +524,9 @@ static u8 jdsp[7936] ATTRIBUTE_ALIGN(32) = {
|
||||
0x80, 0x01, 0x02, 0xBF, 0x00, 0xF4, 0x02, 0xDF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
};
|
||||
|
||||
static DSPTaskInfo audio_task ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static DSPTaskInfo audio_task;
|
||||
|
||||
static u8 AUDIO_YIELD_BUFFER[8192] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 AUDIO_YIELD_BUFFER[8192];
|
||||
|
||||
void DspBoot(void (*requestCallback)(void*)) {
|
||||
DspInitWork();
|
||||
|
||||
@@ -452,13 +452,13 @@ static void dummy() {
|
||||
JUTXfb::getManager()->setDisplayingXfbIndex(0);
|
||||
}
|
||||
|
||||
static Mtx e_mtx ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static Mtx e_mtx = {
|
||||
{1.0f, 0.0f, 0.0f, 0.0f},
|
||||
{0.0f, 1.0f, 0.0f, 0.0f},
|
||||
{0.0f, 0.0f, 1.0f, 0.0f},
|
||||
};
|
||||
|
||||
static u8 clear_z_TX[64] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 clear_z_TX[64] = {
|
||||
0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF,
|
||||
0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
|
||||
@@ -16,10 +16,10 @@ inline f64 getConst2() {
|
||||
return 9.765625E-4;
|
||||
}
|
||||
|
||||
DUSK_GAME_DATA TSinCosTable<13, f32> sincosTable_ ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA TSinCosTable<13, f32> sincosTable_;
|
||||
|
||||
DUSK_GAME_DATA TAtanTable<1024, f32> atanTable_ ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA TAtanTable<1024, f32> atanTable_;
|
||||
|
||||
DUSK_GAME_DATA TAsinAcosTable<1024, f32> asinAcosTable_ ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA TAsinAcosTable<1024, f32> asinAcosTable_;
|
||||
|
||||
} // namespace JMath
|
||||
|
||||
@@ -46,7 +46,7 @@ JPAResource::JPAResource() {
|
||||
mUsrIdx = fldNum = keyNum = texNum = mpCalcEmitterFuncListNum = mpDrawEmitterFuncListNum = mpDrawEmitterChildFuncListNum = mpCalcParticleFuncListNum = mpDrawParticleFuncListNum = mpCalcParticleChildFuncListNum = mpDrawParticleChildFuncListNum = 0;
|
||||
}
|
||||
|
||||
static u8 jpa_pos[324] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 jpa_pos[324] = {
|
||||
0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x32, 0xCE, 0x00, 0x00, 0xCE, 0x00, 0xE7, 0x00, 0x00, 0x19,
|
||||
0x00, 0x00, 0x19, 0xCE, 0x00, 0xE7, 0xCE, 0x00, 0xCE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCE,
|
||||
0x00, 0xCE, 0xCE, 0x00, 0x00, 0x19, 0x00, 0x32, 0x19, 0x00, 0x32, 0xE7, 0x00, 0x00, 0xE7, 0x00,
|
||||
@@ -70,7 +70,7 @@ static u8 jpa_pos[324] ATTRIBUTE_ALIGN(32) = {
|
||||
0x00, 0x00, 0x00, 0xCE,
|
||||
};
|
||||
|
||||
static u8 jpa_crd[32] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u8 jpa_crd[32] = {
|
||||
0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x02, 0x01, 0x00, 0x01,
|
||||
0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x02, 0x02, 0x00, 0x02,
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#endif
|
||||
#include "global.h"
|
||||
|
||||
DUSK_GAME_DATA u8 const JUTResFONT_Ascfont_fix12[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA u8 const JUTResFONT_Ascfont_fix12[] = {
|
||||
0x46, 0x4F, 0x4E, 0x54, 0x62, 0x66, 0x6E, 0x31, 0x00, 0x00, 0x41, 0x60, 0x00, 0x00, 0x00, 0x04,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x49, 0x4E, 0x46, 0x31, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C,
|
||||
|
||||
@@ -136,7 +136,7 @@ void TRK__read_aram(__REGISTER int c, __REGISTER u32 p2, void* p3) {
|
||||
}
|
||||
|
||||
void TRK__write_aram(__REGISTER int c, __REGISTER u32 p2, void* p3) {
|
||||
u8 buff[32] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 buff[32];
|
||||
u32 err;
|
||||
__REGISTER int count = c;
|
||||
__REGISTER u32 bf;
|
||||
|
||||
@@ -85,7 +85,7 @@ DSError TRKDoSupportMask(TRKBuffer*) {
|
||||
}
|
||||
|
||||
DSError TRKDoReadMemory(TRKBuffer* buffer) {
|
||||
u8 buf[0x820] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 buf[0x820];
|
||||
size_t tempLength;
|
||||
int result;
|
||||
int replyErr;
|
||||
@@ -158,7 +158,7 @@ DSError TRKDoReadMemory(TRKBuffer* buffer) {
|
||||
}
|
||||
|
||||
DSError TRKDoWriteMemory(TRKBuffer* b) {
|
||||
u8 buf[0x820] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 buf[0x820];
|
||||
size_t tempLength;
|
||||
int options;
|
||||
int result;
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
#include "__ax.h"
|
||||
|
||||
static s32 __AXBufferAuxA[3][480] ATTRIBUTE_ALIGN(32);
|
||||
static s32 __AXBufferAuxB[3][480] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static s32 __AXBufferAuxA[3][480];
|
||||
ATTRIBUTE_ALIGN(32) static s32 __AXBufferAuxB[3][480];
|
||||
|
||||
static void (* __AXCallbackAuxA)(void*, void*);
|
||||
static void (* __AXCallbackAuxB)(void*, void*);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
#include "__ax.h"
|
||||
|
||||
static AXSPB __AXStudio ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static AXSPB __AXStudio;
|
||||
|
||||
static s32 __AXSpbAL;
|
||||
static s32 __AXSpbAR;
|
||||
|
||||
@@ -35,9 +35,9 @@ static u32 __AXAuxMixCycles[32] = {
|
||||
0x000009E2, 0x00000E97
|
||||
};
|
||||
|
||||
static AXPB __AXPB[AX_MAX_VOICES] ATTRIBUTE_ALIGN(32);
|
||||
static AXPBITDBUFFER __AXITD[AX_MAX_VOICES] ATTRIBUTE_ALIGN(32);
|
||||
static AXPBU __AXUpdates[AX_MAX_VOICES] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static AXPB __AXPB[AX_MAX_VOICES];
|
||||
ATTRIBUTE_ALIGN(32) static AXPBITDBUFFER __AXITD[AX_MAX_VOICES];
|
||||
ATTRIBUTE_ALIGN(32) static AXPBU __AXUpdates[AX_MAX_VOICES];
|
||||
static AXVPB __AXVPB[AX_MAX_VOICES];
|
||||
|
||||
static u32 __AXMaxDspCycles;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
u16 axDspSlaveLength = (AX_DSP_SLAVE_LENGTH * 2);
|
||||
|
||||
u16 axDspSlave[AX_DSP_SLAVE_LENGTH] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) u16 axDspSlave[AX_DSP_SLAVE_LENGTH] = {
|
||||
0x0000, 0x0000, 0x029F, 0x0E88, 0x029F, 0x0E97,
|
||||
0x029F, 0x0EB3, 0x029F, 0x0ED3, 0x029F, 0x0ED9,
|
||||
0x029F, 0x0F0B, 0x029F, 0x0F11, 0x1302, 0x1303,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
#include "__card.h"
|
||||
|
||||
static u8 CardData[352] ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) = {
|
||||
ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) static u8 CardData[352] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x02, 0xFF, 0x00, 0x21, 0x13, 0x06, 0x12, 0x03, 0x12, 0x04,
|
||||
0x13, 0x05, 0x00, 0x92, 0x00, 0xFF, 0x00, 0x88, 0xFF, 0xFF, 0x00, 0x89, 0xFF, 0xFF, 0x00, 0x8A, 0xFF, 0xFF, 0x00,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <dolphin/demo.h>
|
||||
#include "sdk_math.h"
|
||||
|
||||
static s16 __AVX_internal_buffer[3200] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static s16 __AVX_internal_buffer[3200];
|
||||
|
||||
static void (*__AVX_save_isr)(void);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include <dolphin/dolphin.h>
|
||||
#include <dolphin/demo.h>
|
||||
|
||||
u32 DEMOFontBitmap[768] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) u32 DEMOFontBitmap[768] = {
|
||||
0x00000000,
|
||||
0x00000000,
|
||||
0x00000000,
|
||||
|
||||
@@ -43,7 +43,7 @@ void* __piReg;
|
||||
GXBool __GXinBegin;
|
||||
#endif
|
||||
|
||||
static u16 DefaultTexData[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u16 DefaultTexData[] = {
|
||||
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
|
||||
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
|
||||
};
|
||||
|
||||
@@ -7,11 +7,11 @@ const char* __MCCVersion = "<< Dolphin SDK - MCC\tdebug build: Apr 5 2004 03:57
|
||||
const char* __MCCVersion = "<< Dolphin SDK - MCC\trelease build: Apr 5 2004 04:15:49 (0x2301) >>";
|
||||
#endif
|
||||
|
||||
static MCC_ChannelInfo gChannelInfo[16] ATTRIBUTE_ALIGN(32);
|
||||
static char gStreamWork[32] ATTRIBUTE_ALIGN(32);
|
||||
static char m_szAdapterMode[32] ATTRIBUTE_ALIGN(32);
|
||||
static char m_szInitCode[32] ATTRIBUTE_ALIGN(32);
|
||||
static MCC_Info channelInfo[16] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static MCC_ChannelInfo gChannelInfo[16];
|
||||
ATTRIBUTE_ALIGN(32) static char gStreamWork[32];
|
||||
ATTRIBUTE_ALIGN(32) static char m_szAdapterMode[32];
|
||||
ATTRIBUTE_ALIGN(32) static char m_szInitCode[32];
|
||||
ATTRIBUTE_ALIGN(32) static MCC_Info channelInfo[16];
|
||||
|
||||
volatile static BOOL gIsChannelinfoDirty = TRUE;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
#include "__os.h"
|
||||
|
||||
static SramControl Scb ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT);
|
||||
ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) static SramControl Scb;
|
||||
|
||||
// prototypes
|
||||
static int GetRTC(u32* rtc);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
#include "__card.h"
|
||||
|
||||
static u8 CardData[352] ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) = {
|
||||
ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) static u8 CardData[352] = {
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x02, 0xFF, 0x00, 0x21, 0x13, 0x06, 0x12, 0x03, 0x12, 0x04,
|
||||
0x13, 0x05, 0x00, 0x92, 0x00, 0xFF, 0x00, 0x88, 0xFF, 0xFF, 0x00, 0x89, 0xFF, 0xFF, 0x00, 0x8A, 0xFF, 0xFF, 0x00,
|
||||
|
||||
@@ -44,7 +44,7 @@ static u32 LastError;
|
||||
static BOOL ResetRequired;
|
||||
static u32 MotorState;
|
||||
static volatile OSTime LastResetEnd;
|
||||
static u32 __DVDNumTmdBytes ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u32 __DVDNumTmdBytes;
|
||||
static DVDGameTOC* GameToc;
|
||||
static DVDPartitionInfo* PartInfo;
|
||||
static DVDPartitionInfo* BootGameInfo;
|
||||
@@ -80,10 +80,10 @@ static DVDCommandBlock DummyCommandBlock;
|
||||
static OSAlarm ResetAlarm;
|
||||
static OSAlarm CoverAlarm;
|
||||
|
||||
static u8 __DVDGameTocBuffer[OSRoundUp32B(sizeof(DVDGameTOC) * 4)] ATTRIBUTE_ALIGN(32);
|
||||
static u8 __DVDPartInfoBuffer[OSRoundUp32B(sizeof(DVDPartitionInfo) * 4)] ATTRIBUTE_ALIGN(32);
|
||||
static u8 __DVDTmdBuffer[OSRoundUp32B(sizeof(ESTitleMeta))] ATTRIBUTE_ALIGN(32);
|
||||
static u8 __DVDTicketViewBuffer[OSRoundUp32B(sizeof(ESTicketView))] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 __DVDGameTocBuffer[OSRoundUp32B(sizeof(DVDGameTOC) * 4)];
|
||||
ATTRIBUTE_ALIGN(32) static u8 __DVDPartInfoBuffer[OSRoundUp32B(sizeof(DVDPartitionInfo) * 4)];
|
||||
ATTRIBUTE_ALIGN(32) static u8 __DVDTmdBuffer[OSRoundUp32B(sizeof(ESTitleMeta))];
|
||||
ATTRIBUTE_ALIGN(32) static u8 __DVDTicketViewBuffer[OSRoundUp32B(sizeof(ESTicketView))];
|
||||
|
||||
static OSAlarm FatalAlarm;
|
||||
DVDCommandBlock __DVDStopMotorCommandBlock;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "__os.h"
|
||||
#include "__dvd.h"
|
||||
|
||||
static u8 CheckBuffer[32] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 CheckBuffer[32];
|
||||
|
||||
static volatile BOOL lowDone = TRUE;
|
||||
static volatile u32 lowIntType = 0;
|
||||
|
||||
@@ -8,15 +8,15 @@ static volatile u8 requestInProgress = FALSE;
|
||||
static u8 breakRequested;
|
||||
static u8 callbackInProgress;
|
||||
|
||||
static u32 registerBuf[8] ATTRIBUTE_ALIGN(32);
|
||||
static u32 statusRegister[8] ATTRIBUTE_ALIGN(32);
|
||||
static u32 controlRegister[8] ATTRIBUTE_ALIGN(32);
|
||||
static s32 lastTicketError[8] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u32 registerBuf[8];
|
||||
ATTRIBUTE_ALIGN(32) static u32 statusRegister[8];
|
||||
ATTRIBUTE_ALIGN(32) static u32 controlRegister[8];
|
||||
ATTRIBUTE_ALIGN(32) static s32 lastTicketError[8];
|
||||
|
||||
static u32 readLength;
|
||||
static u32 spinUpValue;
|
||||
|
||||
static diRegVals_t diRegValCache ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static diRegVals_t diRegValCache;
|
||||
|
||||
static u8 DVDLowInitCalled = FALSE;
|
||||
|
||||
@@ -42,8 +42,8 @@ typedef struct dvdContext {
|
||||
|
||||
static int freeDvdContext = 0;
|
||||
static u8 dvdContextsInited = FALSE;
|
||||
static dvdContext_t dvdContexts[4] ATTRIBUTE_ALIGN(32);
|
||||
static IOSIoVector ioVec[10] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static dvdContext_t dvdContexts[4];
|
||||
ATTRIBUTE_ALIGN(32) static IOSIoVector ioVec[10];
|
||||
|
||||
static void* ddrAllocAligned32(const int size) {
|
||||
void* low, *high;
|
||||
|
||||
@@ -11,8 +11,8 @@ static NANDCommandBlock NandCb;
|
||||
static NANDFileInfo NandInfo;
|
||||
static DVDCBCallback Callback;
|
||||
static u32 NextOffset;
|
||||
DVDErrorInfo __ErrorInfo ATTRIBUTE_ALIGN(32);
|
||||
DVDErrorInfo __FirstErrorInfo ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) DVDErrorInfo __ErrorInfo;
|
||||
ATTRIBUTE_ALIGN(32) DVDErrorInfo __FirstErrorInfo;
|
||||
|
||||
void cbForNandClose(s32 result, NANDCommandBlock* block) {
|
||||
if (Callback) {
|
||||
|
||||
@@ -33,7 +33,7 @@ s32 ESP_CloseLib(void) {
|
||||
|
||||
s32 ESP_LaunchTitle(u64 titleID, ESTicketView* pTicketView) {
|
||||
s32 ret = 0;
|
||||
u8 buf[256] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 buf[256];
|
||||
IOSIoVector* vec = (IOSIoVector*)(buf + 208);
|
||||
u64* id = (u64*)buf;
|
||||
|
||||
@@ -63,7 +63,7 @@ out:
|
||||
|
||||
s32 ESP_GetTicketViews(ESTitleId titleId, ESTicketView* ticketViewList, u32* ticketViewCnt) {
|
||||
s32 rv = 0;
|
||||
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
|
||||
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
|
||||
ESTitleId* p1 = (ESTitleId*)__esBuf;
|
||||
u32* p2 = (u32*)(__esBuf + 32);
|
||||
@@ -111,7 +111,7 @@ out:
|
||||
|
||||
s32 ESP_DiGetTicketView(const void* ticket, ESTicketView* ticketView) {
|
||||
s32 rv = 0;
|
||||
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
|
||||
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
|
||||
|
||||
if (__esFd < 0 || ticketView == NULL) {
|
||||
@@ -142,7 +142,7 @@ out:
|
||||
|
||||
s32 ESP_DiGetTmd(ESTitleMeta* tmd, u32* tmdSize) {
|
||||
s32 rv = 0;
|
||||
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
|
||||
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
|
||||
u32* p1 = (u32*)__esBuf;
|
||||
|
||||
@@ -184,7 +184,7 @@ out:
|
||||
|
||||
s32 ESP_GetTmdView(ESTitleId titleId, ESTmdView* tmdView, u32* size) {
|
||||
s32 rv = 0;
|
||||
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
|
||||
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
|
||||
ESTitleId* p1 = (ESTitleId*)__esBuf;
|
||||
u32* p2 = (u32*)(__esBuf + 32);
|
||||
@@ -233,7 +233,7 @@ out:
|
||||
|
||||
s32 ESP_GetDataDir(ESTitleId titleId, char* dataDir) {
|
||||
s32 rv = 0;
|
||||
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
|
||||
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
|
||||
ESTitleId* p1 = (ESTitleId*)__esBuf;
|
||||
|
||||
@@ -260,7 +260,7 @@ out:
|
||||
|
||||
s32 ESP_GetTitleId(ESTitleId* titleId) {
|
||||
s32 rv = 0;
|
||||
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
|
||||
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
|
||||
|
||||
if (__esFd < 0 || titleId == NULL) {
|
||||
@@ -283,7 +283,7 @@ out:
|
||||
|
||||
s32 ESP_GetConsumption(ESTicketId ticketId, ESLpEntry* entries, u32* nEntries) {
|
||||
s32 rv = 0;
|
||||
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
|
||||
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
|
||||
ESTicketId* p1 = (ESTicketId*)__esBuf;
|
||||
u32* p2 = (u32*)(__esBuf + 32);
|
||||
|
||||
@@ -25,7 +25,7 @@ typedef struct isfs_GetUsage {
|
||||
} isfs_GetUsage;
|
||||
|
||||
typedef struct __isfsCtxt {
|
||||
u8 ioBuf[ROUNDUP(256)] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 ioBuf[ROUNDUP(256)];
|
||||
ISFSCallback cb;
|
||||
void* ctxt;
|
||||
u32 func;
|
||||
|
||||
@@ -49,7 +49,7 @@ volatile void* __piReg;
|
||||
GXBool __GXinBegin;
|
||||
#endif
|
||||
|
||||
static u16 DefaultTexData[] ATTRIBUTE_ALIGN(32) = {
|
||||
ATTRIBUTE_ALIGN(32) static u16 DefaultTexData[] = {
|
||||
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
|
||||
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace nw4hbm {
|
||||
|
||||
private:
|
||||
/* 0x00 (base) */
|
||||
/* 0xD4 */ ut::Color mVtxColors[VERTEXCOLOR_MAX] ATTRIBUTE_ALIGN(4);
|
||||
/* 0xD4 */ ATTRIBUTE_ALIGN(4) ut::Color mVtxColors[VERTEXCOLOR_MAX];
|
||||
/* 0xE4 */ detail::TexCoordAry mTexCoordAry;
|
||||
};
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace nw4hbm {
|
||||
public:
|
||||
/* 0x14 */ ut::LinkListNode mLinkNode;
|
||||
|
||||
static u8 mMramBuf[LOAD_BUFFER_SIZE] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 mMramBuf[LOAD_BUFFER_SIZE];
|
||||
};
|
||||
typedef ut::LinkList<LoadCommand, offsetof(LoadCommand, mLinkNode)> LoadCommandList;
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ static u32 __relnchFl = 0;
|
||||
|
||||
typedef struct IOSRpcRequest {
|
||||
IOSResourceRequest request;
|
||||
IOSIpcCb cb ATTRIBUTE_ALIGN(32); // I am assuming this is aligned due to where cbArg is stored, and I see nothing between cb and callback_arg?
|
||||
ATTRIBUTE_ALIGN(32) IOSIpcCb cb; // I am assuming this is aligned due to where cbArg is stored, and I see nothing between cb and callback_arg?
|
||||
void* callback_arg;
|
||||
u32 relaunch_flag;
|
||||
OSThreadQueue thread_queue;
|
||||
@@ -37,7 +37,7 @@ static IOSRpcRequest* __relnchRpcSave = 0;
|
||||
|
||||
#define ROUNDUP(sz) (((u32)(sz) + (IPC_BUF_CNT / 2 - 1)) & ~(u32)(IPC_BUF_CNT / 2 - 1))
|
||||
|
||||
static u8 __rpcBuf[ROUNDUP(sizeof(IOSRpcRequest))] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 __rpcBuf[ROUNDUP(sizeof(IOSRpcRequest))];
|
||||
|
||||
static struct {
|
||||
u32 rcount;
|
||||
|
||||
@@ -33,8 +33,8 @@ enum LibState {
|
||||
};
|
||||
|
||||
static enum LibState s_libState = STATE_NOT_INITIALIZED;
|
||||
static char s_currentDir[64] ATTRIBUTE_ALIGN(32) = "/";
|
||||
static char s_homeDir[64] ATTRIBUTE_ALIGN(32) = "";
|
||||
ATTRIBUTE_ALIGN(32) static char s_currentDir[64] = "/";
|
||||
ATTRIBUTE_ALIGN(32) static char s_homeDir[64] = "";
|
||||
|
||||
static BOOL nandOnShutdown(BOOL final, u32 event);
|
||||
void nandConvertPath(char* abspath, const char* wd, const char* relpath);
|
||||
@@ -262,7 +262,7 @@ s32 nandConvertErrorCode(const ISFSError err) {
|
||||
for (; i < sizeof(ERRMAP) / 4; i = i + 2) {
|
||||
if (ERRMAP[i] == err) {
|
||||
if (err == ISFS_ERROR_ECC_CRIT || err == ISFS_ERROR_HMAC || err == ISFS_ERROR_UNKNOWN || err == IOS_ERROR_UNKNOWN || err == IOS_ERROR_ECC_CRIT) {
|
||||
char buf[128] ATTRIBUTE_ALIGN(64);
|
||||
ATTRIBUTE_ALIGN(64) char buf[128];
|
||||
sprintf(buf, "ISFS error code: %d", err);
|
||||
NANDLoggingAddMessageAsync(nandLoggingCallback, err, buf);
|
||||
}
|
||||
@@ -279,7 +279,7 @@ s32 nandConvertErrorCode(const ISFSError err) {
|
||||
|
||||
OSReport("CAUTION! Unexpected error code [%d] was found.\n", err);
|
||||
{
|
||||
char buf[128] ATTRIBUTE_ALIGN(64);
|
||||
ATTRIBUTE_ALIGN(64) char buf[128];
|
||||
sprintf(buf, "ISFS unexpected error code: %d", err);
|
||||
NANDLoggingAddMessageAsync(nandLoggingCallback, err, buf);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
static IOSFd s_fd = -255;
|
||||
static IOSError s_err = ISFS_ERROR_UNKNOWN;
|
||||
static int s_stage;
|
||||
static char s_message[256] ATTRIBUTE_ALIGN(64);
|
||||
ATTRIBUTE_ALIGN(64) static char s_message[256];
|
||||
static NANDLoggingCallback s_callback = 0;
|
||||
|
||||
static void asyncRoutine(ISFSError, void*);
|
||||
@@ -70,8 +70,8 @@ static void callbackRoutine(BOOL result) {
|
||||
|
||||
static void asyncRoutine(ISFSError result, void *ctxt) {
|
||||
ISFSError ret = ISFS_ERROR_UNKNOWN;
|
||||
static char s_rBuf[256] ATTRIBUTE_ALIGN(64);
|
||||
static char s_wBuf[256] ATTRIBUTE_ALIGN(64);
|
||||
ATTRIBUTE_ALIGN(64) static char s_rBuf[256];
|
||||
ATTRIBUTE_ALIGN(64) static char s_wBuf[256];
|
||||
++s_stage;
|
||||
|
||||
if (s_stage == 2) {
|
||||
|
||||
@@ -293,7 +293,7 @@ s32 NANDMove(const char* path, const char* destDir) {
|
||||
}
|
||||
|
||||
static ISFSError nandGetFileStatus(IOSFd fd, u32* length, u32* pos) {
|
||||
ISFSFileStats fstat ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) ISFSFileStats fstat;
|
||||
ISFSError result = ISFS_GetFileStats(fd, &fstat);
|
||||
if (result == ISFS_ERROR_OK) {
|
||||
if (length) {
|
||||
|
||||
@@ -8,7 +8,7 @@ void __OSRelaunchTitle(u32 resetCode) {
|
||||
s32 rc = 0;
|
||||
u32 ticketCnt = 1;
|
||||
ESTicketView* tik = NULL;
|
||||
ESTitleId titleId ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) ESTitleId titleId;
|
||||
__OSPlayTimeType type = OSPLAYTIME_PERMANENT;
|
||||
u32 remain = 0;
|
||||
u8* bi2 = NULL;
|
||||
|
||||
@@ -85,7 +85,7 @@ NWC24Err NWC24iSynchronizeRtcCounter(BOOL param_0) {
|
||||
}
|
||||
|
||||
NWC24Err NWC24SuspendScheduler(void) {
|
||||
static u8 susResult[0x20] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 susResult[0x20];
|
||||
NWC24Err rt = NWC24_OK;
|
||||
NWC24Err closeRt = NWC24_OK;
|
||||
IOSFd fd;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <revolution/nand.h>
|
||||
|
||||
static BOOL PlayRecordGet = FALSE;
|
||||
static OSPlayRecord PlayRecord ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static OSPlayRecord PlayRecord;
|
||||
static NANDFileInfo FileInfo;
|
||||
static NANDCommandBlock Block;
|
||||
static s32 PlayRecordState = 9;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
#include "__os.h"
|
||||
|
||||
OSAlarm __OSExpireAlarm ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) OSAlarm __OSExpireAlarm;
|
||||
OSTime __OSExpireTime;
|
||||
OSPlayTimeCallbackFunc __OSExpireCallback;
|
||||
BOOL __OSExpireSetExpiredFlag;
|
||||
@@ -103,7 +103,7 @@ BOOL __OSWriteExpiredFlag(void) {
|
||||
s32 rv = 0;
|
||||
NANDFileInfo nInfo;
|
||||
BOOL openNInfo = FALSE;
|
||||
u8 titleId[32] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 titleId[32];
|
||||
|
||||
rv = NANDPrivateCreate("/shared2/expired", 63, 0);
|
||||
|
||||
@@ -162,7 +162,7 @@ BOOL __OSWriteExpiredFlagIfSet(void) {
|
||||
void* __OSPlayTimeRebootThread(void* args) {
|
||||
BOOL enabled;
|
||||
u32 frames, fadeShift = 1;
|
||||
__OSExpireAIFadeStruct aiFade ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) __OSExpireAIFadeStruct aiFade;
|
||||
|
||||
__OSExpireAIFade = &aiFade;
|
||||
memset(__OSExpireAIFade, 0, sizeof(__OSExpireAIFadeStruct));
|
||||
@@ -227,9 +227,9 @@ out:
|
||||
s32 __OSGetPlayTime(ESTicketView* ticket, __OSPlayTimeType* type, u32* playTime) {
|
||||
s32 rv;
|
||||
u32 i;
|
||||
ESLpEntry lpEntry[8] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) ESLpEntry lpEntry[8];
|
||||
u32 numCc = 0, seenOther = 0;
|
||||
ESTicketView ticketAligned ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) ESTicketView ticketAligned;
|
||||
|
||||
ASSERTLINE(601, ticket && type && playTime);
|
||||
|
||||
@@ -288,7 +288,7 @@ out:
|
||||
|
||||
s32 __OSGetPlayTimeCurrent(__OSPlayTimeType* type, u32* playTime) {
|
||||
s32 rv;
|
||||
ESTicketView ticket ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) ESTicketView ticket;
|
||||
|
||||
ASSERTLINE(676, type && playTime);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
#include "__os.h"
|
||||
|
||||
static SramControl Scb ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT);
|
||||
ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) static SramControl Scb;
|
||||
|
||||
// prototypes
|
||||
static int GetRTC(u32* rtc);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include <revolution/nand.h>
|
||||
#include <cstring>
|
||||
|
||||
static OSStateFlags StateFlags ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static OSStateFlags StateFlags;
|
||||
|
||||
static u32 CheckSum(OSStateFlags* flags) {
|
||||
u32* ptr, i, sum;
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
|
||||
#include <revolution/private/iosrestypes.h>
|
||||
|
||||
static u32 StmImInBuf[8] ATTRIBUTE_ALIGN(32);
|
||||
static u32 StmImOutBuf[8] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u32 StmImInBuf[8];
|
||||
ATTRIBUTE_ALIGN(32) static u32 StmImOutBuf[8];
|
||||
|
||||
static u32 StmVdInBuf[8] ATTRIBUTE_ALIGN(32);
|
||||
static u32 StmVdOutBuf[8] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u32 StmVdInBuf[8];
|
||||
ATTRIBUTE_ALIGN(32) static u32 StmVdOutBuf[8];
|
||||
|
||||
static u32 StmEhInBuf[8] ATTRIBUTE_ALIGN(32);
|
||||
static u32 StmEhOutBuf[8] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u32 StmEhInBuf[8];
|
||||
ATTRIBUTE_ALIGN(32) static u32 StmEhOutBuf[8];
|
||||
|
||||
static OSResetCallback ResetCallback;
|
||||
static OSPowerCallback PowerCallback;
|
||||
|
||||
@@ -873,4 +873,4 @@ void* OSGetThreadSpecific(s32 index) {
|
||||
|
||||
#include "global.h"
|
||||
extern u8 Debug_BBA_804516D0;
|
||||
u8 Debug_BBA_804516D0 ATTRIBUTE_ALIGN(8);
|
||||
ATTRIBUTE_ALIGN(8) u8 Debug_BBA_804516D0;
|
||||
|
||||
@@ -75,8 +75,8 @@ static const char ConfDirName[] = "/shared2/sys";
|
||||
static const char ConfFileName[] = "/shared2/sys/SYSCONF";
|
||||
static const char ProductInfoFileName[] = "/title/00000001/00000002/data/setting.txt";
|
||||
|
||||
static u8 ConfBuf[0x4000] ATTRIBUTE_ALIGN(32);
|
||||
static u8 ConfBufForFlush[0x4000] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) static u8 ConfBuf[0x4000];
|
||||
ATTRIBUTE_ALIGN(32) static u8 ConfBufForFlush[0x4000];
|
||||
|
||||
static u8 Initialized;
|
||||
static u8 DirtyFlag;
|
||||
|
||||
@@ -262,7 +262,7 @@ void WPADiManageHandler(OSAlarm*, OSContext*) {
|
||||
BTA_HhGetAclQueueInfo();
|
||||
}
|
||||
|
||||
u8 __WPADiManageHandlerStack[4096] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __WPADiManageHandlerStack[4096];
|
||||
|
||||
void WPADiManageHandler0(OSAlarm* alarm, OSContext* context) {
|
||||
OSSwitchFiberEx((u32)alarm, (u32)context, 0, 0, (u32)WPADiManageHandler, (u32)(__WPADiManageHandlerStack + sizeof(__WPADiManageHandlerStack)));
|
||||
|
||||
@@ -41,7 +41,7 @@ WUDControlBlock _wcb;
|
||||
WUDDevInfo _work;
|
||||
static WUDDiscResp _discResp;
|
||||
SCBtDeviceInfoArray _scArray;
|
||||
u8 __WUDHandlerStack[0x1000] ATTRIBUTE_ALIGN(32);
|
||||
ATTRIBUTE_ALIGN(32) u8 __WUDHandlerStack[0x1000];
|
||||
|
||||
extern u8 _scFlush;
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
cmake_minimum_required(VERSION 3.25)
|
||||
project(basic_cosmetics_mod 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 ()
|
||||
|
||||
set(COSMETIC_SOURCES src/color_utils.cpp src/midna_hair_color.cpp src/texture_utils.cpp src/hooks.cpp)
|
||||
|
||||
add_mod(basic_cosmetics_mod
|
||||
FEATURES game fmt
|
||||
SOURCES src/mod.cpp ${COSMETIC_SOURCES}
|
||||
MOD_JSON mod.json
|
||||
RES_DIR res
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"id": "dev.twilitrealm.basic_cosmetics",
|
||||
"name": "Basic Cosmetics",
|
||||
"version": "1.0.0",
|
||||
"author": "Twilit Realm",
|
||||
"description": "Change Basic Cosmetic Colors"
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
#include "color_utils.hpp"
|
||||
|
||||
uint8_t desaturate_rgb_565(uint16_t rgb565Val)
|
||||
{
|
||||
const uint32_t r = (rgb565Val & 0xf800) >> 11;
|
||||
const uint32_t g = (rgb565Val & 0x7e0) >> 5;
|
||||
const uint32_t b = rgb565Val & 0x1f;
|
||||
|
||||
// Here we are doing a quicker (0.22 * r + 0.72 * g + 0.06 * b) which
|
||||
// uses multiplies and shifts rather than division.
|
||||
const uint32_t combined = 30480413 * r + 49085341 * g + 8312839 * b;
|
||||
uint8_t shifted = (combined >> 24) & 0xff;
|
||||
|
||||
// Check if should round up shifted value.
|
||||
if (shifted < 0xff && combined & 0x00800000)
|
||||
{
|
||||
shifted += 1;
|
||||
}
|
||||
|
||||
return shifted;
|
||||
}
|
||||
|
||||
uint16_t blend_overlay_rgb_565(uint8_t grayVal, GXColor color)
|
||||
{
|
||||
uint32_t rTimes255, gTimes255, bTimes255;
|
||||
|
||||
if (grayVal <= 0x7f)
|
||||
{
|
||||
const uint32_t grayTimesTwo = 2 * grayVal;
|
||||
|
||||
rTimes255 = grayTimesTwo * color.r;
|
||||
gTimes255 = grayTimesTwo * color.g;
|
||||
bTimes255 = grayTimesTwo * color.b;
|
||||
}
|
||||
else
|
||||
{
|
||||
const uint32_t multiplier = 2 * (255 - grayVal);
|
||||
|
||||
rTimes255 = 255 * 255 - multiplier * (255 - color.r);
|
||||
gTimes255 = 255 * 255 - multiplier * (255 - color.g);
|
||||
bTimes255 = 255 * 255 - multiplier * (255 - color.b);
|
||||
}
|
||||
|
||||
// Divide each by 255
|
||||
const uint32_t r = (rTimes255 + 1 + (rTimes255 >> 8)) >> 8;
|
||||
const uint32_t g = (gTimes255 + 1 + (gTimes255 >> 8)) >> 8;
|
||||
const uint32_t b = (bTimes255 + 1 + (bTimes255 >> 8)) >> 8;
|
||||
|
||||
return ((r & 0xf8) << 8) | ((g & 0xfc) << 3) | ((b & 0xf8) >> 3);
|
||||
}
|
||||
|
||||
// Helper function to perform overlay blending on a single 8-bit color channel
|
||||
uint8_t blend_overlay_channel(uint8_t base, uint8_t blend)
|
||||
{
|
||||
if (base < 128) {
|
||||
return static_cast<uint8_t>((2 * base * blend) / 255);
|
||||
}
|
||||
|
||||
return static_cast<uint8_t>(255 - (2 * (255 - base) * (255 - blend)) / 255);
|
||||
}
|
||||
|
||||
bool is_valid_hex_color_str(std::string_view hexStr) {
|
||||
return hexStr.find_first_not_of("0123456789ABCDEFabcdef") == std::string_view::npos && hexStr.length() == 6;
|
||||
}
|
||||
|
||||
GXColor hex_color_str_to_gx_color(const std::string& hexColorStr) {
|
||||
u8 r = std::stoi(hexColorStr.substr(0, 2), nullptr, 16);
|
||||
u8 g = std::stoi(hexColorStr.substr(2, 2), nullptr, 16);
|
||||
u8 b = std::stoi(hexColorStr.substr(4, 2), nullptr, 16);
|
||||
return GXColor{r, g, b};
|
||||
}
|
||||
|
||||
static f32 rainbowPhaseAngle = 0.f;
|
||||
|
||||
GXColor get_rainbow_rgb(f32 amplitude) {
|
||||
f32 phase_rad = rainbowPhaseAngle * M_PI / 180.0f;
|
||||
|
||||
u8 r_val = (u8)(amplitude * (sinf(phase_rad) + 1.0f) + 0.5f);
|
||||
u8 g_val = (u8)(amplitude * (sinf(phase_rad + 2.0f * M_PI / 3.0f) + 1.0f) + 0.5f);
|
||||
u8 b_val = (u8)(amplitude * (sinf(phase_rad + 4.0f * M_PI / 3.0f) + 1.0f));
|
||||
GXColor rgbColor;
|
||||
rgbColor.r = r_val;
|
||||
rgbColor.g = g_val;
|
||||
rgbColor.b = b_val;
|
||||
rgbColor.a = 0xff;
|
||||
return rgbColor;
|
||||
}
|
||||
|
||||
void update_rainbow_rgb(f32 increment) {
|
||||
rainbowPhaseAngle += increment;
|
||||
if (rainbowPhaseAngle >= 360.0f) {
|
||||
rainbowPhaseAngle -= 360.0f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* File originally copied from console TPR with permission from isaac
|
||||
* https://github.com/zsrtp/libtp_rel/blob/master/include/util/color_utils.h
|
||||
*/
|
||||
|
||||
#include <gx.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
// Desaturates an RGB565 color to a u8 gray value (0xFF being white and 0x00
|
||||
// being black).
|
||||
uint8_t desaturate_rgb_565(uint16_t rgb565Val);
|
||||
|
||||
// Performs an "Overlay" blend of a u8 gray value and a pointer to a u8
|
||||
// array of {r,g,b}. Returns the result as an RGB565.
|
||||
uint16_t blend_overlay_rgb_565(uint8_t grayVal, GXColor color);
|
||||
|
||||
// Perform overlay blending on a single 8-bit color channel
|
||||
uint8_t blend_overlay_channel(uint8_t base, uint8_t blend);
|
||||
|
||||
bool is_valid_hex_color_str(std::string_view hexStr);
|
||||
|
||||
GXColor hex_color_str_to_gx_color(const std::string& hexColorStr);
|
||||
|
||||
GXColor get_rainbow_rgb(f32 amplitude);
|
||||
|
||||
void update_rainbow_rgb(f32 increment);
|
||||
@@ -0,0 +1,694 @@
|
||||
#include "hooks.hpp"
|
||||
|
||||
#include "mod.hpp"
|
||||
#include "color_utils.hpp"
|
||||
#include "midna_hair_color.hpp"
|
||||
#include "texture_utils.hpp"
|
||||
#include "types.h"
|
||||
|
||||
#include "mods/svc/hook.hpp"
|
||||
#include "mods/svc/log.hpp"
|
||||
|
||||
#include "d/actor/d_a_alink.h"
|
||||
#include "d/actor/d_a_midna.h"
|
||||
#include "d/d_a_item_static.h"
|
||||
#include "d/d_bright_check.h"
|
||||
#include "d/d_file_sel_info.h"
|
||||
#include "d/d_file_select.h"
|
||||
#include "d/d_kankyo.h"
|
||||
#include "d/d_kantera_icon_meter.h"
|
||||
#include "d/d_menu_collect.h"
|
||||
#include "d/d_menu_fishing.h"
|
||||
#include "d/d_menu_fmap2D.h"
|
||||
#include "d/d_menu_insect.h"
|
||||
#include "d/d_menu_letter.h"
|
||||
#include "d/d_menu_option.h"
|
||||
#include "d/d_menu_ring.h"
|
||||
#include "d/d_menu_save.h"
|
||||
#include "d/d_menu_skill.h"
|
||||
#include "d/d_meter_button.h"
|
||||
#include "d/d_meter_hakusha.h"
|
||||
#include "d/d_meter2.h"
|
||||
#include "d/d_meter2_draw.h"
|
||||
#include "d/d_meter2_info.h"
|
||||
#include "d/d_msg_object.h"
|
||||
#include "d/d_msg_out_font.h"
|
||||
#include "d/d_msg_scrn_base.h"
|
||||
#include "d/d_pane_class.h"
|
||||
#include "m_Do/m_Do_dvd_thread.h"
|
||||
#include "SSystem/SComponent/c_phase.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
// Main hook for recoloring textures on load
|
||||
DEFINE_HOOK_SYMBOL(
|
||||
"mDoDvdThd_mountArchive_c::execute", s32(mDoDvdThd_mountArchive_c*), MountArchiveExecute);
|
||||
void mount_archive_execute_post(ModContext*, void* args, void* retval, void* userdata) {
|
||||
auto archive = mods::arg<mDoDvdThd_mountArchive_c*>(args, 0);
|
||||
handle_texture_overrides_on_load(archive);
|
||||
}
|
||||
|
||||
// Helper for getting configVar color
|
||||
static std::optional<GXColor> get_config_var_color(ConfigVarHandle handle, bool allowRainbow = false) {
|
||||
auto colorStr = get_str_option(handle, "");
|
||||
|
||||
// Convert to lowercase
|
||||
for (auto& c : colorStr) {
|
||||
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
|
||||
if (is_valid_hex_color_str(colorStr) || (colorStr == "rainbow" && allowRainbow)) {
|
||||
if (is_valid_hex_color_str(colorStr)) {
|
||||
return hex_color_str_to_gx_color(colorStr);
|
||||
}
|
||||
|
||||
if (allowRainbow) {
|
||||
// Assume rainbow if not a valid hex str
|
||||
auto color = get_rainbow_rgb(127.5f);
|
||||
color.r /= 2;
|
||||
color.g /= 2;
|
||||
color.b /= 2;
|
||||
return color;
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Lantern ambience color
|
||||
DEFINE_HOOK(&dKy_WolfEyeLight_set, WolfEyeLightSet);
|
||||
void wolf_eye_light_set_post(ModContext*, void* args, void* retval, void* userdata) {
|
||||
auto maybeLanternColor = get_config_var_color(get_cvars().lanternGlowColor, true);
|
||||
if (maybeLanternColor.has_value()) {
|
||||
auto lanternColor = maybeLanternColor.value();
|
||||
|
||||
dScnKy_env_light_c* kankyo = dKy_getEnvlight();
|
||||
kankyo->field_0x0c18[0].mColor.r = lanternColor.r;
|
||||
kankyo->field_0x0c18[0].mColor.g = lanternColor.g;
|
||||
kankyo->field_0x0c18[0].mColor.b = lanternColor.b;
|
||||
}
|
||||
}
|
||||
|
||||
// Lantern Sphere color
|
||||
DEFINE_HOOK(&daAlink_c::preKandelaarDraw, PreKandelaarDraw);
|
||||
void pre_kandelaar_draw_post(ModContext*, void* args, void* retval, void* userdata) {
|
||||
auto maybeLanternColor = get_config_var_color(get_cvars().lanternGlowColor, true);
|
||||
if (maybeLanternColor.has_value()) {
|
||||
auto lanternColor = maybeLanternColor.value();
|
||||
|
||||
J3DMaterial* mat_p = daAlink_getAlinkActorClass()->mpKanteraGlowModel->getModelData()->getMaterialNodePointer(0);
|
||||
|
||||
J3DGXColorS10 color;
|
||||
color.r = lanternColor.r;
|
||||
color.g = lanternColor.g;
|
||||
color.b = lanternColor.b;
|
||||
color.a = 255;
|
||||
mat_p->setTevColor(1, &color);
|
||||
|
||||
color.r = lanternColor.r;
|
||||
color.g = lanternColor.g;
|
||||
color.b = lanternColor.b;
|
||||
mat_p->setTevColor(2, &color);
|
||||
}
|
||||
}
|
||||
|
||||
// Main Lantern Meter Color
|
||||
DEFINE_HOOK(&CPaneMgr::setBlackWhite, CPaneMgrSetBlackWhite);
|
||||
HookAction cpane_mgr_set_black_white_pre(ModContext*, void* args, void* retval, void* userdata) {
|
||||
// Check for magic meter
|
||||
auto pane = mods::arg<CPaneMgr*>(args, 0);
|
||||
if (dMeter2Info_getMeterClass() == NULL || pane != dMeter2Info_getMeterClass()->getMeterDrawPtr()->mpMagicMeter) {
|
||||
return HOOK_CONTINUE;
|
||||
}
|
||||
|
||||
// If magic meter, check to see we're setting lantern colors
|
||||
auto& black = mods::arg_ref<JUtility::TColor>(args, 1);
|
||||
auto& white = mods::arg_ref<JUtility::TColor>(args, 2);
|
||||
if (black != JUtility::TColor(255, 255, 140, 255) &&
|
||||
white != JUtility::TColor(230, 170, 0, 255)) {
|
||||
return HOOK_CONTINUE;
|
||||
}
|
||||
|
||||
auto maybeLanternColor = get_config_var_color(get_cvars().lanternGlowColor, true);
|
||||
if (maybeLanternColor.has_value()) {
|
||||
auto lanternColor = maybeLanternColor.value();
|
||||
black = JUtility::TColor(lanternColor.r, lanternColor.g, lanternColor.b, 255);
|
||||
white = JUtility::TColor(lanternColor.r, lanternColor.g, lanternColor.b, 255);
|
||||
}
|
||||
|
||||
return HOOK_CONTINUE;
|
||||
}
|
||||
|
||||
// Lantern Icon Meter Color
|
||||
DEFINE_HOOK(&dKantera_icon_c::setNowGauge, KanteraIconSetNowGauge);
|
||||
void kantera_icon_set_now_gauge_post(ModContext*, void* args, void* retval, void* userdata) {
|
||||
auto maybeLanternColor = get_config_var_color(get_cvars().lanternGlowColor, true);
|
||||
if (maybeLanternColor.has_value()) {
|
||||
auto lanternColor = maybeLanternColor.value();
|
||||
|
||||
auto kanteraIcon = mods::arg<dKantera_icon_c*>(args, 0);
|
||||
kanteraIcon->mpGauge->setBlackWhite(
|
||||
JUtility::TColor(lanternColor.r, lanternColor.g, lanternColor.b, 255),
|
||||
JUtility::TColor(lanternColor.r, lanternColor.g, lanternColor.b, 255));
|
||||
}
|
||||
}
|
||||
|
||||
// Light Sword Effect Color
|
||||
DEFINE_HOOK(&daAlink_c::setLightningSwordEffect, SetLightningSwordEffect);
|
||||
void set_lightning_sword_effect_post(ModContext*, void* args, void* retval, void* userdata) {
|
||||
auto maybeGlowColor = get_config_var_color(get_cvars().lightSwordGlowColor, true);
|
||||
if (maybeGlowColor.has_value()) {
|
||||
auto glowColor = maybeGlowColor.value();
|
||||
|
||||
auto link = mods::arg<daAlink_c*>(args, 0);
|
||||
// Check copied from inside the hooked function
|
||||
if (link->mEquipItem == 0x103 && link->checkNoResetFlg3(daPy_py_c::FLG3_UNK_100000)) {
|
||||
for (size_t i = 0; i < 3; i++) {
|
||||
auto emitter = dComIfGp_particle_getEmitter(link->field_0x327c[i]);
|
||||
if (emitter != NULL) {
|
||||
emitter->setGlobalEnvColor(glowColor.r, glowColor.g, glowColor.b);
|
||||
emitter->setGlobalPrmColor(glowColor.r, glowColor.g, glowColor.b);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void recolor_ui_button(ConfigVarHandle option, u64 tag, J2DScreen* screen) {
|
||||
auto buttonColorStr = get_str_option(option, "");
|
||||
if (is_valid_hex_color_str(buttonColorStr)) {
|
||||
auto color = hex_color_str_to_gx_color(buttonColorStr);
|
||||
auto element = static_cast<J2DPicture*>(screen->search(tag));
|
||||
if (element != nullptr) {
|
||||
element->setBlackWhite(JUtility::TColor(0, 0, 0, 0),
|
||||
JUtility::TColor(color.r, color.g, color.b, 0xFF));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main gameplay UI. Top left hearts and top right buttons
|
||||
DEFINE_HOOK(&dMeter2Draw_c::init, dMeter2Init);
|
||||
void d_meter_2_init_post(ModContext*, void* args, void*, void*) {
|
||||
auto dMeter2Draw = mods::arg<dMeter2Draw_c*>(args, 0);
|
||||
auto screen = dMeter2Draw->getMainScreenPtr();
|
||||
|
||||
// Heart tags on main UI
|
||||
static constexpr std::array kHeartTags = {
|
||||
MULTI_CHAR('hear_00'), MULTI_CHAR('hear_01'), MULTI_CHAR('hear_02'), MULTI_CHAR('hear_03'), MULTI_CHAR('hear_04'), MULTI_CHAR('hear_05'), MULTI_CHAR('hear_06'),
|
||||
MULTI_CHAR('hear_07'), MULTI_CHAR('hear_08'), MULTI_CHAR('hear_09'), MULTI_CHAR('hear_10'), MULTI_CHAR('hear_11'), MULTI_CHAR('hear_12'), MULTI_CHAR('hear_13'),
|
||||
MULTI_CHAR('hear_14'), MULTI_CHAR('hear_15'), MULTI_CHAR('hear_16'), MULTI_CHAR('hear_17'), MULTI_CHAR('hear_18'), MULTI_CHAR('hear_19'), MULTI_CHAR('bigh_00'),
|
||||
MULTI_CHAR('bigh_01'), MULTI_CHAR('bigh_02'), MULTI_CHAR('bigh_03')
|
||||
};
|
||||
|
||||
auto maybeHeartColor = get_config_var_color(get_cvars().heartColor);
|
||||
if (maybeHeartColor.has_value()) {
|
||||
auto heartColor = maybeHeartColor.value();
|
||||
for (auto tag : kHeartTags) {
|
||||
auto element = static_cast<J2DPicture*>(screen->search(tag));
|
||||
if (element != nullptr) {
|
||||
element->setBlackWhite(heartColor, JUtility::TColor(200, 200, 200, 255));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
|
||||
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
|
||||
recolor_ui_button(get_cvars().xButtonColor, MULTI_CHAR('x_btn'), screen);
|
||||
recolor_ui_button(get_cvars().yButtonColor, MULTI_CHAR('y_btn'), screen);
|
||||
recolor_ui_button(get_cvars().zButtonColor, MULTI_CHAR('zbtn'), screen);
|
||||
}
|
||||
|
||||
// Hearts on the file select screen
|
||||
DEFINE_HOOK(&dFile_info_c::setHeartCnt, FileInfoSetHeartCount);
|
||||
void file_info_set_heart_count_post(ModContext*, void* args, void*, void*) {
|
||||
auto fileInfo = mods::arg<dFile_info_c*>(args, 0);
|
||||
|
||||
// Heart tags on file select
|
||||
static constexpr std::array kFileSelectHeartTags {
|
||||
MULTI_CHAR('hear_20'), MULTI_CHAR('hear_21'), MULTI_CHAR('hear_22'), MULTI_CHAR('hear_23'), MULTI_CHAR('hear_24'), MULTI_CHAR('hear_25'), MULTI_CHAR('hear_26'),
|
||||
MULTI_CHAR('hear_27'), MULTI_CHAR('hear_28'), MULTI_CHAR('hear_29'), MULTI_CHAR('hear_30'), MULTI_CHAR('hear_31'), MULTI_CHAR('hear_32'), MULTI_CHAR('hear_33'),
|
||||
MULTI_CHAR('hear_34'), MULTI_CHAR('hear_35'), MULTI_CHAR('hear_36'), MULTI_CHAR('hear_37'), MULTI_CHAR('hear_38'), MULTI_CHAR('hear_39'),
|
||||
};
|
||||
|
||||
auto maybeHeartColor = get_config_var_color(get_cvars().heartColor);
|
||||
if (maybeHeartColor.has_value()) {
|
||||
auto heartColor = maybeHeartColor.value();
|
||||
for (auto tag : kFileSelectHeartTags) {
|
||||
auto element = static_cast<J2DPicture*>(fileInfo->mFileInfo.Scr->search(tag));
|
||||
if (element != nullptr) {
|
||||
element->setBlackWhite(heartColor, JUtility::TColor(200, 200, 200, 255));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Buttons that appear contextually at the bottom of the screen during gameplay
|
||||
// (Are X, Y ever used there?)
|
||||
DEFINE_HOOK(&dMeterButton_c::screenInitButton, ScreenInitButton);
|
||||
void screen_init_button_post(ModContext*, void* args, void*, void*) {
|
||||
auto meterButton = mods::arg<dMeterButton_c*>(args, 0);
|
||||
auto screen = meterButton->mpButtonScreen;
|
||||
|
||||
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn1'), screen);
|
||||
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
|
||||
recolor_ui_button(get_cvars().xButtonColor, MULTI_CHAR('x_btn'), screen);
|
||||
recolor_ui_button(get_cvars().yButtonColor, MULTI_CHAR('y_btn'), screen);
|
||||
recolor_ui_button(get_cvars().zButtonColor, MULTI_CHAR('zbtn'), screen);
|
||||
}
|
||||
|
||||
// A and B buttons when saving a file
|
||||
DEFINE_HOOK(&dMenu_save_c::screenSet, MenuSaveScreenSet);
|
||||
void menu_save_screen_set_post(ModContext*, void* args, void*, void*) {
|
||||
auto menuSave = mods::arg<dMenu_save_c*>(args, 0);
|
||||
auto screen = menuSave->mSaveSel.Scr;
|
||||
|
||||
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('wabtn'), screen);
|
||||
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('wbbtn'), screen);
|
||||
}
|
||||
|
||||
// A and B buttons when selecting a file
|
||||
DEFINE_HOOK(&dFile_select_c::screenSet, FileSelectScreenSet);
|
||||
void file_select_screen_set_post(ModContext*, void* args, void*, void*) {
|
||||
auto fileSelect = mods::arg<dFile_select_c*>(args, 0);
|
||||
auto screen = fileSelect->fileSel.Scr;
|
||||
|
||||
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('wabtn'), screen);
|
||||
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('wbbtn'), screen);
|
||||
}
|
||||
|
||||
// A button on brightness check screen
|
||||
DEFINE_HOOK(&dBrightCheck_c::screenSet, BrightCheckScreenSet);
|
||||
void bright_check_screen_set_post(ModContext*, void* args, void*, void*) {
|
||||
auto brightCheck = mods::arg<dBrightCheck_c*>(args, 0);
|
||||
auto screen = brightCheck->mBrightCheck.Scr;
|
||||
|
||||
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn1'), screen);
|
||||
}
|
||||
|
||||
// X and Y buttons on the item wheel screen
|
||||
DEFINE_HOOK(&dMenu_Ring_c::_create, MenuRingCreate);
|
||||
void menu_ring_create_post(ModContext*, void* args, void*, void*) {
|
||||
auto menuRing = mods::arg<dMenu_Ring_c*>(args, 0);
|
||||
auto screen = menuRing->mpScreen;
|
||||
|
||||
recolor_ui_button(get_cvars().xButtonColor, MULTI_CHAR('xbtn'), screen);
|
||||
recolor_ui_button(get_cvars().yButtonColor, MULTI_CHAR('ybtn'), screen);
|
||||
}
|
||||
|
||||
// A and B buttons on the main pause screen
|
||||
DEFINE_HOOK(&dMenu_Collect2D_c::_create, MenuCollect2DCreate);
|
||||
void menu_collect_2D_create_post(ModContext*, void* args, void*, void*) {
|
||||
auto menuCollect2D = mods::arg<dMenu_Collect2D_c*>(args, 0);
|
||||
auto screen = menuCollect2D->mpScreenIcon;
|
||||
|
||||
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
|
||||
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
|
||||
}
|
||||
|
||||
// A and B buttons on the fishing journal screen
|
||||
DEFINE_HOOK(&dMenu_Fishing_c::screenSetDoIcon, MenuFishingScreenSetDoIcon);
|
||||
void menu_fishing_screen_set_do_icon_post(ModContext*, void* args, void*, void*) {
|
||||
auto menuFishing = mods::arg<dMenu_Fishing_c*>(args, 0);
|
||||
auto screen = menuFishing->mpIconScreen;
|
||||
|
||||
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
|
||||
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
|
||||
}
|
||||
|
||||
// A and B buttons on the insect collection screen
|
||||
DEFINE_HOOK(&dMenu_Insect_c::screenSetDoIcon, MenuInsectScreenSetDoIcon);
|
||||
void menu_insect_screen_set_do_icon_post(ModContext*, void* args, void*, void*) {
|
||||
auto menuInsect = mods::arg<dMenu_Insect_c*>(args, 0);
|
||||
auto screen = menuInsect->mpIconScreen;
|
||||
|
||||
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
|
||||
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
|
||||
}
|
||||
|
||||
// A and B buttons on the letters screen
|
||||
DEFINE_HOOK(&dMenu_Letter_c::screenSetDoIcon, MenuLetterScreenSetDoIcon);
|
||||
void menu_letter_screen_set_do_icon_post(ModContext*, void* args, void*, void*) {
|
||||
auto menuLetter = mods::arg<dMenu_Letter_c*>(args, 0);
|
||||
auto screen = menuLetter->mpIconScreen;
|
||||
|
||||
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
|
||||
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
|
||||
}
|
||||
|
||||
// A, B, and Z buttons on option screen
|
||||
DEFINE_HOOK(&dMenu_Option_c::_create, MenuOptionCreate);
|
||||
void menu_option_create_post(ModContext*, void* args, void*, void*) {
|
||||
auto menuOption = mods::arg<dMenu_Option_c*>(args, 0);
|
||||
auto screen = menuOption->mpScreenIcon;
|
||||
auto backScreen = menuOption->mpBackScreen;
|
||||
auto tvScreen = menuOption->mpTVScreen;
|
||||
|
||||
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
|
||||
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
|
||||
recolor_ui_button(get_cvars().zButtonColor, MULTI_CHAR('g_zbtn'), backScreen);
|
||||
|
||||
// A Button on option's brightness check screen
|
||||
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn1'), tvScreen);
|
||||
}
|
||||
|
||||
// A and B buttons on the hidden skills screen
|
||||
DEFINE_HOOK(&dMenu_Skill_c::screenSetDoIcon, MenuSkillScreenSetDoIcon);
|
||||
void menu_skill_screen_set_do_icon_post(ModContext*, void* args, void*, void*) {
|
||||
auto menuSkill = mods::arg<dMenu_Skill_c*>(args, 0);
|
||||
auto screen = menuSkill->mpIconScreen;
|
||||
|
||||
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
|
||||
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn'), screen);
|
||||
}
|
||||
|
||||
// A, B, and Z buttons on the map screen
|
||||
DEFINE_HOOK(&dMenu_Fmap_c::_create, MenuFMapCreate);
|
||||
void menu_fmap_create_post(ModContext*, void* args, void*, void*) {
|
||||
auto menuFMap = mods::arg<dMenu_Fmap_c*>(args, 0);
|
||||
auto screen = menuFMap->mpDraw2DTop->mpTitleScreen;
|
||||
|
||||
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn'), screen);
|
||||
recolor_ui_button(get_cvars().bButtonColor, MULTI_CHAR('b_btn1'), screen);
|
||||
recolor_ui_button(get_cvars().zButtonColor, MULTI_CHAR('zbtn'), screen);
|
||||
}
|
||||
|
||||
// A button on the howling screen
|
||||
DEFINE_HOOK(&dMsgObject_c::talkStartInit, MsgObjectTalkStartInit);
|
||||
void msg_object_talk_start_init_post(ModContext*, void* args, void*, void*) {
|
||||
auto msgObject = mods::arg<dMsgObject_c*>(args, 0);
|
||||
|
||||
// If textbox kind is howling
|
||||
if (msgObject->mFukiKind == 17) {
|
||||
auto screen = msgObject->mpScrnDraw->mpScreen;
|
||||
|
||||
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('abtn'), screen);
|
||||
}
|
||||
}
|
||||
|
||||
// A button when on Epona
|
||||
DEFINE_HOOK(&dMeterHakusha_c::_create, MeterHakushaCreate);
|
||||
void meter_hakusha_create_post(ModContext*, void* args, void*, void*) {
|
||||
auto meterHakusha = mods::arg<dMeterHakusha_c*>(args, 0);
|
||||
auto screen = meterHakusha->mpButtonScreen;
|
||||
|
||||
recolor_ui_button(get_cvars().aButtonColor, MULTI_CHAR('a_btn1'), screen);
|
||||
}
|
||||
|
||||
// A, B, X, and Y button icons in text
|
||||
DEFINE_HOOK(&COutFont_c::createPane, OutFontCreatePane);
|
||||
void out_font_create_pane_post(ModContext*, void* args, void*, void*) {
|
||||
auto outFont = mods::arg<COutFont_c*>(args, 0);
|
||||
auto paneArr = outFont->mpPane;
|
||||
|
||||
auto maybeAButtonColor = get_config_var_color(get_cvars().aButtonColor);
|
||||
if (maybeAButtonColor.has_value()) {
|
||||
auto aButtonColor = maybeAButtonColor.value();
|
||||
paneArr[0]->setBlackWhite(
|
||||
JUtility::TColor(255, 255, 255, 0),
|
||||
JUtility::TColor(aButtonColor.r,aButtonColor.g,aButtonColor.b, 255));
|
||||
}
|
||||
|
||||
auto maybeBButtonColor = get_config_var_color(get_cvars().bButtonColor);
|
||||
if (maybeBButtonColor.has_value()) {
|
||||
auto bButtonColor = maybeBButtonColor.value();
|
||||
paneArr[1]->setBlackWhite(
|
||||
JUtility::TColor(255, 255, 255, 0),
|
||||
JUtility::TColor(bButtonColor.r,bButtonColor.g,bButtonColor.b, 255));
|
||||
}
|
||||
|
||||
// Condition copied from hooked function
|
||||
auto xyBlack = JUtility::TColor(255, 255, 255, 0);
|
||||
if (outFont->field_0x242 == 1) {
|
||||
xyBlack = JUtility::TColor(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
auto maybeXButtonColor = get_config_var_color(get_cvars().xButtonColor);
|
||||
if (maybeXButtonColor.has_value()) {
|
||||
auto xButtonColor = maybeXButtonColor.value();
|
||||
paneArr[5]->setBlackWhite(xyBlack, JUtility::TColor(xButtonColor.r,xButtonColor.g,xButtonColor.b, 255));
|
||||
}
|
||||
|
||||
auto maybeYButtonColor = get_config_var_color(get_cvars().yButtonColor);
|
||||
if (maybeYButtonColor.has_value()) {
|
||||
auto yButtonColor = maybeYButtonColor.value();
|
||||
paneArr[6]->setBlackWhite(xyBlack, JUtility::TColor(yButtonColor.r,yButtonColor.g,yButtonColor.b, 255));
|
||||
}
|
||||
}
|
||||
|
||||
// Heart icon in text
|
||||
DEFINE_HOOK(&COutFontSet_c::drawFont, OutFontSetDrawFont);
|
||||
void out_font_set_draw_font_post(ModContext*, void* args, void*, void*) {
|
||||
auto outFontSet = mods::arg<COutFontSet_c*>(args, 0);
|
||||
|
||||
auto maybeHeartColor = get_config_var_color(get_cvars().heartColor);
|
||||
if (maybeHeartColor.has_value()) {
|
||||
auto heartColor = maybeHeartColor.value();
|
||||
u32 heartColor_u32 = heartColor.r << 24 | heartColor.g << 16 | heartColor.b << 8 | 0xFF;
|
||||
if (outFontSet->getType() == 0x1B) { // Heart icon type
|
||||
outFontSet->mColor = heartColor_u32;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Heart Drop/Piece of Heart/Heart Container model color
|
||||
DEFINE_HOOK(&daItemBase_c::CreateItemHeap, ItemBaseCreateItemHeap);
|
||||
void item_base_create_item_heap_post(ModContext*, void* args, void*, void*) {
|
||||
auto itemBase = mods::arg<daItemBase_c*>(args, 0);
|
||||
if (itemBase == NULL) {
|
||||
return;
|
||||
}
|
||||
auto itemNo = itemBase->m_itemNo;
|
||||
if (itemNo == dItemNo_HEART_e || itemNo == dItemNo_UTAWA_HEART_e || itemNo == dItemNo_KAKERA_HEART_e) {
|
||||
auto maybeHeartColor = get_config_var_color(get_cvars().heartColor);
|
||||
if (maybeHeartColor.has_value()) {
|
||||
auto heartColor = maybeHeartColor.value();
|
||||
auto heartColorS10 = GXColorS10(heartColor.r, heartColor.g, heartColor.b, heartColor.a);
|
||||
|
||||
// Edit inner heart material color for Piece of Heart / Heart Container
|
||||
if (itemNo == dItemNo_UTAWA_HEART_e || itemNo == dItemNo_KAKERA_HEART_e) {
|
||||
*itemBase->mpModel->getModelData()->getMaterialNodePointer(3)->getTevKColor(1) = heartColor;
|
||||
*itemBase->mpModel->getModelData()->getMaterialNodePointer(3)->getTevColor(1) = heartColorS10;
|
||||
}
|
||||
|
||||
const u8 heartColorRGB[3] = {heartColor.r,heartColor.g,heartColor.b};
|
||||
u8** cRegTable = reinterpret_cast<u8**>(&itemBase->mpBrkAnm->getBrkAnm()->mAnmCRegDataR);
|
||||
u8** kRegTable = reinterpret_cast<u8**>(&itemBase->mpBrkAnm->getBrkAnm()->mAnmKRegDataR);
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
u8* cReg = cRegTable[i];
|
||||
u8* kReg = kRegTable[i];
|
||||
|
||||
auto curColor = heartColorRGB[i];
|
||||
|
||||
// Set heart drop inner heart color
|
||||
cReg[0x3] = curColor;
|
||||
cReg[0xB] = curColor;
|
||||
|
||||
// Set heart drop outer heart color
|
||||
kReg[0x3] = curColor;
|
||||
kReg[0xB] = curColor;
|
||||
|
||||
if (itemNo == dItemNo_KAKERA_HEART_e) {
|
||||
cReg[0x13] = curColor;
|
||||
cReg[0x1B] = curColor;
|
||||
kReg[0x13] = curColor;
|
||||
kReg[0x1B] = curColor;
|
||||
|
||||
}
|
||||
if (itemNo == dItemNo_UTAWA_HEART_e) {
|
||||
cReg[0x13] = curColor;
|
||||
kReg[0x13] = curColor;
|
||||
kReg[0x1B] = curColor;
|
||||
kReg[0x23] = curColor;
|
||||
kReg[0x2B] = curColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Midna Hair Color
|
||||
DEFINE_HOOK(&daMidna_c::create, MidnaCreate);
|
||||
void midna_create_post(ModContext*, void* args, void* retval, void*) {
|
||||
auto step = reinterpret_cast<int*>(retval);
|
||||
// Don't set colors if midna isn't done loading
|
||||
if (*step != cPhs_COMPLEATE_e) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto midna = mods::arg<daMidna_c*>(args, 0);
|
||||
midna->field_0x6e0 = *get_midna_hair_normalColor();
|
||||
if (dKy_darkworld_check()) {
|
||||
midna->field_0x6e8 = *get_midna_hair_normalKColor();
|
||||
midna->field_0x6ec = *get_midna_hair_normalKColor2();
|
||||
} else {
|
||||
midna->field_0x6e8 = *get_midna_hair_lNormalKColor();
|
||||
midna->field_0x6ec = *get_midna_hair_lNormalKColor2();
|
||||
}
|
||||
}
|
||||
|
||||
// Override Midna Hair Color Part 2
|
||||
DEFINE_HOOK(&daMidna_c::setBodyPartMatrix, MidnaSetBodyPartMatrix);
|
||||
|
||||
static GXColorS10 midnaField0x6e0{};
|
||||
static GXColor midnaField0x6e8{};
|
||||
static GXColor midnaField0x6ec{};
|
||||
|
||||
// Copy the original values before setBodyPartMatrix runs
|
||||
HookAction midna_set_body_part_matrix_pre(ModContext*, void* args, void* retval, void* userdata) {
|
||||
auto midna = mods::arg<daMidna_c*>(args, 0);
|
||||
|
||||
midnaField0x6e0 = midna->field_0x6e0;
|
||||
midnaField0x6e8 = midna->field_0x6e8;
|
||||
midnaField0x6ec = midna->field_0x6ec;
|
||||
|
||||
return HOOK_CONTINUE;
|
||||
}
|
||||
|
||||
void midna_set_body_part_matrix_post(ModContext*, void* args, void* retval, void* userdata) {
|
||||
auto midna = mods::arg<daMidna_c*>(args, 0);
|
||||
if (midna->mpHairhandBmd != NULL) {
|
||||
// Restore the original values from before setBodyPartMatrix ran (this undoes the chase)
|
||||
midna->field_0x6e0 = midnaField0x6e0;
|
||||
midna->field_0x6e8 = midnaField0x6e8;
|
||||
midna->field_0x6ec = midnaField0x6ec;
|
||||
|
||||
// Statement copied from inside function to determine colors
|
||||
bool bigColors = midna->checkStateFlg0(daMidna_c::FLG0_UNK_10000000) ||
|
||||
midna->mBckHeap[2].getIdx() == daMidna_c::m_anmDataTable[daMidna_c::ANM_HAIR].mResID ||
|
||||
midna->mBckHeap[2].getIdx() == daMidna_c::m_anmDataTable[daMidna_c::ANM_S_TAKES].mResID ||
|
||||
midna->mBckHeap[2].getIdx() == daMidna_c::m_anmDataTable[daMidna_c::ANM_S_WAITS].mResID ||
|
||||
midna->mBckHeap[2].getIdx() == daMidna_c::m_anmDataTable[daMidna_c::ANM_S_PACKAWAY].mResID ||
|
||||
midna->mBckHeap[2].getIdx() == daMidna_c::m_anmDataTable[daMidna_c::ANM_GRABST].mResID ||
|
||||
midna->checkEndResetStateFlg0(daMidna_c::ERFLG0_UNK_40) ||
|
||||
dComIfGp_checkPlayerStatus1(0, 0x800000);
|
||||
|
||||
GXColorS10 color{};
|
||||
GXColor kcolor1{};
|
||||
GXColor kcolor2{};
|
||||
|
||||
// Set our own colors
|
||||
if (bigColors) {
|
||||
kcolor1 = *get_midna_hair_bigKColor();
|
||||
if (dKy_darkworld_check()) {
|
||||
color = *get_midna_hair_bigColor();
|
||||
kcolor2 = *get_midna_hair_normalKColor2();
|
||||
} else {
|
||||
color = *get_midna_hair_lBigColor();
|
||||
kcolor2 = *get_midna_hair_lBigKColor2();
|
||||
}
|
||||
} else {
|
||||
color = *get_midna_hair_normalColor();
|
||||
if (dKy_darkworld_check()) {
|
||||
kcolor1 = *get_midna_hair_normalKColor();
|
||||
kcolor2 = *get_midna_hair_normalKColor2();
|
||||
} else {
|
||||
kcolor1 = *get_midna_hair_lNormalKColor();
|
||||
kcolor2 = *get_midna_hair_lNormalKColor2();
|
||||
}
|
||||
}
|
||||
|
||||
// Reapply the chase that happens in the function
|
||||
cLib_chaseS(&midna->field_0x6e0.r, color.r, 10);
|
||||
cLib_chaseS(&midna->field_0x6e0.g, color.g, 10);
|
||||
cLib_chaseS(&midna->field_0x6e0.b, color.b, 10);
|
||||
cLib_chaseUC(&midna->field_0x6e8.r, kcolor1.r, 10);
|
||||
cLib_chaseUC(&midna->field_0x6e8.g, kcolor1.g, 10);
|
||||
cLib_chaseUC(&midna->field_0x6e8.b, kcolor1.b, 10);
|
||||
cLib_chaseUC(&midna->field_0x6ec.r, kcolor2.r, 10);
|
||||
cLib_chaseUC(&midna->field_0x6ec.g, kcolor2.g, 10);
|
||||
cLib_chaseUC(&midna->field_0x6ec.b, kcolor2.b, 10);
|
||||
}
|
||||
}
|
||||
|
||||
// Midna Charge Ring Color
|
||||
DEFINE_HOOK(&daAlink_c::setWolfLockDomeModel, SetWolfLockDomeModel);
|
||||
void wolf_lock_dome_model_post(ModContext*, void* args, void* retval, void* userdata) {
|
||||
auto domeRingColorStr = get_str_option(get_cvars().midnaChargeRingColor, "");
|
||||
if (is_valid_hex_color_str(domeRingColorStr)) {
|
||||
auto domeRingColor = hex_color_str_to_gx_color(domeRingColorStr);
|
||||
const u8 domeWave1RGBA[3] = {domeRingColor.r, domeRingColor.g, domeRingColor.b};
|
||||
const u8 domeWave2RGBA[3] = {domeRingColor.r, domeRingColor.g, domeRingColor.b};
|
||||
u8** chromaRegisterTable = reinterpret_cast<u8**>(&daAlink_getAlinkActorClass()->field_0x0724->mAnmCRegDataR);
|
||||
|
||||
for (int i = 0; i < 3; i++)
|
||||
{
|
||||
u8* currentTable = chromaRegisterTable[i];
|
||||
const u8 currentWave1Color = domeWave1RGBA[i];
|
||||
const u8 currentWave2Color = domeWave2RGBA[i];
|
||||
const u8 currentBaseColor = (currentWave1Color + currentWave2Color) / 2;
|
||||
|
||||
currentTable[0x3] = currentBaseColor; // Set Alpha for the ring base
|
||||
currentTable[0x13] = currentWave1Color; // Set Alpha for ring wave 1
|
||||
currentTable[0x23] = currentWave2Color; // Set Alpha for ring wave 2
|
||||
currentTable[0xB] = currentBaseColor; // Set Alpha for darkworld ring base
|
||||
currentTable[0x1B] = currentWave1Color; // Set Alpha for darkworld ring wave 1
|
||||
currentTable[0x2B] = currentWave2Color; // Set Alpha for darkworld ring wave 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#define ADD_POST_HOOK(defined_hook, function, original) \
|
||||
result = mods::hook::add_post<defined_hook>(function); \
|
||||
if (result != MOD_OK) { \
|
||||
mods::log::debug("failed to add post hook to" #original ", Result {}", static_cast<int>(result)); \
|
||||
return result; \
|
||||
}
|
||||
|
||||
#define ADD_PRE_HOOK(defined_hook, function, original) \
|
||||
result = mods::hook::add_pre<defined_hook>(function); \
|
||||
if (result != MOD_OK) { \
|
||||
mods::log::debug("failed to add pre hook to" #original ", Result {}", static_cast<int>(result)); \
|
||||
return result; \
|
||||
}
|
||||
|
||||
ModResult add_all_hooks() {
|
||||
ModResult result{};
|
||||
|
||||
// Main hook for texture recoloring
|
||||
ADD_POST_HOOK(MountArchiveExecute, mount_archive_execute_post, mountArchive_execute)
|
||||
|
||||
// Hooks for lantern glow
|
||||
ADD_POST_HOOK(WolfEyeLightSet, wolf_eye_light_set_post, dKy_WolfEyeLight_set)
|
||||
ADD_POST_HOOK(PreKandelaarDraw, pre_kandelaar_draw_post, daAlink_c::preKandelaarDraw)
|
||||
|
||||
// Hooks for lantern meter color
|
||||
ADD_PRE_HOOK(CPaneMgrSetBlackWhite, cpane_mgr_set_black_white_pre, CPaneMgr::setBlackWhite)
|
||||
ADD_POST_HOOK(KanteraIconSetNowGauge, kantera_icon_set_now_gauge_post, dKantera_icon_c::setNowGauge)
|
||||
|
||||
// Hook for midna charge ring
|
||||
ADD_POST_HOOK(SetWolfLockDomeModel, wolf_lock_dome_model_post, daAlink_c::setWolfLockDomeModel)
|
||||
|
||||
// Hook for light sword glow
|
||||
ADD_POST_HOOK(SetLightningSwordEffect, set_lightning_sword_effect_post, daAlink_c::setLightningSwordEffect)
|
||||
|
||||
// Hooks for Midna Hair Color
|
||||
ADD_POST_HOOK(MidnaCreate, midna_create_post, daMidna_c::create)
|
||||
ADD_PRE_HOOK(MidnaSetBodyPartMatrix, midna_set_body_part_matrix_pre, daMidna_c::setBodyPartMatrix)
|
||||
ADD_POST_HOOK(MidnaSetBodyPartMatrix, midna_set_body_part_matrix_post, daMidna_c::setBodyPartMatrix)
|
||||
|
||||
// Hooks for UI colors
|
||||
ADD_POST_HOOK(dMeter2Init, d_meter_2_init_post, dMeter2Draw_c::init)
|
||||
ADD_POST_HOOK(FileInfoSetHeartCount, file_info_set_heart_count_post, dFile_info_c::setHeartCnt)
|
||||
ADD_POST_HOOK(ScreenInitButton, screen_init_button_post, dMeterButton_c::screenInitButton)
|
||||
ADD_POST_HOOK(MenuSaveScreenSet, menu_save_screen_set_post, dMenu_save_c::screenSet)
|
||||
ADD_POST_HOOK(FileSelectScreenSet, file_select_screen_set_post, dFile_select_c::screenSet)
|
||||
ADD_POST_HOOK(BrightCheckScreenSet, bright_check_screen_set_post, dBrightCheck_c::screenSet)
|
||||
ADD_POST_HOOK(MenuRingCreate, menu_ring_create_post, dMenu_Ring_c::_create)
|
||||
ADD_POST_HOOK(MenuCollect2DCreate, menu_collect_2D_create_post, dMenu_Collect2D_c::_create)
|
||||
ADD_POST_HOOK(MenuFishingScreenSetDoIcon, menu_fishing_screen_set_do_icon_post, dMenu_Fishing_c::screenSetDoIcon)
|
||||
ADD_POST_HOOK(MenuInsectScreenSetDoIcon, menu_insect_screen_set_do_icon_post, dMenu_Insect_c::screenSetDoIcon)
|
||||
ADD_POST_HOOK(MenuLetterScreenSetDoIcon, menu_letter_screen_set_do_icon_post, dMenu_Letter_c::screenSetDoIcon)
|
||||
ADD_POST_HOOK(MenuOptionCreate, menu_option_create_post, dMenu_Option_c::_create)
|
||||
ADD_POST_HOOK(MenuSkillScreenSetDoIcon, menu_skill_screen_set_do_icon_post, dMenu_Skill_c::screenSetDoIcon)
|
||||
ADD_POST_HOOK(OutFontCreatePane, out_font_create_pane_post, COutFont_c::createPane)
|
||||
ADD_POST_HOOK(OutFontSetDrawFont, out_font_set_draw_font_post, COutFontSet_c::drawFont)
|
||||
ADD_POST_HOOK(MenuFMapCreate, menu_fmap_create_post, dMenu_Fmap_c::_create)
|
||||
ADD_POST_HOOK(MsgObjectTalkStartInit, msg_object_talk_start_init_post, dMsgObject_c::talkStartInit)
|
||||
ADD_POST_HOOK(MeterHakushaCreate, meter_hakusha_create_post, dMeterHakusha_c::_create)
|
||||
|
||||
// Heart Model Color
|
||||
ADD_POST_HOOK(ItemBaseCreateItemHeap, item_base_create_item_heap_post, daItemBase_c::CreateItemHeap)
|
||||
|
||||
return MOD_OK;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/service.hpp"
|
||||
|
||||
ModResult add_all_hooks();
|
||||
@@ -0,0 +1,141 @@
|
||||
#include "midna_hair_color.hpp"
|
||||
#include "mod.hpp"
|
||||
|
||||
#include "mods/svc/log.hpp"
|
||||
|
||||
#include <array>
|
||||
|
||||
static constexpr std::array<std::array<GXColor, 9>, 10> midnaHairColors = {{
|
||||
// Default
|
||||
{{
|
||||
/*l_lNormalKColor*/ /*l_normalKColor*/ /*l_normalColor*/
|
||||
{0xFF, 0xDC, 0x00}, {0xB4, 0x87, 0x00}, {0x50, 0x00, 0x00},
|
||||
/*l_bigKColor*/ /*l_lBigColor*/ /*l_bigColor*/
|
||||
{0x50, 0x00, 0x00}, {0xFF, 0x78, 0x00}, {0xFF, 0x64, 0x78},
|
||||
/*l_lNormalKColor2*//*l_normalKColor2*/ /*l_lBigKColor2*/
|
||||
{0x00, 0xC3, 0xEB}, {0xC3, 0xC3, 0x00}, {0xAA, 0xFF, 0xC3}
|
||||
}},
|
||||
// Pink
|
||||
{{
|
||||
{0xF5, 0xCF, 0xF3}, {0xAD, 0x7F, 0x7F}, {0x1B, 0x00, 0x20},
|
||||
{0x3C, 0x02, 0x58}, {0xE3, 0x72, 0xF2}, {0xE3, 0x5F, 0xF8},
|
||||
{0xDD, 0x00, 0xEB}, {0xDD, 0x00, 0xC3}, {0xF6, 0x4C, 0xFF}
|
||||
}},
|
||||
// Red
|
||||
{{
|
||||
{0xE4, 0x65, 0x41}, {0xA1, 0x3E, 0x22}, {0x21, 0x00, 0x00},
|
||||
{0x4F, 0x02, 0x01}, {0xF0, 0x3A, 0x25}, {0xF0, 0x30, 0x8C},
|
||||
{0xEB, 0x00, 0x00}, {0xEB, 0x00, 0x00}, {0xFF, 0x4F, 0x3A}
|
||||
}},
|
||||
// Yellow
|
||||
{{
|
||||
{0x91, 0x83, 0x0E}, {0x66, 0x50, 0x07}, {0x0E, 0x0B, 0x00},
|
||||
{0x24, 0x25, 0x02}, {0xCB, 0xB7, 0x00}, {0xCB, 0x99, 0x78},
|
||||
{0xEB, 0xDE, 0x00}, {0xEB, 0xDE, 0x00}, {0xFF, 0xF8, 0xBF}
|
||||
}},
|
||||
// Green
|
||||
{{
|
||||
{0x35, 0x79, 0x53}, {0x25, 0x4A, 0x2B}, {0x00, 0x0E, 0x05},
|
||||
{0x0A, 0x29, 0x10}, {0x00, 0xB6, 0x6F}, {0x00, 0x98, 0xB3},
|
||||
{0x1F, 0xEB, 0x00}, {0x1F, 0xEB, 0x00}, {0x9A, 0xFF, 0x81}
|
||||
}},
|
||||
// Blue
|
||||
{{
|
||||
{0x00, 0x72, 0xFF}, {0x00, 0x46, 0x85}, {0x00, 0x08, 0x28},
|
||||
{0x16, 0x1B, 0x5D}, {0x00, 0x60, 0xFF}, {0x00, 0x50, 0xFF},
|
||||
{0x00, 0x48, 0xEB}, {0x00, 0x48, 0xC3}, {0x3A, 0x66, 0xFF}
|
||||
}},
|
||||
// Purple
|
||||
{{
|
||||
{0x6F, 0x34, 0xFF}, {0x4E, 0x20, 0x85}, {0x0D, 0x00, 0x34},
|
||||
{0x15, 0x08, 0x79}, {0x62, 0x00, 0xFF}, {0x62, 0x00, 0xFF},
|
||||
{0x7B, 0x00, 0xEB}, {0x7B, 0x00, 0xC3}, {0x94, 0x3E, 0xFF}
|
||||
}},
|
||||
// Brown
|
||||
{{
|
||||
{0x00, 0x00, 0x00}, {0x00, 0x00, 0x00}, {0x1A, 0x05, 0x00},
|
||||
{0x3C, 0x19, 0x0E}, {0x3F, 0x1D, 0x0B}, {0x3F, 0x18, 0x7E},
|
||||
{0x3F, 0x1D, 0x0B}, {0x3F, 0x1D, 0x09}, {0x59, 0x32, 0x1E}
|
||||
}},
|
||||
// White
|
||||
{{
|
||||
{0xF0, 0xF1, 0xF1}, {0xA9, 0x94, 0x7E}, {0x09, 0x0B, 0x0C},
|
||||
{0x22, 0x24, 0x24}, {0xFF, 0xFF, 0xFF}, {0xFF, 0xD5, 0xFF},
|
||||
{0xEA, 0xEA, 0xEA}, {0xEA, 0xEA, 0xC2}, {0xF3, 0xF3, 0xF3}
|
||||
}},
|
||||
// Black
|
||||
{{
|
||||
{0x00, 0x00, 0x00}, {0x00, 0x00, 0x00}, {0x0B, 0x0B, 0x0B},
|
||||
{0x23, 0x23, 0x23}, {0x00, 0x00, 0x00}, {0x00, 0x00, 0x78},
|
||||
{0x00, 0x00, 0x00}, {0x00, 0x00, 0x00}, {0x00, 0x00, 0x00}
|
||||
}}
|
||||
}};
|
||||
|
||||
static struct {
|
||||
GXColorS10 l_normalColor = { 0x50, 0x00, 0x00, 0x00 };
|
||||
GXColor l_normalKColor = { 0xB4, 0x87, 0x00, 0x00 };
|
||||
GXColor l_normalKColor2 = { 0x00, 0xC3, 0xC3, 0x00 };
|
||||
GXColorS10 l_bigColor = { 0xFF, 0x64, 0x78, 0x00 };
|
||||
GXColor l_bigKColor = { 0x1E, 0x00, 0x00, 0x00 };
|
||||
GXColor l_lNormalKColor = { 0xFF, 0xDC, 0x00, 0x00 };
|
||||
GXColor l_lNormalKColor2 = { 0x00, 0xC3, 0xEB, 0x00 };
|
||||
GXColorS10 l_lBigColor = { 0xFF, 0x78, 0x00, 0x00 };
|
||||
GXColor l_lBigKColor2 = { 0xAA, 0xFF, 0xC3, 0x00 };
|
||||
} currentMidnaHairColors;
|
||||
|
||||
void set_all_midna_hair_colors() {
|
||||
auto& g_cvars = get_cvars();
|
||||
auto hairBaseColor = get_int_option(g_cvars.midnaHairBaseColor, 0);
|
||||
auto hairTipsColor = get_int_option(g_cvars.midnaHairTipsColor, 0);
|
||||
|
||||
// Colors we have to convert to GXColorS10
|
||||
auto& normalColor = midnaHairColors.at(hairBaseColor)[2];
|
||||
auto& bigColor = midnaHairColors.at(hairBaseColor)[5];
|
||||
auto& lBigColor = midnaHairColors.at(hairBaseColor)[4];
|
||||
|
||||
currentMidnaHairColors.l_normalColor = GXColorS10{normalColor.r, normalColor.g, normalColor.b};
|
||||
currentMidnaHairColors.l_normalKColor = midnaHairColors.at(hairBaseColor)[1];
|
||||
currentMidnaHairColors.l_normalKColor2 = midnaHairColors.at(hairTipsColor)[7];
|
||||
currentMidnaHairColors.l_bigColor = GXColorS10{bigColor.r, bigColor.g, bigColor.b};
|
||||
currentMidnaHairColors.l_bigKColor = midnaHairColors.at(hairBaseColor)[3];
|
||||
currentMidnaHairColors.l_lNormalKColor = midnaHairColors.at(hairBaseColor)[0];
|
||||
currentMidnaHairColors.l_lNormalKColor2 = midnaHairColors.at(hairTipsColor)[6];
|
||||
currentMidnaHairColors.l_lBigColor = GXColorS10{lBigColor.r, lBigColor.g, lBigColor.b};
|
||||
currentMidnaHairColors.l_lBigKColor2 = midnaHairColors.at(hairTipsColor)[8];
|
||||
}
|
||||
|
||||
const GXColorS10* get_midna_hair_normalColor() {
|
||||
return ¤tMidnaHairColors.l_normalColor;
|
||||
}
|
||||
|
||||
const GXColor* get_midna_hair_normalKColor() {
|
||||
return ¤tMidnaHairColors.l_normalKColor;
|
||||
}
|
||||
|
||||
const GXColor* get_midna_hair_normalKColor2() {
|
||||
return ¤tMidnaHairColors.l_normalKColor2;
|
||||
}
|
||||
|
||||
const GXColorS10* get_midna_hair_bigColor() {
|
||||
return ¤tMidnaHairColors.l_bigColor;
|
||||
}
|
||||
|
||||
const GXColor* get_midna_hair_bigKColor() {
|
||||
return ¤tMidnaHairColors.l_bigKColor;
|
||||
}
|
||||
|
||||
const GXColor* get_midna_hair_lNormalKColor() {
|
||||
return ¤tMidnaHairColors.l_lNormalKColor;
|
||||
}
|
||||
|
||||
const GXColor* get_midna_hair_lNormalKColor2() {
|
||||
return ¤tMidnaHairColors.l_lNormalKColor2;
|
||||
}
|
||||
|
||||
const GXColorS10* get_midna_hair_lBigColor() {
|
||||
return ¤tMidnaHairColors.l_lBigColor;
|
||||
}
|
||||
|
||||
const GXColor* get_midna_hair_lBigKColor2() {
|
||||
return ¤tMidnaHairColors.l_lBigKColor2;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
|
||||
#include <gx.h>
|
||||
|
||||
void set_all_midna_hair_colors();
|
||||
|
||||
const GXColorS10* get_midna_hair_normalColor();
|
||||
const GXColor* get_midna_hair_normalKColor();
|
||||
const GXColor* get_midna_hair_normalKColor2();
|
||||
const GXColorS10* get_midna_hair_bigColor();
|
||||
const GXColor* get_midna_hair_bigKColor();
|
||||
const GXColor* get_midna_hair_lNormalKColor();
|
||||
const GXColor* get_midna_hair_lNormalKColor2();
|
||||
const GXColorS10* get_midna_hair_lBigColor();
|
||||
const GXColor* get_midna_hair_lBigKColor2();
|
||||
@@ -0,0 +1,282 @@
|
||||
#include "mod.hpp"
|
||||
#include "hooks.hpp"
|
||||
#include "color_utils.hpp"
|
||||
#include "midna_hair_color.hpp"
|
||||
#include "option_descriptions.hpp"
|
||||
|
||||
#include "mods/service.hpp"
|
||||
#include "mods/svc/config.h"
|
||||
#include "mods/svc/hook.hpp"
|
||||
#include "mods/svc/log.h"
|
||||
#include "mods/svc/log.hpp"
|
||||
#include "mods/svc/ui.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
DEFINE_MOD();
|
||||
IMPORT_SERVICE(LogService, svc_log);
|
||||
IMPORT_SERVICE(ConfigService, svc_config);
|
||||
IMPORT_SERVICE(HookService, svc_hook);
|
||||
IMPORT_SERVICE(UiService, svc_ui);
|
||||
|
||||
static cvars g_cvars;
|
||||
|
||||
cvars& get_cvars() {
|
||||
return g_cvars;
|
||||
}
|
||||
|
||||
std::string get_str_option(ConfigVarHandle handle, const std::string& fallback) {
|
||||
std::string value{};
|
||||
size_t outLength{};
|
||||
svc_config->get_string(mod_ctx, handle, NULL, 0, &outLength);
|
||||
value.resize(outLength);
|
||||
if (handle == 0 || svc_config->get_string(mod_ctx, handle, value.data(), value.size() + 1, &outLength) != MOD_OK) {
|
||||
return fallback;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
int64_t get_int_option(ConfigVarHandle handle, int64_t fallback) {
|
||||
int64_t value = fallback;
|
||||
if (handle == 0 || svc_config->get_int(mod_ctx, handle, &value) != MOD_OK) {
|
||||
return fallback;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
namespace {
|
||||
UiWindowHandle g_cosmeticsWindow = 0;
|
||||
|
||||
ModResult register_str_option(
|
||||
const char* name, const char* defaultValue, ConfigVarHandle& outHandle, ModError* error) {
|
||||
ConfigVarDesc cvarDesc = CONFIG_VAR_DESC_INIT;
|
||||
cvarDesc.name = name;
|
||||
cvarDesc.type = CONFIG_VAR_STRING;
|
||||
cvarDesc.default_string = defaultValue;
|
||||
if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) {
|
||||
return mods::set_error(error, MOD_ERROR, "failed to register cosmetics option");
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult register_int_option(
|
||||
const char* name, int64_t defaultValue, ConfigVarHandle& outHandle, ModError* error) {
|
||||
ConfigVarDesc cvarDesc = CONFIG_VAR_DESC_INIT;
|
||||
cvarDesc.name = name;
|
||||
cvarDesc.type = CONFIG_VAR_INT;
|
||||
cvarDesc.default_int = defaultValue;
|
||||
if (svc_config->register_var(mod_ctx, &cvarDesc, &outHandle) != MOD_OK) {
|
||||
return mods::set_error(error, MOD_ERROR, "failed to register cosmetics option");
|
||||
}
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void add_control(UiElementHandle pane, const UiControlDesc& desc) {
|
||||
auto result = svc_ui->pane_add_control(mod_ctx, pane, &desc, nullptr);
|
||||
if (result != MOD_OK) {
|
||||
mods::log::debug("pane_add_control failed {}", static_cast<int>(result));
|
||||
}
|
||||
}
|
||||
|
||||
void add_cosmetic_option(UiElementHandle left, ConfigVarHandle cvar, const char* name, const char* helpRml) {
|
||||
UiControlDesc control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_STRING;
|
||||
control.label = name;
|
||||
control.help_rml = helpRml;
|
||||
control.binding = UI_BINDING_CONFIG_VAR;
|
||||
control.config_var = cvar;
|
||||
control.max_length = 6;
|
||||
add_control(left, control);
|
||||
}
|
||||
|
||||
void add_cosmetic_option_with_rainbow(UiElementHandle left, ConfigVarHandle cvar, const char* name, const char* helpRml) {
|
||||
UiControlDesc control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_STRING;
|
||||
control.label = name;
|
||||
control.help_rml = helpRml;
|
||||
control.binding = UI_BINDING_CONFIG_VAR;
|
||||
control.config_var = cvar;
|
||||
control.max_length = 7;
|
||||
add_control(left, control);
|
||||
}
|
||||
|
||||
void add_midna_hair_option(UiElementHandle left, ConfigVarHandle cvar, const std::string& name) {
|
||||
static const char* kMidnaHairOptions[] = {
|
||||
"Default", "Pink", "Red", "Yellow", "Green", "Blue", "Purple", "Brown", "White", "Black"
|
||||
};
|
||||
UiControlDesc control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_SELECT;
|
||||
control.label = name.c_str();
|
||||
control.help_rml = "Choose Midna's hair color.";
|
||||
control.binding = UI_BINDING_CONFIG_VAR;
|
||||
control.config_var = cvar;
|
||||
control.options = kMidnaHairOptions;
|
||||
control.option_count = 10;
|
||||
add_control(left, control);
|
||||
}
|
||||
|
||||
ModResult build_equipment_colors_tab(
|
||||
ModContext*, UiWindowHandle, UiElementHandle left, UiElementHandle right, void*, ModError*) {
|
||||
(void)right;
|
||||
|
||||
add_cosmetic_option(left, g_cvars.herosTunicCapColor, "Hero's Tunic Cap Color", kLinksCapColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.herosTunicTorsoColor, "Hero's Tunic Body Color", kLinksShirtColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.herosTunicSkirtColor, "Hero's Tunic Skirt Color", kLinksSkirtColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.zoraArmorCapColor, "Zora Armor Cap Color", kZoraCapColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.zoraArmorHelmetColor, "Zora Armor Helmet Color", kZoraHelmetColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.zoraArmorTorsoColor, "Zora Armor Torso Color", kZoraTorsoColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.zoraArmorScalesColor, "Zora Armor Scales Color", kZoraScalesColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.zoraArmorFlippersColor, "Zora Armor Flippers Color", kZoraFlippersColorsExplanation);
|
||||
add_cosmetic_option_with_rainbow(left, g_cvars.lanternGlowColor, "Lantern Glow Color", kLanternGlowColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.woodenSwordColor, "Wooden Sword Color", kWoodenSwordColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.ordonSwordBladeColor, "Ordon Sword Blade Color", kOrdonBladeColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.ordonSwordHandleColor, "Ordon Sword Handle Color", kOrdonHandleColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.msBladeColor, "Master Sword Blade Color", kMSBladeColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.msHandleColor, "Master Sword Handle Color", kMSHandleColorsExplanation);
|
||||
add_cosmetic_option_with_rainbow(left, g_cvars.lightSwordGlowColor, "Light Sword Glow Color", kSwordGlowColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.boomerangColor, "Boomerang Color", kBoomerangColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.ironBootsColor, "Iron Boots Color", kIronBootsColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.spinnerColor, "Spinner Color", kSpinnerColorsExplanation);
|
||||
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult build_ui_colors_tab(
|
||||
ModContext*, UiWindowHandle, UiElementHandle left, UiElementHandle right, void*, ModError*) {
|
||||
(void)right;
|
||||
|
||||
add_cosmetic_option(left, g_cvars.aButtonColor, "A Button Color", kAButtonColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.bButtonColor, "B Button Color", kBButtonColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.xButtonColor, "X Button Color", kXButtonColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.yButtonColor, "Y Button Color", kYButtonColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.zButtonColor, "Z Button Color", kZButtonColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.heartColor, "Heart Color", kHeartColorsExplanation);
|
||||
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
ModResult build_misc_colors_tab(
|
||||
ModContext*, UiWindowHandle, UiElementHandle left, UiElementHandle right, void*, ModError*) {
|
||||
(void)right;
|
||||
|
||||
add_midna_hair_option(left, g_cvars.midnaHairBaseColor, "Midna's Hair Base Color");
|
||||
add_midna_hair_option(left, g_cvars.midnaHairTipsColor, "Midna's Hair Tips Color");
|
||||
add_cosmetic_option(left, g_cvars.midnaChargeRingColor, "Midna Charge Ring Color", kChargeRingColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.linkHairColor, "Link's Hair Color", kLinksHairColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.wolfLinkColor, "Wolf Link Color", kWolfLinkColorsExplanation);
|
||||
add_cosmetic_option(left, g_cvars.eponaColor, "Epona Color", kEponaColorsExplanation);
|
||||
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
void on_cosmetics_menu_window_closed(ModContext*, UiWindowHandle, void*) {
|
||||
g_cosmeticsWindow = 0;
|
||||
}
|
||||
|
||||
void on_open_cosmetics_menu(ModContext*, void*) {
|
||||
if (g_cosmeticsWindow != 0) {
|
||||
return;
|
||||
}
|
||||
UiTabDesc tabs[] = {UI_TAB_DESC_INIT, UI_TAB_DESC_INIT, UI_TAB_DESC_INIT};
|
||||
tabs[0].title = "Equipment Colors";
|
||||
tabs[0].build = build_equipment_colors_tab;
|
||||
tabs[1].title = "UI Colors";
|
||||
tabs[1].build = build_ui_colors_tab;
|
||||
tabs[2].title = "Misc Colors";
|
||||
tabs[2].build = build_misc_colors_tab;
|
||||
UiWindowDesc desc = UI_WINDOW_DESC_INIT;
|
||||
desc.tabs = tabs;
|
||||
desc.tab_count = ARRAY_SIZE(tabs);
|
||||
desc.on_closed = on_cosmetics_menu_window_closed;
|
||||
if (svc_ui->window_push(mod_ctx, &desc, &g_cosmeticsWindow) != MOD_OK) {
|
||||
svc_log->error(mod_ctx, "failed to open basic cosmetics window");
|
||||
}
|
||||
}
|
||||
|
||||
ModResult build_panel(ModContext*, UiElementHandle panel, void*, ModError*) {
|
||||
UiControlDesc control = UI_CONTROL_DESC_INIT;
|
||||
control.kind = UI_CONTROL_BUTTON;
|
||||
control.label = "Open Cosmetics Menu";
|
||||
control.on_pressed = on_open_cosmetics_menu;
|
||||
add_control(panel, control);
|
||||
return MOD_OK;
|
||||
}
|
||||
}
|
||||
|
||||
#define REGISTER_COSMETIC_OPTION(option) \
|
||||
result = register_str_option(#option, NULL, g_cvars.option, error); \
|
||||
if (result != MOD_OK) { \
|
||||
return result; \
|
||||
} \
|
||||
|
||||
extern "C" {
|
||||
MOD_EXPORT ModResult mod_initialize(ModError* error) {
|
||||
svc_log->info(mod_ctx, "basic_cosmetics_mod initialized");
|
||||
|
||||
ModResult result{};
|
||||
|
||||
REGISTER_COSMETIC_OPTION(herosTunicCapColor)
|
||||
REGISTER_COSMETIC_OPTION(herosTunicTorsoColor)
|
||||
REGISTER_COSMETIC_OPTION(herosTunicSkirtColor)
|
||||
REGISTER_COSMETIC_OPTION(zoraArmorCapColor)
|
||||
REGISTER_COSMETIC_OPTION(zoraArmorHelmetColor)
|
||||
REGISTER_COSMETIC_OPTION(zoraArmorTorsoColor)
|
||||
REGISTER_COSMETIC_OPTION(zoraArmorScalesColor)
|
||||
REGISTER_COSMETIC_OPTION(zoraArmorFlippersColor)
|
||||
REGISTER_COSMETIC_OPTION(lanternGlowColor)
|
||||
REGISTER_COSMETIC_OPTION(woodenSwordColor)
|
||||
REGISTER_COSMETIC_OPTION(ordonSwordBladeColor)
|
||||
REGISTER_COSMETIC_OPTION(ordonSwordHandleColor)
|
||||
REGISTER_COSMETIC_OPTION(msBladeColor)
|
||||
REGISTER_COSMETIC_OPTION(msHandleColor)
|
||||
REGISTER_COSMETIC_OPTION(lightSwordGlowColor)
|
||||
REGISTER_COSMETIC_OPTION(boomerangColor)
|
||||
REGISTER_COSMETIC_OPTION(ironBootsColor)
|
||||
REGISTER_COSMETIC_OPTION(spinnerColor)
|
||||
REGISTER_COSMETIC_OPTION(aButtonColor)
|
||||
REGISTER_COSMETIC_OPTION(bButtonColor)
|
||||
REGISTER_COSMETIC_OPTION(xButtonColor)
|
||||
REGISTER_COSMETIC_OPTION(yButtonColor)
|
||||
REGISTER_COSMETIC_OPTION(zButtonColor)
|
||||
REGISTER_COSMETIC_OPTION(heartColor)
|
||||
|
||||
result = register_int_option("midnaHairBaseColor", 0, g_cvars.midnaHairBaseColor, error);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
result = register_int_option("midnaHairTipsColor", 0, g_cvars.midnaHairTipsColor, error);
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
REGISTER_COSMETIC_OPTION(midnaChargeRingColor)
|
||||
REGISTER_COSMETIC_OPTION(linkHairColor)
|
||||
REGISTER_COSMETIC_OPTION(wolfLinkColor)
|
||||
REGISTER_COSMETIC_OPTION(eponaColor)
|
||||
|
||||
UiModsPanelDesc panelDesc = UI_MODS_PANEL_DESC_INIT;
|
||||
panelDesc.build = build_panel;
|
||||
svc_ui->register_mods_panel(mod_ctx, &panelDesc);
|
||||
|
||||
// Add all our hooks
|
||||
result = add_all_hooks();
|
||||
if (result != MOD_OK) {
|
||||
return result;
|
||||
}
|
||||
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_update(ModError*) {
|
||||
update_rainbow_rgb(1.0f);
|
||||
set_all_midna_hair_colors();
|
||||
return MOD_OK;
|
||||
}
|
||||
|
||||
MOD_EXPORT ModResult mod_shutdown(ModError*) {
|
||||
svc_log->info(mod_ctx, "basic_cosmetics_mod unloaded");
|
||||
g_cosmeticsWindow = 0;
|
||||
return MOD_OK;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "mods/svc/config.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
struct cvars {
|
||||
ConfigVarHandle herosTunicCapColor = 0;
|
||||
ConfigVarHandle herosTunicTorsoColor = 0;
|
||||
ConfigVarHandle herosTunicSkirtColor = 0;
|
||||
ConfigVarHandle zoraArmorCapColor = 0;
|
||||
ConfigVarHandle zoraArmorHelmetColor = 0;
|
||||
ConfigVarHandle zoraArmorTorsoColor = 0;
|
||||
ConfigVarHandle zoraArmorScalesColor = 0;
|
||||
ConfigVarHandle zoraArmorFlippersColor = 0;
|
||||
ConfigVarHandle lanternGlowColor = 0;
|
||||
ConfigVarHandle woodenSwordColor = 0;
|
||||
ConfigVarHandle ordonSwordBladeColor = 0;
|
||||
ConfigVarHandle ordonSwordHandleColor = 0;
|
||||
ConfigVarHandle msBladeColor = 0;
|
||||
ConfigVarHandle msHandleColor = 0;
|
||||
ConfigVarHandle lightSwordGlowColor = 0;
|
||||
ConfigVarHandle boomerangColor = 0;
|
||||
ConfigVarHandle ironBootsColor = 0;
|
||||
ConfigVarHandle spinnerColor = 0;
|
||||
ConfigVarHandle heartColor = 0;
|
||||
ConfigVarHandle aButtonColor = 0;
|
||||
ConfigVarHandle bButtonColor = 0;
|
||||
ConfigVarHandle xButtonColor = 0;
|
||||
ConfigVarHandle yButtonColor = 0;
|
||||
ConfigVarHandle zButtonColor = 0;
|
||||
ConfigVarHandle midnaHairBaseColor = 0;
|
||||
ConfigVarHandle midnaHairTipsColor = 0;
|
||||
ConfigVarHandle midnaChargeRingColor = 0;
|
||||
ConfigVarHandle linkHairColor = 0;
|
||||
ConfigVarHandle wolfLinkColor = 0;
|
||||
ConfigVarHandle eponaColor = 0;
|
||||
};
|
||||
|
||||
cvars& get_cvars();
|
||||
|
||||
std::string get_str_option(ConfigVarHandle handle, const std::string& fallback);
|
||||
|
||||
int64_t get_int_option(ConfigVarHandle handle, int64_t fallback);
|
||||
@@ -0,0 +1,354 @@
|
||||
#pragma once
|
||||
|
||||
constexpr const char* kLinksCapColorsExplanation =
|
||||
"Set the color of the Hero's Tunic cap with any 6 digit hex code. Loading a new area, changing forms, or changing tunics is required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ab706e<br/>"
|
||||
"- Blue: 6382a0<br/>"
|
||||
"- Purple: 94749a<br/>"
|
||||
"- Orange: ec8644<br/>"
|
||||
"- Yellow: b9ab00<br/>"
|
||||
"- Pink: ec9fc8<br/>"
|
||||
"- Black: 505154<br/>"
|
||||
"- White: f8f7f4<br/>"
|
||||
"- Brown: 91723e<br/>";
|
||||
|
||||
constexpr const char* kLinksShirtColorsExplanation =
|
||||
"Set the color of the Hero's Tunic shirt with any 6 digit hex code. Loading a new area, changing forms, or changing tunics is required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ab706e<br/>"
|
||||
"- Blue: 6382a0<br/>"
|
||||
"- Purple: 94749a<br/>"
|
||||
"- Orange: ec8644<br/>"
|
||||
"- Yellow: b9ab00<br/>"
|
||||
"- Pink: ec9fc8<br/>"
|
||||
"- Black: 505154<br/>"
|
||||
"- White: f8f7f4<br/>"
|
||||
"- Brown: 91723e<br/>";
|
||||
|
||||
constexpr const char* kLinksSkirtColorsExplanation =
|
||||
"Set the color of the Hero's Tunic skirt with any 6 digit hex code. Loading a new area, changing forms, or changing tunics is required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ab706e<br/>"
|
||||
"- Blue: 6382a0<br/>"
|
||||
"- Purple: 94749a<br/>"
|
||||
"- Orange: ec8644<br/>"
|
||||
"- Yellow: b9ab00<br/>"
|
||||
"- Pink: ec9fc8<br/>"
|
||||
"- Black: 505154<br/>"
|
||||
"- White: f8f7f4<br/>"
|
||||
"- Brown: 91723e<br/>";
|
||||
|
||||
constexpr const char* kZoraCapColorsExplanation =
|
||||
"Set the color of the Zora Armor cap with any 6 digit hex code. Loading a new area, changing forms, or changing tunics is required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ab706e<br/>"
|
||||
"- Blue: 6382a0<br/>"
|
||||
"- Purple: 94749a<br/>"
|
||||
"- Orange: ec8644<br/>"
|
||||
"- Yellow: b9ab00<br/>"
|
||||
"- Pink: ec9fc8<br/>"
|
||||
"- Black: 505154<br/>"
|
||||
"- White: f8f7f4<br/>"
|
||||
"- Brown: 91723e<br/>";
|
||||
|
||||
constexpr const char* kZoraHelmetColorsExplanation =
|
||||
"Set the color of the Zora Armor helmet with any 6 digit hex code. Loading a new area, changing forms, or changing tunics is required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ab706e<br/>"
|
||||
"- Blue: 6382a0<br/>"
|
||||
"- Purple: 94749a<br/>"
|
||||
"- Orange: ec8644<br/>"
|
||||
"- Yellow: b9ab00<br/>"
|
||||
"- Pink: ec9fc8<br/>"
|
||||
"- Black: 505154<br/>"
|
||||
"- White: f8f7f4<br/>"
|
||||
"- Brown: 91723e<br/>";
|
||||
|
||||
constexpr const char* kZoraTorsoColorsExplanation =
|
||||
"Set the color of the Zora Armor torso with any 6 digit hex code. Loading a new area, changing forms, or changing tunics is required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ab706e<br/>"
|
||||
"- Blue: 6382a0<br/>"
|
||||
"- Purple: 94749a<br/>"
|
||||
"- Orange: ec8644<br/>"
|
||||
"- Yellow: b9ab00<br/>"
|
||||
"- Pink: ec9fc8<br/>"
|
||||
"- Black: 505154<br/>"
|
||||
"- White: f8f7f4<br/>"
|
||||
"- Brown: 91723e<br/>";
|
||||
|
||||
constexpr const char* kZoraScalesColorsExplanation =
|
||||
"Set the color of the Zora Armor scales with any 6 digit hex code. Loading a new area, changing forms, or changing tunics is required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ab706e<br/>"
|
||||
"- Blue: 6382a0<br/>"
|
||||
"- Purple: 94749a<br/>"
|
||||
"- Orange: ec8644<br/>"
|
||||
"- Yellow: b9ab00<br/>"
|
||||
"- Pink: ec9fc8<br/>"
|
||||
"- Black: 505154<br/>"
|
||||
"- White: f8f7f4<br/>"
|
||||
"- Brown: 91723e<br/>";
|
||||
|
||||
constexpr const char* kZoraFlippersColorsExplanation =
|
||||
"Set the color of the Zora Armor flippers with any 6 digit hex code. Loading a new area, changing forms, or changing tunics is required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ab706e<br/>"
|
||||
"- Blue: 6382a0<br/>"
|
||||
"- Purple: 94749a<br/>"
|
||||
"- Orange: ec8644<br/>"
|
||||
"- Yellow: b9ab00<br/>"
|
||||
"- Pink: ec9fc8<br/>"
|
||||
"- Black: 505154<br/>"
|
||||
"- White: f8f7f4<br/>"
|
||||
"- Brown: 91723e<br/>";
|
||||
|
||||
constexpr const char* kWoodenSwordColorsExplanation =
|
||||
"Set the color with any 6 digit hex code. Loading a new area, changing forms, or changing tunics is required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ab706e<br/>"
|
||||
"- Blue: 6382a0<br/>"
|
||||
"- Purple: 94749a<br/>"
|
||||
"- Orange: ec8644<br/>"
|
||||
"- Yellow: b9ab00<br/>"
|
||||
"- Pink: ec9fc8<br/>"
|
||||
"- Black: 505154<br/>"
|
||||
"- White: f8f7f4<br/>"
|
||||
"- Brown: 91723e<br/>";
|
||||
|
||||
constexpr const char* kLanternGlowColorsExplanation =
|
||||
"Set the color of the lantern light with any 6 digit hex code, or with \"rainbow\" for a rainbow effect.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ff0000<br/>"
|
||||
"- Orange: f68821<br/>"
|
||||
"- Yellow: f6f321<br/>"
|
||||
"- Green: 00ff00<br/>"
|
||||
"- Blue: 0000ff<br/>"
|
||||
"- Purple: 8000ff<br/>"
|
||||
"- White: a0a0a0<br/>";
|
||||
|
||||
constexpr const char* kOrdonBladeColorsExplanation =
|
||||
"Set the color of the Ordon Sword's blade with any 6 digit hex code. Closing and re-opening Dusklight will be required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ff0000<br/>"
|
||||
"- Orange: f68821<br/>"
|
||||
"- Yellow: f6f321<br/>"
|
||||
"- Green: 00ff00<br/>"
|
||||
"- Blue: 0000ff<br/>"
|
||||
"- Purple: 8000ff<br/>"
|
||||
"- White: a0a0a0<br/>"
|
||||
"- Cyan: 30d0d0<br/>";
|
||||
|
||||
constexpr const char* kOrdonHandleColorsExplanation =
|
||||
"Set the color of the Ordon Sword's handle with any 6 digit hex code. Closing and re-opening Dusklight will be required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ff0000<br/>"
|
||||
"- Orange: f68821<br/>"
|
||||
"- Yellow: f6f321<br/>"
|
||||
"- Green: 00ff00<br/>"
|
||||
"- Blue: 0000ff<br/>"
|
||||
"- Purple: 8000ff<br/>"
|
||||
"- White: a0a0a0<br/>"
|
||||
"- Cyan: 30d0d0<br/>";
|
||||
|
||||
constexpr const char* kMSBladeColorsExplanation =
|
||||
"Set the color of the Master Sword's blade with any 6 digit hex code. Closing and re-opening Dusklight will be required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ff0000<br/>"
|
||||
"- Orange: f68821<br/>"
|
||||
"- Yellow: f6f321<br/>"
|
||||
"- Green: 00ff00<br/>"
|
||||
"- Blue: 0000ff<br/>"
|
||||
"- Purple: 8000ff<br/>"
|
||||
"- White: a0a0a0<br/>"
|
||||
"- Cyan: 30d0d0<br/>";
|
||||
|
||||
constexpr const char* kMSHandleColorsExplanation =
|
||||
"Set the color of the Master Sword's handle with any 6 digit hex code. Closing and re-opening Dusklight will be required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ff0000<br/>"
|
||||
"- Orange: f68821<br/>"
|
||||
"- Yellow: f6f321<br/>"
|
||||
"- Green: 00ff00<br/>"
|
||||
"- Blue: 0000ff<br/>"
|
||||
"- Purple: 8000ff<br/>"
|
||||
"- White: a0a0a0<br/>"
|
||||
"- Cyan: 30d0d0<br/>";
|
||||
|
||||
constexpr const char* kSwordGlowColorsExplanation =
|
||||
"Set the color of the light sword glow with any 6 digit hex code, or with \"rainbow\" for a rainbow effect.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ff0000<br/>"
|
||||
"- Orange: f68821<br/>"
|
||||
"- Yellow: f6f321<br/>"
|
||||
"- Green: 00ff00<br/>"
|
||||
"- Blue: 0000ff<br/>"
|
||||
"- Purple: 8000ff<br/>"
|
||||
"- White: a0a0a0<br/>";
|
||||
|
||||
constexpr const char* kBoomerangColorsExplanation =
|
||||
"Set the color of the Gale Boomerang with any 6 digit hex code. Closing and re-opening Dusklight will be required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ab706e<br/>"
|
||||
"- Blue: 6382a0<br/>"
|
||||
"- Purple: 94749a<br/>"
|
||||
"- Orange: ec8644<br/>"
|
||||
"- Yellow: b9ab00<br/>"
|
||||
"- Pink: ec9fc8<br/>"
|
||||
"- Black: 505154<br/>"
|
||||
"- White: f8f7f4<br/>"
|
||||
"- Brown: 91723e<br/>";
|
||||
|
||||
constexpr const char* kIronBootsColorsExplanation =
|
||||
"Set the color of the Iron Boots with any 6 digit hex code. Closing and re-opening Dusklight will be required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ab706e<br/>"
|
||||
"- Blue: 6382a0<br/>"
|
||||
"- Purple: 94749a<br/>"
|
||||
"- Orange: ec8644<br/>"
|
||||
"- Yellow: b9ab00<br/>"
|
||||
"- Pink: ec9fc8<br/>"
|
||||
"- Black: 505154<br/>"
|
||||
"- White: f8f7f4<br/>"
|
||||
"- Brown: 91723e<br/>";
|
||||
|
||||
constexpr const char* kSpinnerColorsExplanation =
|
||||
"Set the color of the Spinner with any 6 digit hex code. Closing and re-opening Dusklight will be required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ab706e<br/>"
|
||||
"- Blue: 6382a0<br/>"
|
||||
"- Purple: 94749a<br/>"
|
||||
"- Orange: ec8644<br/>"
|
||||
"- Yellow: b9ab00<br/>"
|
||||
"- Pink: ec9fc8<br/>"
|
||||
"- Black: 505154<br/>"
|
||||
"- White: f8f7f4<br/>"
|
||||
"- Brown: 91723e<br/>";
|
||||
|
||||
|
||||
constexpr const char* kAButtonColorsExplanation =
|
||||
"Set the color of the A button with any 6 digit hex code. A reload may be necessary to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ff0000<br/>"
|
||||
"- Orange: ff5000<br/>"
|
||||
"- Yellow: ffaf00<br/>"
|
||||
"- Dark Green: 0080ff<br/>"
|
||||
"- Blue: 0000ff<br/>"
|
||||
"- Purple: 8000ff<br/>"
|
||||
"- Grey: 5555ff<br/>"
|
||||
"- Pink: ff20ff<br/>";
|
||||
|
||||
constexpr const char* kBButtonColorsExplanation =
|
||||
"Set the color of the B button with any 6 digit hex code. A reload may be necessary to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Orange: ffff40<br/>"
|
||||
"- Pink: ffa0ff<br/>"
|
||||
"- Green: 00e87b<br/>"
|
||||
"- Blue: 00aaff<br/>"
|
||||
"- Purple: 6078ff<br/>"
|
||||
"- Black: 000000<br/>"
|
||||
"- Teal: 00f3ff<br/>";
|
||||
|
||||
constexpr const char* kXButtonColorsExplanation =
|
||||
"Set the color of the X button with any 6 digit hex code. A reload may be necessary to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ff0000<br/>"
|
||||
"- Orange: ff8200<br/>"
|
||||
"- Yellow: f7df00<br/>"
|
||||
"- Lime Green: 70ff00<br/>"
|
||||
"- Dark Green: 00bd11<br/>"
|
||||
"- Blue: 0000ff<br/>"
|
||||
"- Purple: 800088<br/>"
|
||||
"- Black: 000000<br/>"
|
||||
"- Pink: ff00aa<br/>"
|
||||
"- Cyan: 00ffff<br/>";
|
||||
|
||||
constexpr const char* kYButtonColorsExplanation =
|
||||
"Set the color of the Y button with any 6 digit hex code. A reload may be necessary to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ff0000<br/>"
|
||||
"- Orange: ff8200<br/>"
|
||||
"- Yellow: f7df00<br/>"
|
||||
"- Lime Green: 70ff00<br/>"
|
||||
"- Dark Green: 00bd11<br/>"
|
||||
"- Blue: 0000ff<br/>"
|
||||
"- Purple: 800088<br/>"
|
||||
"- Black: 000000<br/>"
|
||||
"- Pink: ff00aa<br/>"
|
||||
"- Cyan: 00ffff<br/>";
|
||||
|
||||
constexpr const char* kZButtonColorsExplanation =
|
||||
"Set the Z button color with any 6 digit hex code. A reload may be necessary to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ff0000<br/>"
|
||||
"- Orange: ff8200<br/>"
|
||||
"- Yellow: f7df00<br/>"
|
||||
"- Lime Green: 70ff00<br/>"
|
||||
"- Dark Green: 00bd11<br/>"
|
||||
"- Purple: 800088<br/>"
|
||||
"- Black: 000000<br/>"
|
||||
"- Light Blue: 00ffff<br/>";
|
||||
|
||||
constexpr const char* kHeartColorsExplanation =
|
||||
"Set the heart color with any 6 digit hex code. This will also change the color of heart drops. A reload may be necessary to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Orange: ffff40<br/>"
|
||||
"- Pink: ffa0ff<br/>"
|
||||
"- Green: 00e87b<br/>"
|
||||
"- Blue: 00aaff<br/>"
|
||||
"- Purple: 6078ff<br/>"
|
||||
"- Black: 000000<br/>"
|
||||
"- Teal: 00f3ff<br/>";
|
||||
|
||||
constexpr const char* kChargeRingColorsExplanation =
|
||||
"Set the color of Midna's charge ring with any 6 digit hex code.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Pink: ff9f9f<br/>"
|
||||
"- Red: ff0000<br/>"
|
||||
"- Yellow: ffff00<br/>"
|
||||
"- Green: 00ff00<br/>"
|
||||
"- Blue: 0000ff<br/>"
|
||||
"- Purple: ff00ff<br/>"
|
||||
"- Brown: 331900<br/>"
|
||||
"- White: feffff<br/>"
|
||||
"- Black: 000000<br/>";
|
||||
|
||||
constexpr const char* kLinksHairColorsExplanation =
|
||||
"Set the color of Link's hair with any 6 digit hex code. Loading a new area, changing forms, or changing tunics is required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ab706e<br/>"
|
||||
"- Blue: 6382a0<br/>"
|
||||
"- Purple: 94749a<br/>"
|
||||
"- Orange: ec8644<br/>"
|
||||
"- Yellow: b9ab00<br/>"
|
||||
"- Pink: ec9fc8<br/>"
|
||||
"- Black: 505154<br/>"
|
||||
"- White: f8f7f4<br/>"
|
||||
"- Brown: 91723e<br/>";
|
||||
|
||||
constexpr const char* kWolfLinkColorsExplanation =
|
||||
"Set the color of Wolf Link with any 6 digit hex code. Loading a new area or changing forms is required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ab706e<br/>"
|
||||
"- Blue: 6382a0<br/>"
|
||||
"- Purple: 94749a<br/>"
|
||||
"- Orange: ec8644<br/>"
|
||||
"- Yellow: b9ab00<br/>"
|
||||
"- Pink: ec9fc8<br/>"
|
||||
"- Black: 505154<br/>"
|
||||
"- White: f8f7f4<br/>"
|
||||
"- Brown: 91723e<br/>";
|
||||
|
||||
constexpr const char* kEponaColorsExplanation =
|
||||
"Set the color of Wolf Link with any 6 digit hex code. Loading a new area while *not* riding Epona is required to see changes.<br/>"
|
||||
"Common colors:<br/>"
|
||||
"- Red: ab706e<br/>"
|
||||
"- Blue: 6382a0<br/>"
|
||||
"- Purple: 94749a<br/>"
|
||||
"- Orange: ec8644<br/>"
|
||||
"- Yellow: b9ab00<br/>"
|
||||
"- Pink: ec9fc8<br/>"
|
||||
"- Black: 505154<br/>"
|
||||
"- White: f8f7f4<br/>"
|
||||
"- Brown: 91723e<br/>";
|
||||
@@ -0,0 +1,549 @@
|
||||
#include "texture_utils.hpp"
|
||||
#include "color_utils.hpp"
|
||||
#include "mod.hpp"
|
||||
|
||||
#include "mods/svc/config.h"
|
||||
#include "mods/svc/log.hpp"
|
||||
|
||||
// Forward declaration needed for J3DModelLoader to be happy
|
||||
class J3DVertexData;
|
||||
#include "JSystem/J3DGraphLoader/J3DModelLoader.h"
|
||||
#include "JSystem/JKernel/JKRMemArchive.h"
|
||||
#include "JSystem/JSupport/JSupport.h"
|
||||
#include "JSystem/JUtility/JUTNameTab.h"
|
||||
#include "JSystem/JUtility/JUTTexture.h"
|
||||
#include "d/actor/d_a_alink.h"
|
||||
#include "global.h"
|
||||
#include "gx/GXEnum.h"
|
||||
#include "m_Do/m_Do_dvd_thread.h"
|
||||
|
||||
#include <list>
|
||||
|
||||
ResTIMG* find_tex_header_in_tex_1_section(J3DTextureBlock* tex1Ptr, const char* textureName) {
|
||||
if (tex1Ptr == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto strTable = JSUConvertOffsetToPtr<ResNTAB>(tex1Ptr, tex1Ptr->mpNameTable);
|
||||
for (size_t i = 0; i < strTable->mEntryNum && i < tex1Ptr->mTextureNum; i++) {
|
||||
const char* str = strTable->getName(i);
|
||||
|
||||
if (strcmp(str, textureName) == 0) {
|
||||
return &JSUConvertOffsetToPtr<ResTIMG>(tex1Ptr, tex1Ptr->mpTextureRes)[i];
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void recolor_rgb5a3_texture(ResTIMG* texHeaderPtr, GXColor color)
|
||||
{
|
||||
// Precompute lookup tables for both RGB555 (opaque) and RGB444 (translucent) modes
|
||||
uint16_t recolors_rgb555[0x100];
|
||||
uint16_t recolors_rgb444[0x100];
|
||||
|
||||
for (int32_t i = 0; i < 0x100; i++) {
|
||||
const uint8_t r = blend_overlay_channel(i, color.r);
|
||||
const uint8_t g = blend_overlay_channel(i, color.g);
|
||||
const uint8_t b = blend_overlay_channel(i, color.b);
|
||||
|
||||
// Pack as RGB555: Bit 15 set to 1 + 5 bits R, G, B
|
||||
recolors_rgb555[i] = 0x8000 | ((r >> 3) << 10) | ((g >> 3) << 5) | (b >> 3);
|
||||
|
||||
// Pack as RGB444: 4 bits R, G, B (Bit 15 remains 0)
|
||||
recolors_rgb444[i] = ((r >> 4) << 8) | ((g >> 4) << 4) | (b >> 4);
|
||||
}
|
||||
|
||||
constexpr int32_t blockWidth = 4;
|
||||
constexpr int32_t blockHeight = 4;
|
||||
|
||||
const int32_t roundedWidth = texHeaderPtr->width + ((blockWidth - (texHeaderPtr->width % blockWidth)) % blockWidth);
|
||||
const int32_t roundedHeight = texHeaderPtr->height + ((blockHeight - (texHeaderPtr->height % blockHeight)) % blockHeight);
|
||||
|
||||
const int32_t totalPixels = roundedWidth * roundedHeight;
|
||||
|
||||
auto* pixelPtr = reinterpret_cast<BE<uint16_t>*>(JSUConvertOffsetToPtr<u8>(texHeaderPtr, texHeaderPtr->imageOffset));
|
||||
|
||||
for (int32_t i = 0; i < totalPixels; i++) {
|
||||
const uint16_t rawPixel = pixelPtr[i];
|
||||
|
||||
// MSB determines if pixel is opaque or translucent
|
||||
if (rawPixel & 0x8000) {
|
||||
// Pixel is opaque
|
||||
const uint8_t r5 = (rawPixel >> 10) & 0x1F;
|
||||
const uint8_t g5 = (rawPixel >> 5) & 0x1F;
|
||||
const uint8_t b5 = rawPixel & 0x1F;
|
||||
|
||||
// Expand 5-bit to 8-bit
|
||||
const uint8_t r8 = (r5 << 3) | (r5 >> 2);
|
||||
const uint8_t g8 = (g5 << 3) | (g5 >> 2);
|
||||
const uint8_t b8 = (b5 << 3) | (b5 >> 2);
|
||||
|
||||
const uint8_t grayVal = static_cast<uint8_t>((r8 * 77 + g8 * 150 + b8 * 29) >> 8);
|
||||
|
||||
pixelPtr[i] = recolors_rgb555[grayVal];
|
||||
} else {
|
||||
// Pixel is translucent
|
||||
const uint16_t alpha3 = rawPixel & 0x7000;
|
||||
|
||||
const uint8_t r4 = (rawPixel >> 8) & 0x0F;
|
||||
const uint8_t g4 = (rawPixel >> 4) & 0x0F;
|
||||
const uint8_t b4 = rawPixel & 0x0F;
|
||||
|
||||
// Expand 4-bit to 8-bit
|
||||
const uint8_t r8 = (r4 << 4) | r4;
|
||||
const uint8_t g8 = (g4 << 4) | g4;
|
||||
const uint8_t b8 = (b4 << 4) | b4;
|
||||
|
||||
const uint8_t grayVal = static_cast<uint8_t>((r8 * 77 + g8 * 150 + b8 * 29) >> 8);
|
||||
|
||||
// Combine original alpha with recolored RGB444
|
||||
pixelPtr[i] = alpha3 | recolors_rgb444[grayVal];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When left is greater than right
|
||||
// 0b00 points to the left color
|
||||
// 0b01 points to the right color
|
||||
// 0b10 is closer to left color
|
||||
// 0b11 is closer to right color
|
||||
|
||||
// When left is not greater than right
|
||||
// 0b00 points to the left color
|
||||
// 0b01 points to the right color
|
||||
// 0b10 is midway between the colors
|
||||
// 0b11 is transparent
|
||||
|
||||
// That means when maintaining the relative order, if we have to swap the colors:
|
||||
|
||||
// in the case of left being greater than right:
|
||||
// 0b00 will swap to 0b01
|
||||
// 0b01 will swap to 0b00
|
||||
// 0b10 will swap to 0b11
|
||||
// 0b11 will swap to 0b10
|
||||
// So the left bit stays the same, and the right bit changes
|
||||
// Can do xor (^) like 0b01010101 or 0x55 for each u16
|
||||
|
||||
// in the case of left not being greater than right:
|
||||
// 0b00 will swap to 0b01
|
||||
// 0b01 will swap to 0b00
|
||||
// 0b10 will stay the same
|
||||
// 0b11 will stay the same
|
||||
// so if the left bit is a 0, the right bit will change
|
||||
uint32_t swap_index_bits(bool leftIsGreater, uint32_t bits) {
|
||||
if (leftIsGreater) {
|
||||
return bits ^ 0x55555555;
|
||||
}
|
||||
|
||||
const uint32_t mask = ((bits >> 1) & 0x55555555) ^ 0x55555555;
|
||||
return bits ^ mask;
|
||||
}
|
||||
|
||||
void recolor_cmpr_texture(ResTIMG* texHeaderPtr, GXColor color)
|
||||
{
|
||||
uint16_t recolors[0x100];
|
||||
for (int32_t i = 0; i < 0x100; i++) {
|
||||
recolors[i] = blend_overlay_rgb_565(i, color);
|
||||
}
|
||||
|
||||
constexpr int32_t blockWidth = 8;
|
||||
constexpr int32_t blockHeight = 8;
|
||||
|
||||
const int32_t roundedWidth = texHeaderPtr->width + ((blockWidth - (texHeaderPtr->width % blockWidth)) % blockWidth);
|
||||
const int32_t roundedHeight = texHeaderPtr->height + ((blockHeight - (texHeaderPtr->height % blockHeight)) % blockHeight);
|
||||
|
||||
const int32_t numBlocks = roundedWidth / blockWidth * roundedHeight / blockHeight;
|
||||
|
||||
const int32_t iterations = numBlocks * 4;
|
||||
|
||||
uint8_t* currentAddr = JSUConvertOffsetToPtr<u8>(texHeaderPtr, texHeaderPtr->imageOffset);
|
||||
for (int32_t i = 0; i < iterations; i++) {
|
||||
auto* rgb565Ptr = reinterpret_cast<BE<uint16_t>*>(currentAddr);
|
||||
|
||||
auto leftRgb565 = rgb565Ptr[0];
|
||||
auto rightRgb565 = rgb565Ptr[1];
|
||||
const bool leftIsGreater = leftRgb565 > rightRgb565;
|
||||
|
||||
const uint32_t leftGrayVal = desaturate_rgb_565(leftRgb565);
|
||||
const uint32_t rightGrayVal = desaturate_rgb_565(rightRgb565);
|
||||
|
||||
uint16_t leftNewRgb565 = recolors[leftGrayVal];
|
||||
uint16_t rightNewRgb565 = recolors[rightGrayVal];
|
||||
|
||||
bool needsBitSwap = false;
|
||||
|
||||
if (leftIsGreater) {
|
||||
if (leftNewRgb565 == rightNewRgb565) {
|
||||
// Need to make sure that subtracting 1 does not mess
|
||||
// everything up. For example, 0x1000 - 1 => 0x0fff which is
|
||||
// a completely different color.
|
||||
if ((leftNewRgb565 & 0x1f) == 0)
|
||||
{
|
||||
// If left value has 0 blue, we change its blue to 1.
|
||||
leftNewRgb565 += 1;
|
||||
}
|
||||
rightNewRgb565 = leftNewRgb565 - 1;
|
||||
}
|
||||
else if (leftNewRgb565 < rightNewRgb565) {
|
||||
needsBitSwap = true;
|
||||
}
|
||||
}
|
||||
else if (leftNewRgb565 > rightNewRgb565) {
|
||||
needsBitSwap = true;
|
||||
}
|
||||
|
||||
if (needsBitSwap) {
|
||||
// The left and right colors are swapping so that their values
|
||||
// are relative in the same way. We need to update the bits
|
||||
// referencing the palette entries to handle the swap.
|
||||
|
||||
const uint16_t temp = leftNewRgb565;
|
||||
leftNewRgb565 = rightNewRgb565;
|
||||
rightNewRgb565 = temp;
|
||||
|
||||
auto wordPtr = reinterpret_cast<BE<uint32_t>*>(currentAddr);
|
||||
const uint32_t bits = wordPtr[1];
|
||||
|
||||
const uint32_t newBits = swap_index_bits(leftIsGreater, bits);
|
||||
wordPtr[1] = newBits;
|
||||
}
|
||||
|
||||
rgb565Ptr[0] = leftNewRgb565;
|
||||
rgb565Ptr[1] = rightNewRgb565;
|
||||
|
||||
currentAddr += 8;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to encode a single 4x4 sub-block (16 pixels) into an 8-byte CMPR block
|
||||
static void encode_cmpr_sub_block(uint8_t* dst, const uint8_t pixels[16]) {
|
||||
uint8_t min_val = 255;
|
||||
uint8_t max_val = 0;
|
||||
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
if (pixels[i] < min_val) min_val = pixels[i];
|
||||
if (pixels[i] > max_val) max_val = pixels[i];
|
||||
}
|
||||
|
||||
auto intensity_to_rgb565 = [](uint8_t val) -> uint16_t {
|
||||
uint16_t r5 = val >> 3;
|
||||
uint16_t g6 = val >> 2;
|
||||
uint16_t b5 = val >> 3;
|
||||
return static_cast<uint16_t>((r5 << 11) | (g6 << 5) | b5);
|
||||
};
|
||||
|
||||
uint16_t c0_565 = intensity_to_rgb565(max_val);
|
||||
uint16_t c1_565 = intensity_to_rgb565(min_val);
|
||||
uint32_t indices = 0;
|
||||
|
||||
if (max_val > min_val) {
|
||||
// Enforce c0_565 > c1_565 in unsigned 16-bit representation to use 4-color mode
|
||||
if (c0_565 == c1_565) {
|
||||
if ((c0_565 & 0x001F) < 0x001F) {
|
||||
c0_565 += 1;
|
||||
} else {
|
||||
c1_565 -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Interpolated 8-bit intensity values for quantization
|
||||
const int c0 = max_val;
|
||||
const int c1 = min_val;
|
||||
const int c2 = (2 * max_val + min_val) / 3;
|
||||
const int c3 = (max_val + 2 * min_val) / 3;
|
||||
|
||||
// Map each pixel to the nearest palette entry
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
const int p = pixels[i];
|
||||
const int d0 = std::abs(p - c0);
|
||||
const int d1 = std::abs(p - c1);
|
||||
const int d2 = std::abs(p - c2);
|
||||
const int d3 = std::abs(p - c3);
|
||||
|
||||
uint32_t best_idx = 0;
|
||||
int min_d = d0;
|
||||
|
||||
if (d1 < min_d) { min_d = d1; best_idx = 1; }
|
||||
if (d2 < min_d) { min_d = d2; best_idx = 2; }
|
||||
if (d3 < min_d) { min_d = d3; best_idx = 3; }
|
||||
|
||||
indices |= (best_idx << (30 - (2 * i)));
|
||||
}
|
||||
}
|
||||
|
||||
// Account for big endian data expectation
|
||||
dst[0] = static_cast<uint8_t>(c0_565 >> 8);
|
||||
dst[1] = static_cast<uint8_t>(c0_565 & 0xFF);
|
||||
dst[2] = static_cast<uint8_t>(c1_565 >> 8);
|
||||
dst[3] = static_cast<uint8_t>(c1_565 & 0xFF);
|
||||
dst[4] = static_cast<uint8_t>(indices >> 24);
|
||||
dst[5] = static_cast<uint8_t>((indices >> 16) & 0xFF);
|
||||
dst[6] = static_cast<uint8_t>((indices >> 8) & 0xFF);
|
||||
dst[7] = static_cast<uint8_t>(indices & 0xFF);
|
||||
}
|
||||
|
||||
bool convert_i8_to_cmpr(ResTIMG* texHeaderPtr) {
|
||||
|
||||
const uint16_t width = texHeaderPtr->width;
|
||||
const uint16_t height = texHeaderPtr->height;
|
||||
|
||||
if (width % 8 != 0 || height % 8 != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t tilesX = width / 8;
|
||||
const uint32_t blocksY_CMPR = height / 8;
|
||||
|
||||
auto* imageData = JSUConvertOffsetToPtr<uint8_t>(texHeaderPtr, texHeaderPtr->imageOffset);
|
||||
uint8_t* writePtr = imageData;
|
||||
|
||||
// Process image in 8x8 blocks
|
||||
for (uint32_t by = 0; by < blocksY_CMPR; ++by) {
|
||||
for (uint32_t bx = 0; bx < tilesX; ++bx) {
|
||||
// Locate the two stacked 8x4 I8 tiles (32 bytes each) making up the 8x8 block
|
||||
const uint32_t topTileIdx = (2 * by) * tilesX + bx;
|
||||
const uint32_t bottomTileIdx = (2 * by + 1) * tilesX + bx;
|
||||
|
||||
const uint8_t* topTileData = imageData + (topTileIdx * 32);
|
||||
const uint8_t* bottomTileData = imageData + (bottomTileIdx * 32);
|
||||
|
||||
uint8_t subBlockPixels[4][16];
|
||||
|
||||
// Extract Sub-block 0 (Top-Left) and Sub-block 1 (Top-Right)
|
||||
for (int row = 0; row < 4; ++row) {
|
||||
for (int col = 0; col < 4; ++col) {
|
||||
subBlockPixels[0][row * 4 + col] = topTileData[row * 8 + col];
|
||||
subBlockPixels[1][row * 4 + col] = topTileData[row * 8 + col + 4];
|
||||
}
|
||||
}
|
||||
|
||||
// Extract Sub-block 2 (Bottom-Left) and Sub-block 3 (Bottom-Right)
|
||||
for (int row = 0; row < 4; ++row) {
|
||||
for (int col = 0; col < 4; ++col) {
|
||||
subBlockPixels[2][row * 4 + col] = bottomTileData[row * 8 + col];
|
||||
subBlockPixels[3][row * 4 + col] = bottomTileData[row * 8 + col + 4];
|
||||
}
|
||||
}
|
||||
|
||||
// Encode the 4 sub-blocks in CMPR order (32 bytes per 8x8 block)
|
||||
for (const auto& subBlockPixel : subBlockPixels) {
|
||||
encode_cmpr_sub_block(writePtr, subBlockPixel);
|
||||
writePtr += 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update ResTIMG header metadata
|
||||
texHeaderPtr->format = GX_TF_CMPR; // 0x0E
|
||||
texHeaderPtr->colorFormat = 0;
|
||||
texHeaderPtr->numColors = 0;
|
||||
texHeaderPtr->paletteOffset = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void recolor_texture(J3DTextureBlock* tex1Ptr, const char* textureName, GXColor color) {
|
||||
ResTIMG* texHeaderPtr = find_tex_header_in_tex_1_section(tex1Ptr, textureName);
|
||||
if (texHeaderPtr == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (texHeaderPtr->format) {
|
||||
case GX_TF_CMPR:
|
||||
recolor_cmpr_texture(texHeaderPtr, color);
|
||||
break;
|
||||
case GX_TF_RGB5A3:
|
||||
recolor_rgb5a3_texture(texHeaderPtr, color);
|
||||
break;
|
||||
case GX_TF_I8:
|
||||
if (convert_i8_to_cmpr(texHeaderPtr)) {
|
||||
recolor_cmpr_texture(texHeaderPtr, color);
|
||||
} else {
|
||||
mods::log::debug("Could not convert {} from i8 to cmpr", textureName);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
J3DTextureBlock* find_tex_1_in_bmd(J3DModelFileData* bmdPtr)
|
||||
{
|
||||
if (bmdPtr == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (bmdPtr->mMagic1 != MULTI_CHAR('J3D2')) {
|
||||
// Model was not a BMD or BDL!
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (bmdPtr->mMagic2 != MULTI_CHAR('bmd3') && bmdPtr->mMagic2 != MULTI_CHAR('bdl4')) {
|
||||
// Model was not a BMD or BDL!
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
J3DModelBlock* curBlock = bmdPtr->mBlocks;
|
||||
for (int32_t i = 0; i < bmdPtr->mBlockNum; i++) {
|
||||
if (curBlock->mBlockType == MULTI_CHAR('TEX1')) {
|
||||
return static_cast<J3DTextureBlock*>(curBlock);
|
||||
}
|
||||
|
||||
// Line taken from J3DModelLoader.cpp
|
||||
curBlock = (J3DModelBlock*)((uintptr_t)curBlock + curBlock->mBlockSize);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
struct CosmeticOverride {
|
||||
std::list<std::string_view> textures{};
|
||||
ConfigVarHandle hexColor{0};
|
||||
};
|
||||
|
||||
auto& get_cosmetic_overrides() {
|
||||
static std::unordered_map<s32, std::unordered_map<std::string_view, std::list<CosmeticOverride>>> cosmeticOverrides{};
|
||||
if (cosmeticOverrides.empty()) {
|
||||
auto& g_cvars = get_cvars();
|
||||
// Ordon Clothes Link Model
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Bmdl.arc")]["bmwr/al_swb.bmd"] = {
|
||||
{.textures = {"al_SWB"}, .hexColor = g_cvars.woodenSwordColor},
|
||||
};
|
||||
// Main Link Model
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Kmdl.arc")]["bmwr/al_head.bmd"] = {
|
||||
{.textures = {"al_cap"}, .hexColor = g_cvars.herosTunicCapColor},
|
||||
{.textures = {"al_hair"}, .hexColor = g_cvars.linkHairColor},
|
||||
};
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Kmdl.arc")]["bmwr/al.bmd"] = {
|
||||
{.textures = {"al_upbody"}, .hexColor = g_cvars.herosTunicTorsoColor},
|
||||
{.textures = {"al_lowbody"}, .hexColor = g_cvars.herosTunicSkirtColor},
|
||||
};
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Kmdl.arc")]["bmwr/al_bootsh.bmd"] = {
|
||||
{.textures = {"al_bootsH"}, .hexColor = g_cvars.ironBootsColor},
|
||||
};
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Kmdl.arc")]["bmwr/al_swb.bmd"] = {
|
||||
{.textures = {"al_SWB"}, .hexColor = g_cvars.woodenSwordColor},
|
||||
};
|
||||
// Zora Armor Link Model
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Zmdl.arc")]["bmwr/zl_head.bmd"] = {
|
||||
{.textures = {"zl_cap"}, .hexColor = g_cvars.zoraArmorCapColor},
|
||||
{.textures = {"zl_helmet"}, .hexColor = g_cvars.zoraArmorHelmetColor},
|
||||
};
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Zmdl.arc")]["bmwr/zl.bmd"] = {
|
||||
{.textures = {"zl_armor", "zl_armL"}, .hexColor = g_cvars.zoraArmorTorsoColor},
|
||||
{.textures = {"zl_body"}, .hexColor = g_cvars.zoraArmorScalesColor},
|
||||
{.textures = {"zl_boots"}, .hexColor = g_cvars.zoraArmorFlippersColor},
|
||||
};
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Zmdl.arc")]["bmwr/al_bootsh.bmd"] = {
|
||||
{.textures = {"al_bootsH"}, .hexColor = g_cvars.ironBootsColor},
|
||||
};
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Zmdl.arc")]["bmwr/al_swb.bmd"] = {
|
||||
{.textures = {"al_SWB"}, .hexColor = g_cvars.woodenSwordColor},
|
||||
};
|
||||
// Zora Armor field model
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/O_gD_zora.arc")]["bmdr/o_gd_al_zora.bmd"] = {
|
||||
{.textures = {"zl_armor"}, .hexColor = g_cvars.zoraArmorTorsoColor},
|
||||
{.textures = {"zl_body"}, .hexColor = g_cvars.zoraArmorScalesColor},
|
||||
{.textures = {"zl_helmet"}, .hexColor = g_cvars.zoraArmorHelmetColor},
|
||||
{.textures = {"zl_cap"}, .hexColor = g_cvars.zoraArmorCapColor},
|
||||
};
|
||||
// Magic Armor Model
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Mmdl.arc")]["bmwr/al_bootsh.bmd"] = {
|
||||
{.textures = {"al_bootsH"}, .hexColor = g_cvars.ironBootsColor},
|
||||
};
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Mmdl.arc")]["bmwr/al_swb.bmd"] = {
|
||||
{.textures = {"al_SWB"}, .hexColor = g_cvars.woodenSwordColor},
|
||||
};
|
||||
// Ordon Sword Colors
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Alink.arc")]["bmwr/al_swa.bmd"] = {
|
||||
{.textures = {"al_SWA", "hilight01"}, .hexColor = g_cvars.ordonSwordBladeColor},
|
||||
{.textures = {"al_SWgripA"}, .hexColor = g_cvars.ordonSwordHandleColor},
|
||||
};
|
||||
// Master Sword Colors
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Alink.arc")]["bmwe/al_swm.bmd"] = {
|
||||
{.textures = {"al_SWM"}, .hexColor = g_cvars.msBladeColor},
|
||||
{.textures = {"al_SWgripM"}, .hexColor = g_cvars.msHandleColor},
|
||||
};
|
||||
// Boomerang Color
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Alink.arc")]["bmdr/al_boom.bmd"] = {
|
||||
{.textures = {"L_al_boom00"}, .hexColor = g_cvars.boomerangColor},
|
||||
};
|
||||
// Spinner Color
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Alink.arc")]["bmdr/al_sp.bmd"] = {
|
||||
{.textures = {"al_SP"}, .hexColor = g_cvars.spinnerColor},
|
||||
};
|
||||
// Epona Color
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Horse.arc")]["bmdr/hs.bmd"] = {
|
||||
{.textures = {"hs_body", "hs_eye.1", "hs_eye.2", "hs_eye.3"}, .hexColor = g_cvars.eponaColor},
|
||||
};
|
||||
// Wolf Link Color
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/Wmdl.arc")]["bmwr/wl.bmd"] = {
|
||||
{.textures = {"wl_body", "wl_eye.1", "wl_eye.2", "wl_eye.3", "wl_eye.4", "wl_eye.5"}, .hexColor = g_cvars.wolfLinkColor},
|
||||
};
|
||||
// Wooden Sword Item Model
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/O_gD_SWB.arc")]["bmdr/o_gd_al_swb.bmd"] = {
|
||||
{.textures = {"al_SWB"}, .hexColor = g_cvars.woodenSwordColor},
|
||||
};
|
||||
// Boomerang Item Model
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/O_gD_boom.arc")]["bmdr/o_gd_boom.bmd"] = {
|
||||
{.textures = {"L_al_boom00"}, .hexColor = g_cvars.boomerangColor},
|
||||
};
|
||||
// Iron Boots Item Model
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/O_gD_boot.arc")]["bmwr/o_gd_al_bootsh.bmd"] = {
|
||||
{.textures = {"al_bootsH"}, .hexColor = g_cvars.ironBootsColor},
|
||||
};
|
||||
// Spinner Item Model
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/O_gD_SP.arc")]["bmdr/o_gd_al_sp.bmd"] = {
|
||||
{.textures = {"al_SP"}, .hexColor = g_cvars.spinnerColor},
|
||||
};
|
||||
// Gale Boomerang for Ook
|
||||
cosmeticOverrides[DVDConvertPathToEntrynum("/res/Object/E_mk.arc")]["bmdr/bm.bmd"] = {
|
||||
{.textures = {"L_al_boom00", "bm_boom"}, .hexColor = g_cvars.boomerangColor},
|
||||
};
|
||||
}
|
||||
return cosmeticOverrides;
|
||||
}
|
||||
|
||||
s32 get_entry_number(mDoDvdThd_mountArchive_c* mountArchive) {
|
||||
return mountArchive->mEntryNumber;
|
||||
}
|
||||
|
||||
void handle_texture_overrides_on_load(mDoDvdThd_mountArchive_c* mountArchive) {
|
||||
|
||||
auto entryNum = get_entry_number(mountArchive);
|
||||
auto& cosmeticOverrides = get_cosmetic_overrides();
|
||||
if (!cosmeticOverrides.contains(entryNum)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& [resName, overrides] : cosmeticOverrides[entryNum]) {
|
||||
|
||||
auto* archive = mountArchive->getArchive();
|
||||
auto* entry = archive->findFsResource(resName.data(), 0);
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto* tex1Addr = find_tex_1_in_bmd(static_cast<J3DModelFileData*>(archive->fetchResource(entry, NULL)));
|
||||
if (!tex1Addr) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const auto& cosmeticOverride : overrides) {
|
||||
const auto& [textures, hexColorVar] = cosmeticOverride;
|
||||
const auto& hexColorStr = get_str_option(hexColorVar, "");
|
||||
if (!is_valid_hex_color_str(hexColorStr)) {
|
||||
mods::log::debug("Invalid Hex Str {}", hexColorStr);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto color = hex_color_str_to_gx_color(hexColorStr);
|
||||
if (tex1Addr) {
|
||||
for (const auto& textureName : textures) {
|
||||
recolor_texture(tex1Addr, textureName.data(), color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* File originally copied from console TPR with permission from isaac
|
||||
* https://github.com/zsrtp/libtp_rel/blob/master/include/util/texture_utils.h
|
||||
*/
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
struct ResTIMG;
|
||||
struct J3DTextureBlock;
|
||||
class J3DModelFileData;
|
||||
class mDoDvdThd_mountArchive_c;
|
||||
|
||||
ResTIMG* find_tex_header_in_tex_1_section(J3DTextureBlock* tex1Ptr, const char* textureName);
|
||||
|
||||
uint32_t swap_index_bits(bool leftIsGreater, uint32_t bits);
|
||||
|
||||
void recolor_cmpr_texture(J3DTextureBlock* tex1Ptr, const char* textureName, const uint8_t* rgb);
|
||||
|
||||
J3DTextureBlock* find_tex_1_in_bmd(J3DModelFileData* bmdPtr);
|
||||
|
||||
void handle_texture_overrides_on_load(mDoDvdThd_mountArchive_c* mountArchive);
|
||||
+12
-14
@@ -1,6 +1,6 @@
|
||||
# Android Shell
|
||||
|
||||
This directory contains a minimal SDLActivity-based Android app wrapper for Dusklight.
|
||||
This directory contains Dusklight's Android shell built on top of Borealis.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -25,24 +25,19 @@ cmake --build --preset android-arm64
|
||||
|
||||
This build produces `build/android-arm64/libmain.so`
|
||||
|
||||
## Refresh SDL Java Shim (Optional)
|
||||
|
||||
If you update SDL and want to refresh the embedded Java shim files:
|
||||
|
||||
```bash
|
||||
./android/scripts/sync-sdl-java.sh
|
||||
```
|
||||
|
||||
## Build APK
|
||||
|
||||
```bash
|
||||
cd android
|
||||
cd platforms/android
|
||||
./gradlew :app:assembleDebug
|
||||
```
|
||||
|
||||
Output APK:
|
||||
|
||||
- `android/app/build/outputs/apk/debug/app-debug.apk`
|
||||
- `app/build/outputs/apk/debug/app-arm64-v8a-debug.apk`
|
||||
|
||||
Aurora needs a hardware-backed graphics adapter. If an AVD has GPU
|
||||
acceleration disabled, launch it with `-gpu host`.
|
||||
|
||||
## Launch With Runtime Args (adb)
|
||||
|
||||
@@ -50,10 +45,13 @@ You can pass command-line args through the activity intent:
|
||||
|
||||
```bash
|
||||
adb shell am start -n dev.twilitrealm.dusk/.DuskActivity \
|
||||
--es dusk_args "--backend vulkan"
|
||||
--es borealis_args "--backend vulkan"
|
||||
```
|
||||
|
||||
Supported extras:
|
||||
|
||||
- `dusk_args`: single shell-like argument string
|
||||
- `dusk_argv`: string-array argv
|
||||
- `borealis_args`: single shell-like argument string
|
||||
- `borealis_argv`: string-array argv
|
||||
|
||||
The legacy `dusk_args` and `dusk_argv` names remain accepted during the shell
|
||||
transition.
|
||||
|
||||
@@ -2,163 +2,32 @@ plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
def versionNameStr = (System.getenv("DUSK_VERSION") ?: "v0.1.0").replaceFirst("^v", "")
|
||||
def versionCodeInt = (System.getenv("DUSK_VERSION_CODE") ?: "100000").toInteger()
|
||||
|
||||
def duskRepoDir = rootProject.projectDir.parentFile.parentFile
|
||||
def androidNativeBuildDir = new File(duskRepoDir, 'build/android-arm64')
|
||||
def nativeLibrary = new File(androidNativeBuildDir, 'libmain.so')
|
||||
def stageStripValue = providers.gradleProperty('ANDROID_STAGE_STRIP')
|
||||
.orElse(providers.gradleProperty('androidStageStrip'))
|
||||
.orElse(providers.environmentVariable('ANDROID_STAGE_STRIP'))
|
||||
.orElse('1')
|
||||
def androidHome = {
|
||||
def sdkPath = System.getenv('ANDROID_HOME')
|
||||
if (!sdkPath) {
|
||||
throw new GradleException('ANDROID_HOME is not available')
|
||||
}
|
||||
def sdkDir = new File(sdkPath)
|
||||
if (!sdkDir.isDirectory()) {
|
||||
throw new GradleException("ANDROID_HOME points to a missing directory: ${sdkDir}")
|
||||
}
|
||||
sdkDir
|
||||
}.memoize()
|
||||
def borealisDir = new File(duskRepoDir, 'extern/borealis')
|
||||
|
||||
def androidNdkVersion = {
|
||||
def ndkVersion = System.getenv('ANDROID_NDK_VERSION')
|
||||
if (!ndkVersion) {
|
||||
throw new GradleException('ANDROID_NDK_VERSION is not available')
|
||||
}
|
||||
ndkVersion
|
||||
}.memoize()
|
||||
ext.borealisAndroid = [
|
||||
borealisDir: borealisDir,
|
||||
propertiesFile: new File(duskRepoDir, 'build/android-arm64/borealis-android.properties'),
|
||||
namespace: 'dev.twilitrealm.dusk',
|
||||
applicationId: 'dev.twilitrealm.dusk',
|
||||
abis: ['arm64-v8a'],
|
||||
assets: [
|
||||
[
|
||||
from: new File(duskRepoDir, 'res'),
|
||||
into: 'res',
|
||||
excludes: ['**/.DS_Store']
|
||||
],
|
||||
[
|
||||
from: new File(duskRepoDir, 'build/android-arm64/bundled_mods'),
|
||||
into: 'mods',
|
||||
includes: ['*.dusk']
|
||||
]
|
||||
],
|
||||
proguardRules: file('proguard-rules.pro')
|
||||
]
|
||||
|
||||
def androidNdkDir = {
|
||||
def ndkDir = new File(androidHome(), "ndk/${androidNdkVersion()}")
|
||||
if (!new File(ndkDir, 'build/cmake/android.toolchain.cmake').isFile()) {
|
||||
throw new GradleException(
|
||||
"Android NDK ${androidNdkVersion()} is missing or invalid at ${ndkDir}"
|
||||
)
|
||||
}
|
||||
ndkDir
|
||||
}.memoize()
|
||||
|
||||
def llvmPrebuiltDir = {
|
||||
def prebuiltRoot = new File(androidNdkDir(), 'toolchains/llvm/prebuilt')
|
||||
def prebuiltDir = (prebuiltRoot.listFiles()?.findAll { it.isDirectory() } ?: [])
|
||||
.sort { it.name }
|
||||
.find { candidate ->
|
||||
new File(
|
||||
candidate,
|
||||
'sysroot/usr/lib/aarch64-linux-android/libc++_shared.so'
|
||||
).isFile()
|
||||
}
|
||||
if (!prebuiltDir) {
|
||||
throw new GradleException("Cannot find NDK libc++_shared.so under ${prebuiltRoot}")
|
||||
}
|
||||
prebuiltDir
|
||||
}.memoize()
|
||||
|
||||
def stlLibrary = {
|
||||
def library = new File(
|
||||
llvmPrebuiltDir(),
|
||||
'sysroot/usr/lib/aarch64-linux-android/libc++_shared.so'
|
||||
)
|
||||
if (!library.isFile()) {
|
||||
throw new GradleException("libc++_shared.so is missing")
|
||||
}
|
||||
library
|
||||
}.memoize()
|
||||
|
||||
def stagedJniLibsDir = layout.buildDirectory.dir('generated/jniLibs/dusklight')
|
||||
|
||||
def stageJniLibs = tasks.register('stageJniLibs', Sync) {
|
||||
group = 'build'
|
||||
from(nativeLibrary) {
|
||||
rename { 'libmain.so' }
|
||||
into 'arm64-v8a'
|
||||
}
|
||||
from(providers.provider { stlLibrary() }) {
|
||||
rename { 'libc++_shared.so' }
|
||||
into 'arm64-v8a'
|
||||
}
|
||||
into(stagedJniLibsDir)
|
||||
|
||||
doFirst {
|
||||
if (!nativeLibrary.isFile()) {
|
||||
throw new GradleException("Native library is missing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def duskGeneratedAssetsDir = layout.buildDirectory.dir('generated/assets/dusklight')
|
||||
def syncDuskAssets = tasks.register('syncDuskAssets', Sync) {
|
||||
from(new File(duskRepoDir, 'res')) {
|
||||
into 'res'
|
||||
exclude '**/.DS_Store'
|
||||
}
|
||||
from(new File(androidNativeBuildDir, 'bundled_mods')) {
|
||||
into 'mods'
|
||||
include '*.dusk'
|
||||
}
|
||||
into(duskGeneratedAssetsDir)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'dev.twilitrealm.dusk'
|
||||
compileSdk 36
|
||||
ndkVersion androidNdkVersion()
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'dev.twilitrealm.dusk'
|
||||
minSdk 26
|
||||
targetSdk 36
|
||||
versionCode versionCodeInt
|
||||
versionName versionNameStr
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDirs = [stagedJniLibsDir]
|
||||
assets.srcDirs = [duskGeneratedAssetsDir]
|
||||
}
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs {
|
||||
if (stageStripValue.get() == '0') {
|
||||
keepDebugSymbols += '**/libmain.so'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include 'arm64-v8a'
|
||||
universalApk false
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
apply from: new File(borealisDir, 'platforms/android/gradle/borealis-application.gradle')
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
|
||||
tasks.named('preBuild').configure {
|
||||
dependsOn(stageJniLibs, syncDuskAssets)
|
||||
}
|
||||
|
||||
+1
-4
@@ -1,4 +1 @@
|
||||
# Keep SDL activity and related JNI bridge methods.
|
||||
-keep class org.libsdl.app.** { *; }
|
||||
-keep class dev.twilitrealm.dusk.DuskHttpClient { *; }
|
||||
-keep class dev.twilitrealm.dusk.DuskHttpClient$Response { *; }
|
||||
-keep class dev.twilitrealm.dusk.DuskActivity { *; }
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
android:resource="@xml/game_mode_config" />
|
||||
|
||||
<provider
|
||||
android:name="dev.twilitrealm.dusk.DuskDocumentsProvider"
|
||||
android:authorities="dev.twilitrealm.dusk.documents"
|
||||
android:name="dev.encounter.borealis.BorealisDocumentsProvider"
|
||||
android:authorities="${applicationId}.documents"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||
|
||||
@@ -1,102 +1,23 @@
|
||||
package dev.twilitrealm.dusk;
|
||||
|
||||
import android.app.ActionBar;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.ClipData;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.provider.Settings;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowInsetsController;
|
||||
|
||||
import org.libsdl.app.SDLActivity;
|
||||
import org.libsdl.app.SDLSurface;
|
||||
import dev.encounter.borealis.BorealisActivity;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DuskActivity extends SDLActivity {
|
||||
public class DuskActivity extends BorealisActivity {
|
||||
private static final String TAG = "DuskActivity";
|
||||
private static final float DEFAULT_SURFACE_FRAME_RATE = 60.0f;
|
||||
private static final int FOLDER_DIALOG_REQUEST_CODE = 0x4455;
|
||||
private static final int MANAGE_STORAGE_REQUEST_CODE = 0x4456;
|
||||
private static final String EXTERNAL_STORAGE_AUTHORITY =
|
||||
"com.android.externalstorage.documents";
|
||||
|
||||
private long folderDialogUserdata = 0;
|
||||
private boolean awaitingManageStoragePermission = false;
|
||||
|
||||
private static native void nativeFolderDialogResult(long userdata, String path, String error);
|
||||
|
||||
private static String[] splitArgs(String raw) {
|
||||
List<String> out = new ArrayList<>();
|
||||
StringBuilder current = new StringBuilder();
|
||||
boolean inSingle = false;
|
||||
boolean inDouble = false;
|
||||
boolean escaped = false;
|
||||
|
||||
for (int i = 0; i < raw.length(); ++i) {
|
||||
char c = raw.charAt(i);
|
||||
if (escaped) {
|
||||
current.append(c);
|
||||
escaped = false;
|
||||
continue;
|
||||
}
|
||||
if (c == '\\' && !inSingle) {
|
||||
escaped = true;
|
||||
continue;
|
||||
}
|
||||
if (c == '"' && !inSingle) {
|
||||
inDouble = !inDouble;
|
||||
continue;
|
||||
}
|
||||
if (c == '\'' && !inDouble) {
|
||||
inSingle = !inSingle;
|
||||
continue;
|
||||
}
|
||||
if (!inSingle && !inDouble && Character.isWhitespace(c)) {
|
||||
if (current.length() > 0) {
|
||||
out.add(current.toString());
|
||||
current.setLength(0);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
current.append(c);
|
||||
}
|
||||
|
||||
if (escaped) {
|
||||
current.append('\\');
|
||||
}
|
||||
if (current.length() > 0) {
|
||||
out.add(current.toString());
|
||||
}
|
||||
return out.toArray(new String[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
extractBundledMods();
|
||||
super.onCreate(savedInstanceState);
|
||||
hideSystemBars();
|
||||
}
|
||||
|
||||
// Bundled mod packages ship as APK assets, which the native loader cannot read directly;
|
||||
@@ -143,459 +64,23 @@ public class DuskActivity extends SDLActivity {
|
||||
file.delete();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected SDLSurface createSDLSurface(Context context) {
|
||||
return new DuskSurface(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
hideSystemBars();
|
||||
if (awaitingManageStoragePermission) {
|
||||
resumeFolderDialogAfterPermissionGrant();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onWindowFocusChanged(boolean hasFocus) {
|
||||
super.onWindowFocusChanged(hasFocus);
|
||||
if (hasFocus) {
|
||||
hideSystemBars();
|
||||
}
|
||||
}
|
||||
|
||||
private void hideSystemBars() {
|
||||
Window window = getWindow();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.setDecorFitsSystemWindows(false);
|
||||
WindowInsetsController ctrl = window.getDecorView().getWindowInsetsController();
|
||||
if (ctrl != null) {
|
||||
ctrl.setSystemBarsBehavior(
|
||||
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
|
||||
ctrl.hide(WindowInsets.Type.systemBars());
|
||||
}
|
||||
} else {
|
||||
View decorView = window.getDecorView();
|
||||
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
|
||||
decorView.setSystemUiVisibility(uiOptions);
|
||||
ActionBar actionBar = getActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getLibraries() {
|
||||
// SDL3 is statically linked into libmain.so in this build.
|
||||
return new String[] {
|
||||
"main"
|
||||
};
|
||||
}
|
||||
|
||||
public void setPreferredSurfaceFrameRate(float frameRate) {
|
||||
runOnUiThread(() -> {
|
||||
if (mSurface instanceof DuskSurface) {
|
||||
((DuskSurface)mSurface).setPreferredFrameRate(frameRate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static final class DuskSurface extends SDLSurface {
|
||||
private float preferredFrameRate = DEFAULT_SURFACE_FRAME_RATE;
|
||||
|
||||
DuskSurface(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
super.surfaceChanged(holder, format, width, height);
|
||||
setTargetFrameRate(holder);
|
||||
}
|
||||
|
||||
void setPreferredFrameRate(float frameRate) {
|
||||
preferredFrameRate = frameRate;
|
||||
setTargetFrameRate(getHolder());
|
||||
}
|
||||
|
||||
private void setTargetFrameRate(SurfaceHolder holder) {
|
||||
if (!mIsSurfaceReady || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
return;
|
||||
}
|
||||
|
||||
Surface surface = holder != null ? holder.getSurface() : getHolder().getSurface();
|
||||
if (surface == null || !surface.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
float targetFrameRate = getMaxSupportedFrameRate();
|
||||
if (preferredFrameRate > 0.0f) {
|
||||
targetFrameRate = preferredFrameRate;
|
||||
}
|
||||
if (targetFrameRate <= 0.0f) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
surface.setFrameRate(
|
||||
targetFrameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
|
||||
Log.v(TAG, "Requested surface frame rate " + targetFrameRate + " fps");
|
||||
} catch (RuntimeException e) {
|
||||
Log.w(TAG, "Failed to request surface frame rate", e);
|
||||
}
|
||||
}
|
||||
|
||||
private float getMaxSupportedFrameRate() {
|
||||
if (mDisplay == null) {
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
float maxFrameRate = mDisplay.getRefreshRate();
|
||||
Display.Mode[] modes = mDisplay.getSupportedModes();
|
||||
if (modes == null) {
|
||||
return maxFrameRate;
|
||||
}
|
||||
|
||||
for (Display.Mode mode : modes) {
|
||||
maxFrameRate = Math.max(maxFrameRate, mode.getRefreshRate());
|
||||
}
|
||||
return maxFrameRate;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getArguments() {
|
||||
String[] arguments = super.getArguments();
|
||||
if (arguments.length > 0) {
|
||||
return arguments;
|
||||
}
|
||||
|
||||
Intent intent = getIntent();
|
||||
if (intent != null) {
|
||||
String[] argv = intent.getStringArrayExtra("dusk_argv");
|
||||
if (argv != null && argv.length > 0) {
|
||||
return argv;
|
||||
}
|
||||
|
||||
String rawArgs = intent.getStringExtra("dusk_args");
|
||||
if (rawArgs != null) {
|
||||
String trimmed = rawArgs.trim();
|
||||
if (!trimmed.isEmpty()) {
|
||||
return splitArgs(trimmed);
|
||||
}
|
||||
}
|
||||
if (intent == null) {
|
||||
return arguments;
|
||||
}
|
||||
return new String[0];
|
||||
String[] argv = intent.getStringArrayExtra("dusk_argv");
|
||||
if (argv != null && argv.length > 0) {
|
||||
return argv;
|
||||
}
|
||||
String rawArgs = intent.getStringExtra("dusk_args");
|
||||
return rawArgs == null ? arguments : splitArguments(rawArgs.trim());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
persistUriPermissions(data);
|
||||
}
|
||||
if (requestCode == FOLDER_DIALOG_REQUEST_CODE) {
|
||||
finishFolderDialog(resultCode, data);
|
||||
return;
|
||||
}
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
}
|
||||
|
||||
public boolean showFolderDialog(long userdata) {
|
||||
if (userdata == 0 || folderDialogUserdata != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
folderDialogUserdata = userdata;
|
||||
if (requiresManageStoragePermission() && !hasManageStoragePermission()) {
|
||||
if (!requestManageStoragePermission()) {
|
||||
finishFolderDialogWithError("Unable to request Android file access permission");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
openFolderDialog();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void openFolderDialog() {
|
||||
runOnUiThread(() -> {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
|
||||
|
||||
try {
|
||||
startActivityForResult(intent, FOLDER_DIALOG_REQUEST_CODE);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w(TAG, "Unable to open folder dialog.", e);
|
||||
finishFolderDialog(Activity.RESULT_CANCELED, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean requiresManageStoragePermission() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;
|
||||
}
|
||||
|
||||
private boolean hasManageStoragePermission() {
|
||||
return !requiresManageStoragePermission() || Environment.isExternalStorageManager();
|
||||
}
|
||||
|
||||
private boolean requestManageStoragePermission() {
|
||||
if (!requiresManageStoragePermission()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
awaitingManageStoragePermission = true;
|
||||
runOnUiThread(() -> {
|
||||
if (tryStartManageStorageIntent(
|
||||
new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||
.setData(Uri.parse("package:" + getPackageName()))) ||
|
||||
tryStartManageStorageIntent(
|
||||
new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
finishFolderDialogWithError("Unable to request Android file access permission");
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean tryStartManageStorageIntent(Intent intent) {
|
||||
try {
|
||||
startActivityForResult(intent, MANAGE_STORAGE_REQUEST_CODE);
|
||||
return true;
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w(TAG, "Unable to open all-files access settings.", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void resumeFolderDialogAfterPermissionGrant() {
|
||||
awaitingManageStoragePermission = false;
|
||||
if (folderDialogUserdata == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasManageStoragePermission()) {
|
||||
openFolderDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
finishFolderDialogWithError(
|
||||
"Allow \"All files access\" for Dusklight before choosing a custom data folder");
|
||||
}
|
||||
|
||||
private void finishFolderDialogWithError(String error) {
|
||||
long userdata = folderDialogUserdata;
|
||||
folderDialogUserdata = 0;
|
||||
awaitingManageStoragePermission = false;
|
||||
if (userdata != 0) {
|
||||
nativeFolderDialogResult(userdata, null, error);
|
||||
}
|
||||
}
|
||||
|
||||
private void finishFolderDialog(int resultCode, Intent data) {
|
||||
long userdata = folderDialogUserdata;
|
||||
folderDialogUserdata = 0;
|
||||
if (userdata == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resultCode == RESULT_OK && data != null && data.getData() != null) {
|
||||
String path = getRealPathForUri(data.getData());
|
||||
if (path != null && !path.isEmpty()) {
|
||||
nativeFolderDialogResult(userdata, path, null);
|
||||
} else {
|
||||
nativeFolderDialogResult(
|
||||
userdata, null, "Selected folder is not available as a filesystem path");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
nativeFolderDialogResult(userdata, null, null);
|
||||
}
|
||||
|
||||
private String getRealPathForUri(Uri uri) {
|
||||
if (uri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String scheme = uri.getScheme();
|
||||
if ("file".equals(scheme)) {
|
||||
return uri.getPath();
|
||||
}
|
||||
|
||||
if (!"content".equals(scheme) ||
|
||||
!EXTERNAL_STORAGE_AUTHORITY.equals(uri.getAuthority()) ||
|
||||
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return getExternalStoragePathForDocumentId(getExternalStorageDocumentId(uri));
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.w(TAG, "Unable to resolve URI: " + uri, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String getExternalStorageDocumentId(Uri uri) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && isTreeDocumentUri(uri)) {
|
||||
return DocumentsContract.getTreeDocumentId(uri);
|
||||
}
|
||||
|
||||
return DocumentsContract.getDocumentId(uri);
|
||||
}
|
||||
|
||||
private static boolean isTreeDocumentUri(Uri uri) {
|
||||
List<String> segments = uri.getPathSegments();
|
||||
return segments.size() >= 2 && "tree".equals(segments.get(0));
|
||||
}
|
||||
|
||||
private String getExternalStoragePathForDocumentId(String documentId) {
|
||||
if (documentId == null || documentId.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
if (documentId.startsWith("raw:")) {
|
||||
return documentId.substring("raw:".length());
|
||||
}
|
||||
|
||||
String[] parts = documentId.split(":", 2);
|
||||
String volumeId = parts[0];
|
||||
String relativePath = parts.length > 1 ? parts[1] : "";
|
||||
|
||||
File root = getExternalStorageRoot(volumeId);
|
||||
if (root == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return relativePath.isEmpty()
|
||||
? root.getAbsolutePath()
|
||||
: new File(root, relativePath).getAbsolutePath();
|
||||
}
|
||||
|
||||
private File getExternalStorageRoot(String volumeId) {
|
||||
if ("primary".equalsIgnoreCase(volumeId)) {
|
||||
return Environment.getExternalStorageDirectory();
|
||||
}
|
||||
if ("home".equalsIgnoreCase(volumeId)) {
|
||||
return new File(
|
||||
Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOCUMENTS);
|
||||
}
|
||||
|
||||
File[] externalFilesDirs = getExternalFilesDirs(null);
|
||||
if (externalFilesDirs != null) {
|
||||
for (File externalFilesDir : externalFilesDirs) {
|
||||
File root = getStorageRootForExternalFilesDir(externalFilesDir);
|
||||
if (root != null && volumeId.equalsIgnoreCase(root.getName())) {
|
||||
return root;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File fallback = new File("/storage", volumeId);
|
||||
return fallback.exists() ? fallback : null;
|
||||
}
|
||||
|
||||
private File getStorageRootForExternalFilesDir(File externalFilesDir) {
|
||||
if (externalFilesDir == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String path = externalFilesDir.getAbsolutePath();
|
||||
int androidDir = path.indexOf("/Android/");
|
||||
if (androidDir <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new File(path.substring(0, androidDir));
|
||||
}
|
||||
|
||||
private void persistUriPermissions(Intent data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int permissionFlags =
|
||||
data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
if (permissionFlags == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
Uri uri = data.getData();
|
||||
if (uri != null) {
|
||||
persistUriPermission(uri, permissionFlags);
|
||||
}
|
||||
|
||||
ClipData clipData = data.getClipData();
|
||||
if (clipData == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < clipData.getItemCount(); ++i) {
|
||||
Uri itemUri = clipData.getItemAt(i).getUri();
|
||||
if (itemUri != null) {
|
||||
persistUriPermission(itemUri, permissionFlags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void persistUriPermission(Uri uri, int permissionFlags) {
|
||||
if ((permissionFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
|
||||
persistUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION, "read");
|
||||
}
|
||||
if ((permissionFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
|
||||
persistUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, "write");
|
||||
}
|
||||
}
|
||||
|
||||
private void persistUriPermission(Uri uri, int permissionFlag, String permissionName) {
|
||||
try {
|
||||
getContentResolver().takePersistableUriPermission(uri, permissionFlag);
|
||||
} catch (SecurityException | IllegalArgumentException e) {
|
||||
Log.w(TAG, "Unable to persist " + permissionName + " URI permission for " + uri, e);
|
||||
}
|
||||
}
|
||||
|
||||
public String getDisplayNameForUri(String uriString) {
|
||||
if (uriString == null || uriString.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
Uri uri = Uri.parse(uriString);
|
||||
if ("content".equals(uri.getScheme())) {
|
||||
try (Cursor cursor = getContentResolver().query(
|
||||
uri, new String[] { OpenableColumns.DISPLAY_NAME }, null, null, null))
|
||||
{
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
int displayNameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
|
||||
if (displayNameColumn >= 0) {
|
||||
String displayName = cursor.getString(displayNameColumn);
|
||||
if (displayName != null && !displayName.isEmpty()) {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (SecurityException | IllegalArgumentException e) {
|
||||
Log.w(TAG, "Unable to query display name for " + uri, e);
|
||||
}
|
||||
} else if ("file".equals(uri.getScheme())) {
|
||||
String path = uri.getPath();
|
||||
if (path != null && !path.isEmpty()) {
|
||||
String name = new File(path).getName();
|
||||
if (!name.isEmpty()) {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String lastSegment = uri.getLastPathSegment();
|
||||
return lastSegment != null ? lastSegment : "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,467 +0,0 @@
|
||||
package dev.twilitrealm.dusk;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.res.AssetFileDescriptor;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
import android.provider.DocumentsContract.Root;
|
||||
import android.provider.DocumentsProvider;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class DuskDocumentsProvider extends DocumentsProvider {
|
||||
public static final String AUTHORITY = "dev.twilitrealm.dusk.documents";
|
||||
|
||||
private static final String ROOT_ID = "dusk";
|
||||
private static final String ROOT_DOCUMENT_ID = "root";
|
||||
private static final String LOCATION_DESCRIPTOR_NAME = "data_location.json";
|
||||
private static final String DIRECTORY_MIME_TYPE = Document.MIME_TYPE_DIR;
|
||||
|
||||
private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
|
||||
Root.COLUMN_ROOT_ID,
|
||||
Root.COLUMN_FLAGS,
|
||||
Root.COLUMN_TITLE,
|
||||
Root.COLUMN_DOCUMENT_ID,
|
||||
Root.COLUMN_ICON,
|
||||
Root.COLUMN_AVAILABLE_BYTES,
|
||||
Root.COLUMN_SUMMARY
|
||||
};
|
||||
|
||||
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
|
||||
Document.COLUMN_DOCUMENT_ID,
|
||||
Document.COLUMN_DISPLAY_NAME,
|
||||
Document.COLUMN_FLAGS,
|
||||
Document.COLUMN_MIME_TYPE,
|
||||
Document.COLUMN_LAST_MODIFIED,
|
||||
Document.COLUMN_SIZE
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
if (!isCustomDataPathEnabled()) {
|
||||
ensureUserDirectories();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
|
||||
final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
|
||||
if (isCustomDataPathEnabled()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
final File root = getRootDirectory();
|
||||
final MatrixCursor.RowBuilder row = result.newRow();
|
||||
|
||||
row.add(Root.COLUMN_ROOT_ID, ROOT_ID);
|
||||
row.add(Root.COLUMN_FLAGS,
|
||||
Root.FLAG_LOCAL_ONLY |
|
||||
Root.FLAG_SUPPORTS_CREATE |
|
||||
Root.FLAG_SUPPORTS_IS_CHILD);
|
||||
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.app_name));
|
||||
row.add(Root.COLUMN_DOCUMENT_ID, ROOT_DOCUMENT_ID);
|
||||
row.add(Root.COLUMN_ICON, R.mipmap.icon);
|
||||
row.add(Root.COLUMN_AVAILABLE_BYTES, root.getFreeSpace());
|
||||
row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.documents_provider_summary));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
|
||||
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
|
||||
includeDocument(result, documentId, getFileForDocumentId(documentId));
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
|
||||
throws FileNotFoundException
|
||||
{
|
||||
return queryChildDocumentsInternal(parentDocumentId, projection);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, Bundle queryArgs)
|
||||
throws FileNotFoundException
|
||||
{
|
||||
return queryChildDocumentsInternal(parentDocumentId, projection);
|
||||
}
|
||||
|
||||
private Cursor queryChildDocumentsInternal(String parentDocumentId, String[] projection)
|
||||
throws FileNotFoundException
|
||||
{
|
||||
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
|
||||
final File parent = getFileForDocumentId(parentDocumentId);
|
||||
final File[] files = parent.listFiles();
|
||||
result.setNotificationUri(getContext().getContentResolver(), getChildDocumentsUri(parentDocumentId));
|
||||
|
||||
if (files == null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (File file : files) {
|
||||
includeDocument(result, getDocumentIdForFile(file), file);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isChildDocument(String parentDocumentId, String documentId) {
|
||||
try {
|
||||
final File parent = getFileForDocumentId(parentDocumentId);
|
||||
final File child = getFileForDocumentId(documentId);
|
||||
return isInside(parent, child);
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String createDocument(String parentDocumentId, String mimeType, String displayName)
|
||||
throws FileNotFoundException
|
||||
{
|
||||
final File parent = getFileForDocumentId(parentDocumentId);
|
||||
if (!parent.isDirectory()) {
|
||||
throw new FileNotFoundException("Parent is not a directory: " + parentDocumentId);
|
||||
}
|
||||
|
||||
final String safeDisplayName = sanitizeDisplayName(displayName);
|
||||
final File file = buildUniqueFile(parent, safeDisplayName);
|
||||
final boolean created;
|
||||
if (DIRECTORY_MIME_TYPE.equals(mimeType)) {
|
||||
created = file.mkdir();
|
||||
} else {
|
||||
try {
|
||||
created = file.createNewFile();
|
||||
} catch (IOException e) {
|
||||
throw asFileNotFound("Unable to create document", e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!created) {
|
||||
throw new FileNotFoundException("Unable to create document: " + displayName);
|
||||
}
|
||||
|
||||
notifyChildrenChanged(parentDocumentId);
|
||||
return getDocumentIdForFile(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String renameDocument(String documentId, String displayName) throws FileNotFoundException {
|
||||
final File file = getFileForDocumentId(documentId);
|
||||
if (ROOT_DOCUMENT_ID.equals(documentId)) {
|
||||
throw new FileNotFoundException("Cannot rename root document");
|
||||
}
|
||||
|
||||
final File target = buildUniqueFile(file.getParentFile(), sanitizeDisplayName(displayName));
|
||||
final String parentDocumentId = getDocumentIdForFile(file.getParentFile());
|
||||
if (!file.renameTo(target)) {
|
||||
throw new FileNotFoundException("Unable to rename document: " + documentId);
|
||||
}
|
||||
notifyDocumentChanged(documentId);
|
||||
notifyDocumentChanged(getDocumentIdForFile(target));
|
||||
notifyChildrenChanged(parentDocumentId);
|
||||
return getDocumentIdForFile(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deleteDocument(String documentId) throws FileNotFoundException {
|
||||
if (ROOT_DOCUMENT_ID.equals(documentId)) {
|
||||
throw new FileNotFoundException("Cannot delete root document");
|
||||
}
|
||||
|
||||
final File file = getFileForDocumentId(documentId);
|
||||
final String parentDocumentId = getDocumentIdForFile(file.getParentFile());
|
||||
deleteRecursively(file);
|
||||
notifyDocumentChanged(documentId);
|
||||
notifyChildrenChanged(parentDocumentId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal)
|
||||
throws FileNotFoundException
|
||||
{
|
||||
return ParcelFileDescriptor.open(getFileForDocumentId(documentId), modeToParcelMode(mode));
|
||||
}
|
||||
|
||||
@Override
|
||||
public AssetFileDescriptor openDocumentThumbnail(String documentId, android.graphics.Point sizeHint,
|
||||
CancellationSignal signal) throws FileNotFoundException
|
||||
{
|
||||
throw new FileNotFoundException("Thumbnails are not supported");
|
||||
}
|
||||
|
||||
private void includeDocument(MatrixCursor result, String documentId, File file) throws FileNotFoundException {
|
||||
final MatrixCursor.RowBuilder row = result.newRow();
|
||||
final boolean isDirectory = file.isDirectory();
|
||||
final String displayName = ROOT_DOCUMENT_ID.equals(documentId)
|
||||
? getContext().getString(R.string.documents_provider_root_name)
|
||||
: file.getName();
|
||||
|
||||
int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME;
|
||||
if (isDirectory) {
|
||||
flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
|
||||
} else if (file.canWrite()) {
|
||||
flags |= Document.FLAG_SUPPORTS_WRITE;
|
||||
}
|
||||
if (ROOT_DOCUMENT_ID.equals(documentId)) {
|
||||
flags &= ~(Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME);
|
||||
}
|
||||
|
||||
row.add(Document.COLUMN_DOCUMENT_ID, documentId);
|
||||
row.add(Document.COLUMN_DISPLAY_NAME, displayName);
|
||||
row.add(Document.COLUMN_FLAGS, flags);
|
||||
row.add(Document.COLUMN_MIME_TYPE, isDirectory ? DIRECTORY_MIME_TYPE : getMimeType(file));
|
||||
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
|
||||
row.add(Document.COLUMN_SIZE, isDirectory ? null : file.length());
|
||||
}
|
||||
|
||||
private File getRootDirectory() throws FileNotFoundException {
|
||||
if (isCustomDataPathEnabled()) {
|
||||
throw new FileNotFoundException(
|
||||
"Dusk DocumentsProvider is disabled while a custom data path is configured");
|
||||
}
|
||||
|
||||
final File root = getContext().getFilesDir();
|
||||
if (root == null) {
|
||||
throw new FileNotFoundException("Dusklight files directory is unavailable");
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
private File getFileForDocumentId(String documentId) throws FileNotFoundException {
|
||||
final File root = getRootDirectory();
|
||||
if (ROOT_DOCUMENT_ID.equals(documentId)) {
|
||||
return root;
|
||||
}
|
||||
if (!documentId.startsWith(ROOT_DOCUMENT_ID + "/")) {
|
||||
throw new FileNotFoundException("Invalid document id: " + documentId);
|
||||
}
|
||||
|
||||
final String relativePath = documentId.substring(ROOT_DOCUMENT_ID.length() + 1);
|
||||
final File file = new File(root, relativePath);
|
||||
if (!isInside(root, file)) {
|
||||
throw new FileNotFoundException("Document escapes Dusklight files directory: " + documentId);
|
||||
}
|
||||
if (!file.exists()) {
|
||||
throw new FileNotFoundException("Document does not exist: " + documentId);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
private String getDocumentIdForFile(File file) throws FileNotFoundException {
|
||||
final File root = getRootDirectory();
|
||||
if (sameFile(root, file)) {
|
||||
return ROOT_DOCUMENT_ID;
|
||||
}
|
||||
if (!isInside(root, file)) {
|
||||
throw new FileNotFoundException("File escapes Dusklight files directory: " + file);
|
||||
}
|
||||
|
||||
final String rootPath = canonicalPath(root);
|
||||
final String filePath = canonicalPath(file);
|
||||
return ROOT_DOCUMENT_ID + "/" + filePath.substring(rootPath.length() + 1);
|
||||
}
|
||||
|
||||
private void ensureUserDirectories() {
|
||||
final File root = getContext().getFilesDir();
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
new File(root, "texture_replacements").mkdirs();
|
||||
new File(root, "USA/Card A").mkdirs();
|
||||
new File(root, "EUR/Card A").mkdirs();
|
||||
}
|
||||
|
||||
private boolean isCustomDataPathEnabled() {
|
||||
if (getContext() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final File filesDir = getContext().getFilesDir();
|
||||
if (filesDir == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final File descriptor = new File(filesDir, LOCATION_DESCRIPTOR_NAME);
|
||||
if (!descriptor.isFile()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final JSONObject json = new JSONObject(readText(descriptor));
|
||||
return "custom".equals(json.optString("mode", "default"));
|
||||
} catch (IOException | JSONException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String readText(File file) throws IOException {
|
||||
try (FileInputStream input = new FileInputStream(file);
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream())
|
||||
{
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = input.read(buffer)) != -1) {
|
||||
output.write(buffer, 0, bytesRead);
|
||||
}
|
||||
return output.toString(StandardCharsets.UTF_8.name());
|
||||
}
|
||||
}
|
||||
|
||||
private static String[] resolveRootProjection(String[] projection) {
|
||||
return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
|
||||
}
|
||||
|
||||
private static String[] resolveDocumentProjection(String[] projection) {
|
||||
return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
|
||||
}
|
||||
|
||||
private static String sanitizeDisplayName(String displayName) throws FileNotFoundException {
|
||||
if (displayName == null) {
|
||||
throw new FileNotFoundException("Document name is empty");
|
||||
}
|
||||
|
||||
final String sanitized = displayName.trim();
|
||||
if (sanitized.isEmpty() || ".".equals(sanitized) || "..".equals(sanitized) ||
|
||||
sanitized.contains("/") || sanitized.contains("\\"))
|
||||
{
|
||||
throw new FileNotFoundException("Invalid document name: " + displayName);
|
||||
}
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private static File buildUniqueFile(File parent, String displayName) {
|
||||
File file = new File(parent, displayName);
|
||||
if (!file.exists()) {
|
||||
return file;
|
||||
}
|
||||
|
||||
final int dot = displayName.lastIndexOf('.');
|
||||
final String baseName = dot > 0 ? displayName.substring(0, dot) : displayName;
|
||||
final String extension = dot > 0 ? displayName.substring(dot) : "";
|
||||
for (int i = 1; i < 100; ++i) {
|
||||
file = new File(parent, baseName + " (" + i + ")" + extension);
|
||||
if (!file.exists()) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return new File(parent, baseName + " (" + System.currentTimeMillis() + ")" + extension);
|
||||
}
|
||||
|
||||
private static int modeToParcelMode(String mode) {
|
||||
if ("r".equals(mode)) {
|
||||
return ParcelFileDescriptor.MODE_READ_ONLY;
|
||||
}
|
||||
if ("w".equals(mode) || "wt".equals(mode)) {
|
||||
return ParcelFileDescriptor.MODE_WRITE_ONLY |
|
||||
ParcelFileDescriptor.MODE_CREATE |
|
||||
ParcelFileDescriptor.MODE_TRUNCATE;
|
||||
}
|
||||
if ("wa".equals(mode)) {
|
||||
return ParcelFileDescriptor.MODE_WRITE_ONLY |
|
||||
ParcelFileDescriptor.MODE_CREATE |
|
||||
ParcelFileDescriptor.MODE_APPEND;
|
||||
}
|
||||
if ("rw".equals(mode)) {
|
||||
return ParcelFileDescriptor.MODE_READ_WRITE |
|
||||
ParcelFileDescriptor.MODE_CREATE;
|
||||
}
|
||||
if ("rwt".equals(mode)) {
|
||||
return ParcelFileDescriptor.MODE_READ_WRITE |
|
||||
ParcelFileDescriptor.MODE_CREATE |
|
||||
ParcelFileDescriptor.MODE_TRUNCATE;
|
||||
}
|
||||
return ParcelFileDescriptor.MODE_READ_ONLY;
|
||||
}
|
||||
|
||||
private static String getMimeType(File file) {
|
||||
final int dot = file.getName().lastIndexOf('.');
|
||||
if (dot >= 0) {
|
||||
final String extension = file.getName().substring(dot + 1).toLowerCase();
|
||||
final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
|
||||
if (mimeType != null) {
|
||||
return mimeType;
|
||||
}
|
||||
}
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
private Uri getChildDocumentsUri(String parentDocumentId) {
|
||||
return DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId);
|
||||
}
|
||||
|
||||
private void notifyChildrenChanged(String parentDocumentId) {
|
||||
final ContentResolver resolver = getContext().getContentResolver();
|
||||
resolver.notifyChange(getChildDocumentsUri(parentDocumentId), null, false);
|
||||
}
|
||||
|
||||
private void notifyDocumentChanged(String documentId) {
|
||||
final ContentResolver resolver = getContext().getContentResolver();
|
||||
resolver.notifyChange(DocumentsContract.buildDocumentUri(AUTHORITY, documentId), null, false);
|
||||
}
|
||||
|
||||
private static void deleteRecursively(File file) throws FileNotFoundException {
|
||||
if (file.isDirectory()) {
|
||||
final File[] children = file.listFiles();
|
||||
if (children != null) {
|
||||
for (File child : children) {
|
||||
deleteRecursively(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!file.delete()) {
|
||||
throw new FileNotFoundException("Unable to delete document: " + file);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isInside(File parent, File child) {
|
||||
try {
|
||||
final String parentPath = canonicalPath(parent);
|
||||
final String childPath = canonicalPath(child);
|
||||
return childPath.equals(parentPath) || childPath.startsWith(parentPath + File.separator);
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean sameFile(File a, File b) {
|
||||
try {
|
||||
return canonicalPath(a).equals(canonicalPath(b));
|
||||
} catch (FileNotFoundException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static String canonicalPath(File file) throws FileNotFoundException {
|
||||
try {
|
||||
return file.getCanonicalPath();
|
||||
} catch (IOException e) {
|
||||
throw asFileNotFound("Unable to resolve path", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static FileNotFoundException asFileNotFound(String message, IOException cause) {
|
||||
final FileNotFoundException exception = new FileNotFoundException(message + ": " + cause.getMessage());
|
||||
exception.initCause(cause);
|
||||
return exception;
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
package dev.twilitrealm.dusk;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.net.ssl.HttpsURLConnection;
|
||||
|
||||
public final class DuskHttpClient {
|
||||
public static final int ERROR_NONE = 0;
|
||||
public static final int ERROR_INVALID_URL = 1;
|
||||
public static final int ERROR_UNSUPPORTED_SCHEME = 2;
|
||||
public static final int ERROR_TIMEOUT = 3;
|
||||
public static final int ERROR_TOO_LARGE = 4;
|
||||
public static final int ERROR_NETWORK = 5;
|
||||
|
||||
private static final int MAX_REDIRECTS = 5;
|
||||
|
||||
public static final class Response {
|
||||
public int error;
|
||||
public String message;
|
||||
public int statusCode;
|
||||
public String[] headerNames;
|
||||
public String[] headerValues;
|
||||
public byte[] body;
|
||||
|
||||
Response(int error, String message, int statusCode, String[] headerNames,
|
||||
String[] headerValues, byte[] body) {
|
||||
this.error = error;
|
||||
this.message = message;
|
||||
this.statusCode = statusCode;
|
||||
this.headerNames = headerNames != null ? headerNames : new String[0];
|
||||
this.headerValues = headerValues != null ? headerValues : new String[0];
|
||||
this.body = body != null ? body : new byte[0];
|
||||
}
|
||||
}
|
||||
|
||||
private DuskHttpClient() {
|
||||
}
|
||||
|
||||
public static Response get(String url, String[] headerNames, String[] headerValues,
|
||||
int timeoutMs, long maxBodyBytes) {
|
||||
if (url == null || url.isEmpty()) {
|
||||
return fail(ERROR_INVALID_URL, "URL is empty");
|
||||
}
|
||||
|
||||
try {
|
||||
URL currentUrl = new URL(url);
|
||||
if (!isHttps(currentUrl)) {
|
||||
return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported");
|
||||
}
|
||||
|
||||
for (int redirect = 0; redirect <= MAX_REDIRECTS; ++redirect) {
|
||||
HttpsURLConnection connection =
|
||||
(HttpsURLConnection) currentUrl.openConnection();
|
||||
try {
|
||||
connection.setRequestMethod("GET");
|
||||
connection.setConnectTimeout(timeoutMs);
|
||||
connection.setReadTimeout(timeoutMs);
|
||||
connection.setUseCaches(false);
|
||||
connection.setInstanceFollowRedirects(false);
|
||||
applyHeaders(connection, headerNames, headerValues);
|
||||
|
||||
int statusCode = connection.getResponseCode();
|
||||
if (isRedirect(statusCode)) {
|
||||
String location = connection.getHeaderField("Location");
|
||||
if (location == null || location.isEmpty()) {
|
||||
return fail(ERROR_NETWORK, "Redirect response did not include Location",
|
||||
statusCode, connection, new byte[0]);
|
||||
}
|
||||
|
||||
URL nextUrl = new URL(currentUrl, location);
|
||||
if (!isHttps(nextUrl)) {
|
||||
return fail(ERROR_UNSUPPORTED_SCHEME,
|
||||
"Only https:// redirects are supported", statusCode,
|
||||
connection, new byte[0]);
|
||||
}
|
||||
currentUrl = nextUrl;
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] body = readBody(connection, statusCode, maxBodyBytes);
|
||||
return success(statusCode, connection, body);
|
||||
} catch (ResponseTooLargeException e) {
|
||||
return fail(ERROR_TOO_LARGE, "Response body exceeded the configured limit",
|
||||
safeStatusCode(connection), connection, e.partialBody);
|
||||
} finally {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
return fail(ERROR_NETWORK, "Too many redirects");
|
||||
} catch (MalformedURLException e) {
|
||||
return fail(ERROR_INVALID_URL, "Failed to parse URL");
|
||||
} catch (SocketTimeoutException e) {
|
||||
return fail(ERROR_TIMEOUT, "Request timed out");
|
||||
} catch (IOException e) {
|
||||
String message = e.getMessage();
|
||||
return fail(ERROR_NETWORK, message != null ? message : e.toString());
|
||||
} catch (ClassCastException e) {
|
||||
return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported");
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyHeaders(HttpsURLConnection connection, String[] names,
|
||||
String[] values) {
|
||||
if (names == null || values == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
int count = Math.min(names.length, values.length);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (names[i] != null && values[i] != null) {
|
||||
connection.setRequestProperty(names[i], values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isHttps(URL url) {
|
||||
return "https".equalsIgnoreCase(url.getProtocol());
|
||||
}
|
||||
|
||||
private static boolean isRedirect(int statusCode) {
|
||||
return statusCode == HttpURLConnection.HTTP_MOVED_PERM ||
|
||||
statusCode == HttpURLConnection.HTTP_MOVED_TEMP ||
|
||||
statusCode == HttpURLConnection.HTTP_SEE_OTHER ||
|
||||
statusCode == 307 ||
|
||||
statusCode == 308;
|
||||
}
|
||||
|
||||
private static byte[] readBody(HttpsURLConnection connection, int statusCode,
|
||||
long maxBodyBytes) throws IOException,
|
||||
ResponseTooLargeException {
|
||||
InputStream stream = statusCode >= HttpURLConnection.HTTP_BAD_REQUEST ?
|
||||
connection.getErrorStream() : connection.getInputStream();
|
||||
if (stream == null) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
try (InputStream bodyStream = stream;
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
|
||||
byte[] buffer = new byte[8192];
|
||||
long total = 0;
|
||||
while (true) {
|
||||
int read = bodyStream.read(buffer);
|
||||
if (read < 0) {
|
||||
return out.toByteArray();
|
||||
}
|
||||
if (read == 0) {
|
||||
continue;
|
||||
}
|
||||
if (read > maxBodyBytes || total > maxBodyBytes - read) {
|
||||
throw new ResponseTooLargeException(out.toByteArray());
|
||||
}
|
||||
out.write(buffer, 0, read);
|
||||
total += read;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int safeStatusCode(HttpsURLConnection connection) {
|
||||
try {
|
||||
return connection.getResponseCode();
|
||||
} catch (IOException e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static Response success(int statusCode, HttpsURLConnection connection, byte[] body) {
|
||||
HeaderLists headers = readHeaders(connection);
|
||||
return new Response(ERROR_NONE, "", statusCode, headers.names, headers.values, body);
|
||||
}
|
||||
|
||||
private static Response fail(int error, String message) {
|
||||
return new Response(error, message, 0, null, null, null);
|
||||
}
|
||||
|
||||
private static Response fail(int error, String message, int statusCode,
|
||||
HttpsURLConnection connection, byte[] body) {
|
||||
HeaderLists headers = readHeaders(connection);
|
||||
return new Response(error, message, statusCode, headers.names, headers.values, body);
|
||||
}
|
||||
|
||||
private static HeaderLists readHeaders(HttpsURLConnection connection) {
|
||||
List<String> names = new ArrayList<>();
|
||||
List<String> values = new ArrayList<>();
|
||||
|
||||
Map<String, List<String>> headerFields = connection.getHeaderFields();
|
||||
if (headerFields == null) {
|
||||
return new HeaderLists(new String[0], new String[0]);
|
||||
}
|
||||
|
||||
for (Map.Entry<String, List<String>> entry : headerFields.entrySet()) {
|
||||
String name = entry.getKey();
|
||||
if (name == null) {
|
||||
continue;
|
||||
}
|
||||
List<String> entryValues = entry.getValue();
|
||||
if (entryValues == null || entryValues.isEmpty()) {
|
||||
names.add(name);
|
||||
values.add("");
|
||||
continue;
|
||||
}
|
||||
for (String value : entryValues) {
|
||||
names.add(name);
|
||||
values.add(value != null ? value : "");
|
||||
}
|
||||
}
|
||||
|
||||
return new HeaderLists(names.toArray(new String[0]), values.toArray(new String[0]));
|
||||
}
|
||||
|
||||
private static final class HeaderLists {
|
||||
final String[] names;
|
||||
final String[] values;
|
||||
|
||||
HeaderLists(String[] names, String[] values) {
|
||||
this.names = names;
|
||||
this.values = values;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ResponseTooLargeException extends Exception {
|
||||
final byte[] partialBody;
|
||||
|
||||
ResponseTooLargeException(byte[] partialBody) {
|
||||
this.partialBody = partialBody;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.hardware.usb.UsbDevice;
|
||||
|
||||
interface HIDDevice
|
||||
{
|
||||
public int getId();
|
||||
public int getVendorId();
|
||||
public int getProductId();
|
||||
public String getSerialNumber();
|
||||
public int getVersion();
|
||||
public String getManufacturerName();
|
||||
public String getProductName();
|
||||
public UsbDevice getDevice();
|
||||
public boolean open();
|
||||
public int writeReport(byte[] report, boolean feature);
|
||||
public boolean readReport(byte[] report, boolean feature);
|
||||
public void setFrozen(boolean frozen);
|
||||
public void close();
|
||||
public void shutdown();
|
||||
}
|
||||
@@ -1,829 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCallback;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.bluetooth.BluetoothGattDescriptor;
|
||||
import android.bluetooth.BluetoothManager;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
import android.bluetooth.BluetoothGattService;
|
||||
import android.hardware.usb.UsbDevice;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.os.*;
|
||||
|
||||
//import com.android.internal.util.HexDump;
|
||||
|
||||
import java.lang.Runnable;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.UUID;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.Matcher;
|
||||
|
||||
class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice {
|
||||
|
||||
private static final String TAG = "hidapi";
|
||||
private HIDDeviceManager mManager;
|
||||
private BluetoothDevice mDevice;
|
||||
private int mDeviceId;
|
||||
private BluetoothGatt mGatt;
|
||||
private boolean mIsRegistered = false;
|
||||
private boolean mIsConnected = false;
|
||||
private boolean mIsChromebook = false;
|
||||
private boolean mIsReconnecting = false;
|
||||
private boolean mHasEnabledNotifications = false;
|
||||
private boolean mHasSeenInputUpdate = false;
|
||||
private boolean mFrozen = false;
|
||||
private LinkedList<GattOperation> mOperations;
|
||||
GattOperation mCurrentOperation = null;
|
||||
private Handler mHandler;
|
||||
private int mProductId = -1;
|
||||
private int mReportId = 0;
|
||||
private UUID mInputCharacteristic;
|
||||
|
||||
private static final int D0G_BLE2_PID = 0x1106;
|
||||
private static final int TRITON_BLE_PID = 0x1303;
|
||||
|
||||
|
||||
private static final int TRANSPORT_AUTO = 0;
|
||||
private static final int TRANSPORT_BREDR = 1;
|
||||
private static final int TRANSPORT_LE = 2;
|
||||
|
||||
private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000;
|
||||
|
||||
static final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3");
|
||||
static final UUID inputCharacteristicD0G = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3");
|
||||
static final UUID inputCharacteristicTriton_0x45 = UUID.fromString("100F6C7A-1735-4313-B402-38567131E5F3");
|
||||
static final UUID inputCharacteristicTriton_0x47 = UUID.fromString("100F6C7C-1735-4313-B402-38567131E5F3");
|
||||
static final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3");
|
||||
static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 };
|
||||
|
||||
private HashMap<Integer, BluetoothGattCharacteristic> mOutputReportChars = new HashMap<Integer, BluetoothGattCharacteristic>();
|
||||
|
||||
static class GattOperation {
|
||||
private enum Operation {
|
||||
CHR_READ,
|
||||
CHR_WRITE,
|
||||
ENABLE_NOTIFICATION
|
||||
}
|
||||
|
||||
Operation mOp;
|
||||
UUID mUuid;
|
||||
byte[] mValue;
|
||||
BluetoothGatt mGatt;
|
||||
boolean mResult = true;
|
||||
int mDelayMs = 0;
|
||||
|
||||
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
|
||||
mGatt = gatt;
|
||||
mOp = operation;
|
||||
mUuid = uuid;
|
||||
}
|
||||
|
||||
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, int delayMs) {
|
||||
mGatt = gatt;
|
||||
mOp = operation;
|
||||
mUuid = uuid;
|
||||
mDelayMs = delayMs;
|
||||
}
|
||||
|
||||
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
|
||||
mGatt = gatt;
|
||||
mOp = operation;
|
||||
mUuid = uuid;
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value, int delayMs) {
|
||||
mGatt = gatt;
|
||||
mOp = operation;
|
||||
mUuid = uuid;
|
||||
mValue = value;
|
||||
mDelayMs = delayMs;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
// This is executed in main thread
|
||||
BluetoothGattCharacteristic chr;
|
||||
|
||||
switch (mOp) {
|
||||
case CHR_READ:
|
||||
chr = getCharacteristic(mUuid);
|
||||
//Log.v(TAG, "Reading characteristic " + chr.getUuid());
|
||||
if (!mGatt.readCharacteristic(chr)) {
|
||||
Log.e(TAG, "Unable to read characteristic " + mUuid.toString());
|
||||
mResult = false;
|
||||
break;
|
||||
}
|
||||
mResult = true;
|
||||
break;
|
||||
case CHR_WRITE:
|
||||
chr = getCharacteristic(mUuid);
|
||||
//Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
|
||||
chr.setValue(mValue);
|
||||
if (!mGatt.writeCharacteristic(chr)) {
|
||||
Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
|
||||
mResult = false;
|
||||
break;
|
||||
}
|
||||
mResult = true;
|
||||
break;
|
||||
case ENABLE_NOTIFICATION:
|
||||
chr = getCharacteristic(mUuid);
|
||||
//Log.v(TAG, "Writing descriptor of " + chr.getUuid());
|
||||
if (chr != null) {
|
||||
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
|
||||
if (cccd != null) {
|
||||
int properties = chr.getProperties();
|
||||
byte[] value;
|
||||
if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
|
||||
value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
|
||||
} else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) {
|
||||
value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
|
||||
} else {
|
||||
Log.e(TAG, "Unable to start notifications on input characteristic");
|
||||
mResult = false;
|
||||
return;
|
||||
}
|
||||
|
||||
mGatt.setCharacteristicNotification(chr, true);
|
||||
cccd.setValue(value);
|
||||
if (!mGatt.writeDescriptor(cccd)) {
|
||||
Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
|
||||
mResult = false;
|
||||
return;
|
||||
}
|
||||
mResult = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean finish() {
|
||||
return mResult;
|
||||
}
|
||||
|
||||
public int getDelayMs() { return mDelayMs; }
|
||||
|
||||
private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
|
||||
BluetoothGattService valveService = mGatt.getService(steamControllerService);
|
||||
if (valveService == null)
|
||||
return null;
|
||||
return valveService.getCharacteristic(uuid);
|
||||
}
|
||||
|
||||
static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) {
|
||||
return new GattOperation(gatt, Operation.CHR_READ, uuid);
|
||||
}
|
||||
|
||||
static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) {
|
||||
return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value);
|
||||
}
|
||||
|
||||
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
|
||||
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid);
|
||||
}
|
||||
|
||||
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid, int delayMs) {
|
||||
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid, delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) {
|
||||
mManager = manager;
|
||||
mDevice = device;
|
||||
mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier());
|
||||
mIsRegistered = false;
|
||||
mIsChromebook = SDLActivity.isChromebook();
|
||||
mOperations = new LinkedList<GattOperation>();
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
mGatt = connectGatt();
|
||||
mHasEnabledNotifications = false;
|
||||
mHasSeenInputUpdate = false;
|
||||
// final HIDDeviceBLESteamController finalThis = this;
|
||||
// mHandler.postDelayed(new Runnable() {
|
||||
// @Override
|
||||
// void run() {
|
||||
// finalThis.checkConnectionForChromebookIssue();
|
||||
// }
|
||||
// }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
String getIdentifier() {
|
||||
return String.format("SteamController.%s", mDevice.getAddress());
|
||||
}
|
||||
|
||||
BluetoothGatt getGatt() {
|
||||
return mGatt;
|
||||
}
|
||||
|
||||
// Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
|
||||
// of TRANSPORT_LE. Let's force ourselves to connect low energy.
|
||||
private BluetoothGatt connectGatt(boolean managed) {
|
||||
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
|
||||
try {
|
||||
return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
|
||||
} catch (Exception e) {
|
||||
return mDevice.connectGatt(mManager.getContext(), managed, this);
|
||||
}
|
||||
} else {
|
||||
return mDevice.connectGatt(mManager.getContext(), managed, this);
|
||||
}
|
||||
}
|
||||
|
||||
private BluetoothGatt connectGatt() {
|
||||
return connectGatt(false);
|
||||
}
|
||||
|
||||
protected int getConnectionState() {
|
||||
|
||||
Context context = mManager.getContext();
|
||||
if (context == null) {
|
||||
// We are lacking any context to get our Bluetooth information. We'll just assume disconnected.
|
||||
return BluetoothProfile.STATE_DISCONNECTED;
|
||||
}
|
||||
|
||||
BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
|
||||
if (btManager == null) {
|
||||
// This device doesn't support Bluetooth. We should never be here, because how did
|
||||
// we instantiate a device to start with?
|
||||
return BluetoothProfile.STATE_DISCONNECTED;
|
||||
}
|
||||
|
||||
return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
|
||||
}
|
||||
|
||||
void reconnect() {
|
||||
|
||||
if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
|
||||
mGatt.disconnect();
|
||||
mGatt = connectGatt();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected void checkConnectionForChromebookIssue() {
|
||||
if (!mIsChromebook) {
|
||||
// We only do this on Chromebooks, because otherwise it's really annoying to just attempt
|
||||
// over and over.
|
||||
return;
|
||||
}
|
||||
|
||||
int connectionState = getConnectionState();
|
||||
|
||||
switch (connectionState) {
|
||||
case BluetoothProfile.STATE_CONNECTED:
|
||||
if (!mIsConnected) {
|
||||
// We are in the Bad Chromebook Place. We can force a disconnect
|
||||
// to try to recover.
|
||||
Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect.");
|
||||
mIsReconnecting = true;
|
||||
mGatt.disconnect();
|
||||
mGatt = connectGatt(false);
|
||||
break;
|
||||
}
|
||||
else if (!isRegistered()) {
|
||||
if (mGatt.getServices().size() > 0) {
|
||||
Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover.");
|
||||
probeService(this);
|
||||
}
|
||||
else {
|
||||
Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover.");
|
||||
mIsReconnecting = true;
|
||||
mGatt.disconnect();
|
||||
mGatt = connectGatt(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!");
|
||||
return;
|
||||
}
|
||||
break;
|
||||
|
||||
case BluetoothProfile.STATE_DISCONNECTED:
|
||||
Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover.");
|
||||
|
||||
mIsReconnecting = true;
|
||||
mGatt.disconnect();
|
||||
mGatt = connectGatt(false);
|
||||
break;
|
||||
|
||||
case BluetoothProfile.STATE_CONNECTING:
|
||||
Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer.");
|
||||
break;
|
||||
}
|
||||
|
||||
final HIDDeviceBLESteamController finalThis = this;
|
||||
mHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
finalThis.checkConnectionForChromebookIssue();
|
||||
}
|
||||
}, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
|
||||
}
|
||||
|
||||
private boolean isRegistered() {
|
||||
return mIsRegistered;
|
||||
}
|
||||
|
||||
private void setRegistered() {
|
||||
mIsRegistered = true;
|
||||
}
|
||||
|
||||
private boolean probeService(HIDDeviceBLESteamController controller) {
|
||||
|
||||
if (isRegistered()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!mIsConnected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Log.v(TAG, "probeService controller=" + controller);
|
||||
|
||||
for (BluetoothGattService service : mGatt.getServices()) {
|
||||
if (service.getUuid().equals(steamControllerService)) {
|
||||
Log.v(TAG, "Found Valve steam controller service " + service.getUuid());
|
||||
|
||||
for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
|
||||
if (chr.getUuid().equals(inputCharacteristicTriton_0x45)) {
|
||||
Log.v(TAG, "Found Triton input characteristic 0x45");
|
||||
mProductId = TRITON_BLE_PID;
|
||||
mReportId = 0x45;
|
||||
mInputCharacteristic = chr.getUuid();
|
||||
} else if (chr.getUuid().equals(inputCharacteristicTriton_0x47)) {
|
||||
Log.v(TAG, "Found Triton input characteristic 0x47");
|
||||
mProductId = TRITON_BLE_PID;
|
||||
mReportId = 0x47;
|
||||
mInputCharacteristic = chr.getUuid();
|
||||
} else if (chr.getUuid().equals(inputCharacteristicD0G)) {
|
||||
Log.v(TAG, "Found D0G input characteristic");
|
||||
mProductId = D0G_BLE2_PID;
|
||||
mReportId = 0x03;
|
||||
mInputCharacteristic = chr.getUuid();
|
||||
} else {
|
||||
Pattern reportPattern = Pattern.compile("100F6C([0-9A-Z]{2})", Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcher = reportPattern.matcher(chr.getUuid().toString());
|
||||
|
||||
if (matcher.find()) {
|
||||
try {
|
||||
int reportId = Integer.parseInt(matcher.group(1), 16);
|
||||
|
||||
reportId -= 0x35;
|
||||
if (reportId >= 0x80) {
|
||||
// This is a Triton output report characteristic that we need to care about.
|
||||
Log.v(TAG, "Found Triton output report 0x" + Integer.toString(reportId, 16));
|
||||
mOutputReportChars.put(reportId, chr);
|
||||
}
|
||||
}
|
||||
catch (NumberFormatException nfe) {
|
||||
Log.w(TAG, "Could not parse report characteristic " + chr.getUuid().toString() + ": " + nfe.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
|
||||
if (chr.getUuid().equals(mInputCharacteristic)) {
|
||||
// Start notifications
|
||||
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
|
||||
if (cccd != null) {
|
||||
enableNotification(chr.getUuid());
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) {
|
||||
Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us.");
|
||||
mIsConnected = false;
|
||||
mIsReconnecting = true;
|
||||
mGatt.disconnect();
|
||||
mGatt = connectGatt(false);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void finishCurrentGattOperation() {
|
||||
GattOperation op = null;
|
||||
synchronized (mOperations) {
|
||||
if (mCurrentOperation != null) {
|
||||
op = mCurrentOperation;
|
||||
mCurrentOperation = null;
|
||||
}
|
||||
}
|
||||
if (op != null) {
|
||||
boolean result = op.finish(); // TODO: Maybe in main thread as well?
|
||||
|
||||
// Our operation failed, let's add it back to the beginning of our queue.
|
||||
if (!result) {
|
||||
mOperations.addFirst(op);
|
||||
}
|
||||
}
|
||||
executeNextGattOperation();
|
||||
}
|
||||
|
||||
private void executeNextGattOperation() {
|
||||
synchronized (mOperations) {
|
||||
if (mCurrentOperation != null)
|
||||
return;
|
||||
|
||||
if (mOperations.isEmpty())
|
||||
return;
|
||||
|
||||
mCurrentOperation = mOperations.removeFirst();
|
||||
}
|
||||
|
||||
Runnable gattOperationRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
synchronized (mOperations) {
|
||||
if (mCurrentOperation == null) {
|
||||
Log.e(TAG, "Current operation null in executor?");
|
||||
return;
|
||||
}
|
||||
|
||||
mCurrentOperation.run();
|
||||
// now wait for the GATT callback and when it comes, finish this operation
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (mCurrentOperation.getDelayMs() == 0) {
|
||||
// Run in main thread
|
||||
mHandler.post(gattOperationRunnable);
|
||||
}
|
||||
else {
|
||||
// If we have a delay on this operation, wait before we post it.
|
||||
mHandler.postDelayed(gattOperationRunnable, mCurrentOperation.getDelayMs());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private void queueGattOperation(GattOperation op) {
|
||||
synchronized (mOperations) {
|
||||
mOperations.add(op);
|
||||
}
|
||||
executeNextGattOperation();
|
||||
}
|
||||
|
||||
private void enableNotification(UUID chrUuid) {
|
||||
// Add a 500ms delay to notification write for Amazon Fire TV devices, as otherwise if we do this too quickly after connecting
|
||||
// it will return success and then silently drop the operation on the floor.
|
||||
GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid, 500);
|
||||
queueGattOperation(op);
|
||||
|
||||
// Amazon Fire devices can also silently timeout on writeDescriptor, so
|
||||
// set up a little delayed check that will attempt to write a second time.
|
||||
//
|
||||
// While this only seems to be needed on Amazon Fire TV devices at present, it
|
||||
// doesn't hurt to have a retry on other devices as well.
|
||||
//
|
||||
final HIDDeviceBLESteamController finalThis = this;
|
||||
final UUID finalUuid = chrUuid;
|
||||
mHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!finalThis.mHasEnabledNotifications) {
|
||||
|
||||
if (finalThis.mHasSeenInputUpdate) {
|
||||
// Amazon Five devices may have enabled notifications on the input characteristic and not given us a callback. If we've seen
|
||||
// input reports, though, somewhat by definition notifications are enabled.
|
||||
Log.w(TAG, "WriteDescriptor has never returned, but we've seen input reports. Moving on with controller initialization.");
|
||||
finalThis.mHasEnabledNotifications = true;
|
||||
finalThis.enableValveMode();
|
||||
return;
|
||||
}
|
||||
|
||||
// Give one more try.
|
||||
GattOperation retry = HIDDeviceBLESteamController.GattOperation.enableNotification(finalThis.mGatt, finalUuid, 500);
|
||||
finalThis.queueGattOperation(retry);
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
void writeCharacteristic(UUID uuid, byte[] value) {
|
||||
GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value);
|
||||
queueGattOperation(op);
|
||||
}
|
||||
|
||||
void readCharacteristic(UUID uuid) {
|
||||
GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid);
|
||||
queueGattOperation(op);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////////// BluetoothGattCallback overridden methods
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public void onConnectionStateChange(BluetoothGatt g, int status, int newState) {
|
||||
//Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
|
||||
mIsReconnecting = false;
|
||||
if (newState == 2) {
|
||||
mIsConnected = true;
|
||||
// Run directly, without GattOperation
|
||||
if (!isRegistered()) {
|
||||
mHandler.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
mGatt.discoverServices();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (newState == 0) {
|
||||
mIsConnected = false;
|
||||
}
|
||||
|
||||
// Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
|
||||
//Log.v(TAG, "onServicesDiscovered status=" + status);
|
||||
if (status == 0) {
|
||||
if (gatt.getServices().size() == 0) {
|
||||
Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack.");
|
||||
mIsReconnecting = true;
|
||||
mIsConnected = false;
|
||||
gatt.disconnect();
|
||||
mGatt = connectGatt(false);
|
||||
} else {
|
||||
if (getProductId() == TRITON_BLE_PID) {
|
||||
// Android will not properly play well with Data Length Extensions without manually requesting a large MTU,
|
||||
// and Triton controllers require DLE support.
|
||||
//
|
||||
// 517 is basically a "magic number" as far as Android's bluetooth code is concerned, so do not change
|
||||
// this value. It is functionally "please enable data length extensions" on some Android builds.
|
||||
mGatt.requestMtu(517);
|
||||
}
|
||||
|
||||
probeService(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||
//Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid());
|
||||
|
||||
if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) {
|
||||
mManager.HIDDeviceReportResponse(getId(), characteristic.getValue());
|
||||
}
|
||||
|
||||
finishCurrentGattOperation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
|
||||
//Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
|
||||
|
||||
if (characteristic.getUuid().equals(reportCharacteristic)) {
|
||||
// Only register controller with the native side once it has been fully configured
|
||||
if (!isRegistered()) {
|
||||
Log.v(TAG, "Registering Steam Controller with ID: " + getId());
|
||||
mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true, mReportId);
|
||||
setRegistered();
|
||||
}
|
||||
}
|
||||
|
||||
finishCurrentGattOperation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
|
||||
// Enable this for verbose logging of controller input reports
|
||||
//Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue()));
|
||||
|
||||
if (characteristic.getUuid().equals(mInputCharacteristic) && !mFrozen) {
|
||||
mHasSeenInputUpdate = true;
|
||||
mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
|
||||
//Log.v(TAG, "onDescriptorRead status=" + status);
|
||||
}
|
||||
|
||||
private void enableValveMode()
|
||||
{
|
||||
BluetoothGattService valveService = mGatt.getService(steamControllerService);
|
||||
if (valveService == null)
|
||||
return;
|
||||
|
||||
BluetoothGattCharacteristic reportChr = valveService.getCharacteristic(reportCharacteristic);
|
||||
if (reportChr != null) {
|
||||
if (getProductId() == TRITON_BLE_PID) {
|
||||
// For Triton we just mark things registered.
|
||||
Log.v(TAG, "Registering Triton Steam Controller with ID: " + getId());
|
||||
mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true, mReportId);
|
||||
setRegistered();
|
||||
} else {
|
||||
// For the original controller, we need to manually enter Valve mode.
|
||||
Log.v(TAG, "Writing report characteristic to enter valve mode");
|
||||
reportChr.setValue(enterValveMode);
|
||||
mGatt.writeCharacteristic(reportChr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
|
||||
BluetoothGattCharacteristic chr = descriptor.getCharacteristic();
|
||||
//Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid());
|
||||
|
||||
if (chr.getUuid().equals(mInputCharacteristic)) {
|
||||
mHasEnabledNotifications = true;
|
||||
enableValveMode();
|
||||
}
|
||||
|
||||
finishCurrentGattOperation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
|
||||
//Log.v(TAG, "onReliableWriteCompleted status=" + status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
|
||||
//Log.v(TAG, "onReadRemoteRssi status=" + status);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
|
||||
//Log.v(TAG, "onMtuChanged status=" + status);
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////// Public API
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return mDeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVendorId() {
|
||||
// Valve Corporation
|
||||
final int VALVE_USB_VID = 0x28DE;
|
||||
return VALVE_USB_VID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProductId() {
|
||||
if (mProductId > 0) {
|
||||
// We've already set a product ID.
|
||||
return mProductId;
|
||||
}
|
||||
|
||||
if (mDevice.getName().startsWith("Steam Ctrl")) {
|
||||
// We're a newer Triton device
|
||||
mProductId = TRITON_BLE_PID;
|
||||
} else {
|
||||
// We're an OG Steam Controller
|
||||
mProductId = D0G_BLE2_PID;
|
||||
}
|
||||
|
||||
return mProductId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSerialNumber() {
|
||||
// This will be read later via feature report by Steam
|
||||
return "12345";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVersion() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturerName() {
|
||||
return "Valve Corporation";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProductName() {
|
||||
return "Steam Controller";
|
||||
}
|
||||
|
||||
@Override
|
||||
public UsbDevice getDevice() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean open() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int writeReport(byte[] report, boolean feature) {
|
||||
if (!isRegistered()) {
|
||||
Log.e(TAG, "Attempted writeReport before Steam Controller is registered!");
|
||||
if (mIsConnected) {
|
||||
probeService(this);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (feature) {
|
||||
// We need to skip the first byte, as that doesn't go over the air
|
||||
byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
|
||||
//Log.v(TAG, "writeFeatureReport " + HexDump.dumpHexString(actual_report));
|
||||
writeCharacteristic(reportCharacteristic, actual_report);
|
||||
return report.length;
|
||||
} else {
|
||||
// If we're an original-recipe Steam Controller we just write to the characteristic directly.
|
||||
if (getProductId() == D0G_BLE2_PID) {
|
||||
//Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report));
|
||||
writeCharacteristic(reportCharacteristic, report);
|
||||
return report.length;
|
||||
}
|
||||
|
||||
// If we're a Triton, we need to find the correct report characteristic.
|
||||
if (report.length > 0) {
|
||||
int reportId = report[0] & 0xFF;
|
||||
BluetoothGattCharacteristic targetedReportCharacteristic = mOutputReportChars.get(reportId);
|
||||
if (targetedReportCharacteristic != null) {
|
||||
byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
|
||||
//Log.v(TAG, "writeOutputReport 0x" + Integer.toString(reportId, 16) + " " + HexDump.dumpHexString(report));
|
||||
writeCharacteristic(targetedReportCharacteristic.getUuid(), actual_report);
|
||||
return report.length;
|
||||
} else {
|
||||
Log.w(TAG, "Got report write request for unknown report type 0x" + Integer.toString(reportId, 16));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readReport(byte[] report, boolean feature) {
|
||||
if (!isRegistered()) {
|
||||
Log.e(TAG, "Attempted readReport before Steam Controller is registered!");
|
||||
if (mIsConnected) {
|
||||
probeService(this);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (feature) {
|
||||
readCharacteristic(reportCharacteristic);
|
||||
return true;
|
||||
} else {
|
||||
// Not implemented
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFrozen(boolean frozen) {
|
||||
mFrozen = frozen;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
close();
|
||||
|
||||
BluetoothGatt g = mGatt;
|
||||
if (g != null) {
|
||||
g.disconnect();
|
||||
g.close();
|
||||
mGatt = null;
|
||||
}
|
||||
mManager = null;
|
||||
mIsRegistered = false;
|
||||
mIsConnected = false;
|
||||
mOperations.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,698 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.PendingIntent;
|
||||
import android.bluetooth.BluetoothAdapter;
|
||||
import android.bluetooth.BluetoothDevice;
|
||||
import android.bluetooth.BluetoothManager;
|
||||
import android.bluetooth.BluetoothProfile;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.hardware.usb.*;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class HIDDeviceManager {
|
||||
private static final String TAG = "hidapi";
|
||||
private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION";
|
||||
|
||||
private static HIDDeviceManager sManager;
|
||||
private static int sManagerRefCount = 0;
|
||||
|
||||
static public HIDDeviceManager acquire(Context context) {
|
||||
if (sManagerRefCount == 0) {
|
||||
sManager = new HIDDeviceManager(context);
|
||||
}
|
||||
++sManagerRefCount;
|
||||
return sManager;
|
||||
}
|
||||
|
||||
static public void release(HIDDeviceManager manager) {
|
||||
if (manager == sManager) {
|
||||
--sManagerRefCount;
|
||||
if (sManagerRefCount == 0) {
|
||||
sManager.close();
|
||||
sManager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Context mContext;
|
||||
private HashMap<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>();
|
||||
private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>();
|
||||
private int mNextDeviceId = 0;
|
||||
private SharedPreferences mSharedPreferences = null;
|
||||
private boolean mIsChromebook = false;
|
||||
private UsbManager mUsbManager;
|
||||
private Handler mHandler;
|
||||
private BluetoothManager mBluetoothManager;
|
||||
private List<BluetoothDevice> mLastBluetoothDevices;
|
||||
|
||||
private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
|
||||
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
handleUsbDeviceAttached(usbDevice);
|
||||
} else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
|
||||
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
handleUsbDeviceDetached(usbDevice);
|
||||
} else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) {
|
||||
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
|
||||
handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
// Bluetooth device was connected. If it was a Steam Controller, handle it
|
||||
if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
|
||||
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
Log.d(TAG, "Bluetooth device connected: " + device);
|
||||
|
||||
if (isSteamController(device)) {
|
||||
connectBluetoothDevice(device);
|
||||
}
|
||||
}
|
||||
|
||||
// Bluetooth device was disconnected, remove from controller manager (if any)
|
||||
if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
|
||||
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
|
||||
Log.d(TAG, "Bluetooth device disconnected: " + device);
|
||||
|
||||
disconnectBluetoothDevice(device);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private HIDDeviceManager(final Context context) {
|
||||
mContext = context;
|
||||
|
||||
HIDDeviceRegisterCallback();
|
||||
|
||||
mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE);
|
||||
mIsChromebook = SDLActivity.isChromebook();
|
||||
|
||||
// if (shouldClear) {
|
||||
// SharedPreferences.Editor spedit = mSharedPreferences.edit();
|
||||
// spedit.clear();
|
||||
// spedit.apply();
|
||||
// }
|
||||
// else
|
||||
{
|
||||
mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0);
|
||||
}
|
||||
}
|
||||
|
||||
Context getContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
int getDeviceIDForIdentifier(String identifier) {
|
||||
SharedPreferences.Editor spedit = mSharedPreferences.edit();
|
||||
|
||||
int result = mSharedPreferences.getInt(identifier, 0);
|
||||
if (result == 0) {
|
||||
result = mNextDeviceId++;
|
||||
spedit.putInt("next_device_id", mNextDeviceId);
|
||||
}
|
||||
|
||||
spedit.putInt(identifier, result);
|
||||
spedit.apply();
|
||||
return result;
|
||||
}
|
||||
|
||||
private void initializeUSB() {
|
||||
mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE);
|
||||
if (mUsbManager == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
// Logging
|
||||
for (UsbDevice device : mUsbManager.getDeviceList().values()) {
|
||||
Log.i(TAG,"Path: " + device.getDeviceName());
|
||||
Log.i(TAG,"Manufacturer: " + device.getManufacturerName());
|
||||
Log.i(TAG,"Product: " + device.getProductName());
|
||||
Log.i(TAG,"ID: " + device.getDeviceId());
|
||||
Log.i(TAG,"Class: " + device.getDeviceClass());
|
||||
Log.i(TAG,"Protocol: " + device.getDeviceProtocol());
|
||||
Log.i(TAG,"Vendor ID " + device.getVendorId());
|
||||
Log.i(TAG,"Product ID: " + device.getProductId());
|
||||
Log.i(TAG,"Interface count: " + device.getInterfaceCount());
|
||||
Log.i(TAG,"---------------------------------------");
|
||||
|
||||
// Get interface details
|
||||
for (int index = 0; index < device.getInterfaceCount(); index++) {
|
||||
UsbInterface mUsbInterface = device.getInterface(index);
|
||||
Log.i(TAG," ***** *****");
|
||||
Log.i(TAG," Interface index: " + index);
|
||||
Log.i(TAG," Interface ID: " + mUsbInterface.getId());
|
||||
Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass());
|
||||
Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass());
|
||||
Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol());
|
||||
Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount());
|
||||
|
||||
// Get endpoint details
|
||||
for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++)
|
||||
{
|
||||
UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi);
|
||||
Log.i(TAG," ++++ ++++ ++++");
|
||||
Log.i(TAG," Endpoint index: " + epi);
|
||||
Log.i(TAG," Attributes: " + mEndpoint.getAttributes());
|
||||
Log.i(TAG," Direction: " + mEndpoint.getDirection());
|
||||
Log.i(TAG," Number: " + mEndpoint.getEndpointNumber());
|
||||
Log.i(TAG," Interval: " + mEndpoint.getInterval());
|
||||
Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize());
|
||||
Log.i(TAG," Type: " + mEndpoint.getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.i(TAG," No more devices connected.");
|
||||
*/
|
||||
|
||||
// Register for USB broadcasts and permission completions
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
|
||||
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
|
||||
filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION);
|
||||
if (Build.VERSION.SDK_INT >= 33) { /* Android 13.0 (TIRAMISU) */
|
||||
mContext.registerReceiver(mUsbBroadcast, filter, Context.RECEIVER_EXPORTED);
|
||||
} else {
|
||||
mContext.registerReceiver(mUsbBroadcast, filter);
|
||||
}
|
||||
|
||||
for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
|
||||
handleUsbDeviceAttached(usbDevice);
|
||||
}
|
||||
}
|
||||
|
||||
UsbManager getUSBManager() {
|
||||
return mUsbManager;
|
||||
}
|
||||
|
||||
private void shutdownUSB() {
|
||||
try {
|
||||
mContext.unregisterReceiver(mUsbBroadcast);
|
||||
} catch (Exception e) {
|
||||
// We may not have registered, that's okay
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) {
|
||||
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
|
||||
return true;
|
||||
}
|
||||
if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) {
|
||||
final int XB360_IFACE_SUBCLASS = 93;
|
||||
final int XB360_IFACE_PROTOCOL = 1; // Wired
|
||||
final int XB360W_IFACE_PROTOCOL = 129; // Wireless
|
||||
final int[] SUPPORTED_VENDORS = {
|
||||
0x0079, // GPD Win 2
|
||||
0x044f, // Thrustmaster
|
||||
0x045e, // Microsoft
|
||||
0x046d, // Logitech
|
||||
0x056e, // Elecom
|
||||
0x06a3, // Saitek
|
||||
0x0738, // Mad Catz
|
||||
0x07ff, // Mad Catz
|
||||
0x0e6f, // PDP
|
||||
0x0f0d, // Hori
|
||||
0x1038, // SteelSeries
|
||||
0x11c9, // Nacon
|
||||
0x12ab, // Unknown
|
||||
0x1430, // RedOctane
|
||||
0x146b, // BigBen
|
||||
0x1532, // Razer Sabertooth
|
||||
0x15e4, // Numark
|
||||
0x162e, // Joytech
|
||||
0x1689, // Razer Onza
|
||||
0x1949, // Lab126, Inc.
|
||||
0x1bad, // Harmonix
|
||||
0x20d6, // PowerA
|
||||
0x24c6, // PowerA
|
||||
0x2c22, // Qanba
|
||||
0x2dc8, // 8BitDo
|
||||
0x3537, // GameSir
|
||||
0x37d7, // Flydigi
|
||||
0x9886, // ASTRO Gaming
|
||||
};
|
||||
|
||||
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||
usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
|
||||
(usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL ||
|
||||
usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) {
|
||||
int vendor_id = usbDevice.getVendorId();
|
||||
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||
if (vendor_id == supportedVid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) {
|
||||
final int XB1_IFACE_SUBCLASS = 71;
|
||||
final int XB1_IFACE_PROTOCOL = 208;
|
||||
final int[] SUPPORTED_VENDORS = {
|
||||
0x03f0, // HP
|
||||
0x044f, // Thrustmaster
|
||||
0x045e, // Microsoft
|
||||
0x0738, // Mad Catz
|
||||
0x0b05, // ASUS
|
||||
0x0e6f, // PDP
|
||||
0x0f0d, // Hori
|
||||
0x10f5, // Turtle Beach
|
||||
0x1532, // Razer Wildcat
|
||||
0x20d6, // PowerA
|
||||
0x24c6, // PowerA
|
||||
0x294b, // Snakebyte
|
||||
0x2dc8, // 8BitDo
|
||||
0x2e24, // Hyperkin
|
||||
0x2e95, // SCUF
|
||||
0x3285, // Nacon
|
||||
0x3537, // GameSir
|
||||
0x366c, // ByoWave
|
||||
};
|
||||
|
||||
if (usbInterface.getId() == 0 &&
|
||||
usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
|
||||
usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
|
||||
usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
|
||||
int vendor_id = usbDevice.getVendorId();
|
||||
for (int supportedVid : SUPPORTED_VENDORS) {
|
||||
if (vendor_id == supportedVid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void handleUsbDeviceAttached(UsbDevice usbDevice) {
|
||||
connectHIDDeviceUSB(usbDevice);
|
||||
}
|
||||
|
||||
private void handleUsbDeviceDetached(UsbDevice usbDevice) {
|
||||
List<Integer> devices = new ArrayList<Integer>();
|
||||
for (HIDDevice device : mDevicesById.values()) {
|
||||
if (usbDevice.equals(device.getDevice())) {
|
||||
devices.add(device.getId());
|
||||
}
|
||||
}
|
||||
for (int id : devices) {
|
||||
HIDDevice device = mDevicesById.get(id);
|
||||
mDevicesById.remove(id);
|
||||
device.shutdown();
|
||||
HIDDeviceDisconnected(id);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) {
|
||||
for (HIDDevice device : mDevicesById.values()) {
|
||||
if (usbDevice.equals(device.getDevice())) {
|
||||
boolean opened = false;
|
||||
if (permission_granted) {
|
||||
opened = device.open();
|
||||
}
|
||||
HIDDeviceOpenResult(device.getId(), opened);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void connectHIDDeviceUSB(UsbDevice usbDevice) {
|
||||
synchronized (this) {
|
||||
int interface_mask = 0;
|
||||
for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) {
|
||||
UsbInterface usbInterface = usbDevice.getInterface(interface_index);
|
||||
if (isHIDDeviceInterface(usbDevice, usbInterface)) {
|
||||
// Check to see if we've already added this interface
|
||||
// This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive
|
||||
int interface_id = usbInterface.getId();
|
||||
if ((interface_mask & (1 << interface_id)) != 0) {
|
||||
continue;
|
||||
}
|
||||
interface_mask |= (1 << interface_id);
|
||||
|
||||
HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index);
|
||||
int id = device.getId();
|
||||
mDevicesById.put(id, device);
|
||||
HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol(), false, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeBluetooth() {
|
||||
Log.d(TAG, "Initializing Bluetooth");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 31 /* Android 12 */ &&
|
||||
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH_CONNECT");
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT <= 30 /* Android 11.0 (R) */ &&
|
||||
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
|
||||
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
|
||||
Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find bonded bluetooth controllers and create SteamControllers for them
|
||||
mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
|
||||
if (mBluetoothManager == null) {
|
||||
// This device doesn't support Bluetooth.
|
||||
return;
|
||||
}
|
||||
|
||||
BluetoothAdapter btAdapter = mBluetoothManager.getAdapter();
|
||||
if (btAdapter == null) {
|
||||
// This device has Bluetooth support in the codebase, but has no available adapters.
|
||||
return;
|
||||
}
|
||||
|
||||
// Get our bonded devices.
|
||||
for (BluetoothDevice device : btAdapter.getBondedDevices()) {
|
||||
|
||||
Log.d(TAG, "Bluetooth device available: " + device);
|
||||
if (isSteamController(device)) {
|
||||
connectBluetoothDevice(device);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// NOTE: These don't work on Chromebooks, to my undying dismay.
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
|
||||
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
|
||||
if (Build.VERSION.SDK_INT >= 33) { /* Android 13.0 (TIRAMISU) */
|
||||
mContext.registerReceiver(mBluetoothBroadcast, filter, Context.RECEIVER_EXPORTED);
|
||||
} else {
|
||||
mContext.registerReceiver(mBluetoothBroadcast, filter);
|
||||
}
|
||||
|
||||
if (mIsChromebook) {
|
||||
mHandler = new Handler(Looper.getMainLooper());
|
||||
mLastBluetoothDevices = new ArrayList<BluetoothDevice>();
|
||||
|
||||
// final HIDDeviceManager finalThis = this;
|
||||
// mHandler.postDelayed(new Runnable() {
|
||||
// @Override
|
||||
// public void run() {
|
||||
// finalThis.chromebookConnectionHandler();
|
||||
// }
|
||||
// }, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
private void shutdownBluetooth() {
|
||||
try {
|
||||
mContext.unregisterReceiver(mBluetoothBroadcast);
|
||||
} catch (Exception e) {
|
||||
// We may not have registered, that's okay
|
||||
}
|
||||
}
|
||||
|
||||
// Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly.
|
||||
// This function provides a sort of dummy version of that, watching for changes in the
|
||||
// connected devices and attempting to add controllers as things change.
|
||||
void chromebookConnectionHandler() {
|
||||
if (!mIsChromebook) {
|
||||
return;
|
||||
}
|
||||
|
||||
ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>();
|
||||
ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>();
|
||||
|
||||
List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
|
||||
|
||||
for (BluetoothDevice bluetoothDevice : currentConnected) {
|
||||
if (!mLastBluetoothDevices.contains(bluetoothDevice)) {
|
||||
connected.add(bluetoothDevice);
|
||||
}
|
||||
}
|
||||
for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) {
|
||||
if (!currentConnected.contains(bluetoothDevice)) {
|
||||
disconnected.add(bluetoothDevice);
|
||||
}
|
||||
}
|
||||
|
||||
mLastBluetoothDevices = currentConnected;
|
||||
|
||||
for (BluetoothDevice bluetoothDevice : disconnected) {
|
||||
disconnectBluetoothDevice(bluetoothDevice);
|
||||
}
|
||||
for (BluetoothDevice bluetoothDevice : connected) {
|
||||
connectBluetoothDevice(bluetoothDevice);
|
||||
}
|
||||
|
||||
final HIDDeviceManager finalThis = this;
|
||||
mHandler.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
finalThis.chromebookConnectionHandler();
|
||||
}
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) {
|
||||
Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice);
|
||||
synchronized (this) {
|
||||
if (mBluetoothDevices.containsKey(bluetoothDevice)) {
|
||||
Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect");
|
||||
|
||||
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
|
||||
device.reconnect();
|
||||
|
||||
return false;
|
||||
}
|
||||
HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice);
|
||||
int id = device.getId();
|
||||
mBluetoothDevices.put(bluetoothDevice, device);
|
||||
mDevicesById.put(id, device);
|
||||
|
||||
// The Steam Controller will mark itself connected once initialization is complete
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) {
|
||||
synchronized (this) {
|
||||
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
|
||||
if (device == null)
|
||||
return;
|
||||
|
||||
int id = device.getId();
|
||||
mBluetoothDevices.remove(bluetoothDevice);
|
||||
mDevicesById.remove(id);
|
||||
device.shutdown();
|
||||
HIDDeviceDisconnected(id);
|
||||
}
|
||||
}
|
||||
|
||||
boolean isSteamController(BluetoothDevice bluetoothDevice) {
|
||||
// Sanity check. If you pass in a null device, by definition it is never a Steam Controller.
|
||||
if (bluetoothDevice == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the device has no local name, we really don't want to try an equality check against it.
|
||||
if (bluetoothDevice.getName() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Steam Controllers will always support Bluetooth Low Energy
|
||||
if ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Match on the name either the original Steam Controller or the new second-generation one advertise with.
|
||||
return bluetoothDevice.getName().equals("SteamController") || bluetoothDevice.getName().startsWith("Steam Ctrl");
|
||||
}
|
||||
|
||||
private void close() {
|
||||
shutdownUSB();
|
||||
shutdownBluetooth();
|
||||
synchronized (this) {
|
||||
for (HIDDevice device : mDevicesById.values()) {
|
||||
device.shutdown();
|
||||
}
|
||||
mDevicesById.clear();
|
||||
mBluetoothDevices.clear();
|
||||
HIDDeviceReleaseCallback();
|
||||
}
|
||||
}
|
||||
|
||||
public void setFrozen(boolean frozen) {
|
||||
synchronized (this) {
|
||||
for (HIDDevice device : mDevicesById.values()) {
|
||||
device.setFrozen(frozen);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private HIDDevice getDevice(int id) {
|
||||
synchronized (this) {
|
||||
HIDDevice result = mDevicesById.get(id);
|
||||
if (result == null) {
|
||||
Log.v(TAG, "No device for id: " + id);
|
||||
Log.v(TAG, "Available devices: " + mDevicesById.keySet());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
////////// JNI interface functions
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
boolean initialize(boolean usb, boolean bluetooth) {
|
||||
Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")");
|
||||
|
||||
if (usb) {
|
||||
initializeUSB();
|
||||
}
|
||||
if (bluetooth) {
|
||||
initializeBluetooth();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean openDevice(int deviceID) {
|
||||
Log.v(TAG, "openDevice deviceID=" + deviceID);
|
||||
HIDDevice device = getDevice(deviceID);
|
||||
if (device == null) {
|
||||
HIDDeviceDisconnected(deviceID);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Look to see if this is a USB device and we have permission to access it
|
||||
UsbDevice usbDevice = device.getDevice();
|
||||
if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) {
|
||||
HIDDeviceOpenPending(deviceID);
|
||||
try {
|
||||
final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31
|
||||
int flags;
|
||||
if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
|
||||
flags = FLAG_MUTABLE;
|
||||
} else {
|
||||
flags = 0;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(HIDDeviceManager.ACTION_USB_PERMISSION);
|
||||
intent.setPackage(mContext.getPackageName());
|
||||
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, intent, flags));
|
||||
} catch (Exception e) {
|
||||
Log.v(TAG, "Couldn't request permission for USB device " + usbDevice);
|
||||
HIDDeviceOpenResult(deviceID, false);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return device.open();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int writeReport(int deviceID, byte[] report, boolean feature) {
|
||||
try {
|
||||
//Log.v(TAG, "writeReport deviceID=" + deviceID + " length=" + report.length);
|
||||
HIDDevice device;
|
||||
device = getDevice(deviceID);
|
||||
if (device == null) {
|
||||
HIDDeviceDisconnected(deviceID);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return device.writeReport(report, feature);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
boolean readReport(int deviceID, byte[] report, boolean feature) {
|
||||
try {
|
||||
//Log.v(TAG, "readReport deviceID=" + deviceID);
|
||||
HIDDevice device;
|
||||
device = getDevice(deviceID);
|
||||
if (device == null) {
|
||||
HIDDeviceDisconnected(deviceID);
|
||||
return false;
|
||||
}
|
||||
|
||||
return device.readReport(report, feature);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void closeDevice(int deviceID) {
|
||||
try {
|
||||
Log.v(TAG, "closeDevice deviceID=" + deviceID);
|
||||
HIDDevice device;
|
||||
device = getDevice(deviceID);
|
||||
if (device == null) {
|
||||
HIDDeviceDisconnected(deviceID);
|
||||
return;
|
||||
}
|
||||
|
||||
device.close();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
/////////////// Native methods
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private native void HIDDeviceRegisterCallback();
|
||||
private native void HIDDeviceReleaseCallback();
|
||||
|
||||
native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol, boolean bBluetooth, int reportID);
|
||||
native void HIDDeviceOpenPending(int deviceID);
|
||||
native void HIDDeviceOpenResult(int deviceID, boolean opened);
|
||||
native void HIDDeviceDisconnected(int deviceID);
|
||||
|
||||
native void HIDDeviceInputReport(int deviceID, byte[] report);
|
||||
native void HIDDeviceReportResponse(int deviceID, byte[] report);
|
||||
}
|
||||
@@ -1,354 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.hardware.usb.*;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
class HIDDeviceUSB implements HIDDevice {
|
||||
|
||||
private static final String TAG = "hidapi";
|
||||
|
||||
protected HIDDeviceManager mManager;
|
||||
protected UsbDevice mDevice;
|
||||
protected int mInterfaceIndex;
|
||||
protected int mInterface;
|
||||
protected int mDeviceId;
|
||||
protected UsbDeviceConnection mConnection;
|
||||
protected UsbEndpoint mInputEndpoint;
|
||||
protected UsbEndpoint mOutputEndpoint;
|
||||
protected InputThread mInputThread;
|
||||
protected boolean mRunning;
|
||||
protected boolean mFrozen;
|
||||
protected boolean mClaimed;
|
||||
|
||||
public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) {
|
||||
mManager = manager;
|
||||
mDevice = usbDevice;
|
||||
mInterfaceIndex = interface_index;
|
||||
mInterface = mDevice.getInterface(mInterfaceIndex).getId();
|
||||
mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier());
|
||||
mRunning = false;
|
||||
mClaimed = false;
|
||||
}
|
||||
|
||||
String getIdentifier() {
|
||||
return String.format(Locale.ENGLISH, "%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return mDeviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVendorId() {
|
||||
return mDevice.getVendorId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProductId() {
|
||||
return mDevice.getProductId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSerialNumber() {
|
||||
String result = null;
|
||||
try {
|
||||
result = mDevice.getSerialNumber();
|
||||
}
|
||||
catch (SecurityException exception) {
|
||||
//Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage());
|
||||
}
|
||||
if (result == null) {
|
||||
result = "";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getVersion() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturerName() {
|
||||
String result;
|
||||
result = mDevice.getManufacturerName();
|
||||
if (result == null) {
|
||||
result = String.format("%x", getVendorId());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getProductName() {
|
||||
String result;
|
||||
result = mDevice.getProductName();
|
||||
if (result == null) {
|
||||
result = String.format("%x", getProductId());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UsbDevice getDevice() {
|
||||
return mDevice;
|
||||
}
|
||||
|
||||
String getDeviceName() {
|
||||
return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean open() {
|
||||
mConnection = mManager.getUSBManager().openDevice(mDevice);
|
||||
if (mConnection == null) {
|
||||
Log.w(TAG, "Unable to open USB device " + getDeviceName());
|
||||
return false;
|
||||
}
|
||||
|
||||
// Force claim our interface
|
||||
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
|
||||
if (!mConnection.claimInterface(iface, true)) {
|
||||
Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName());
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
mClaimed = true;
|
||||
|
||||
// Find the endpoints
|
||||
for (int j = 0; j < iface.getEndpointCount(); j++) {
|
||||
UsbEndpoint endpt = iface.getEndpoint(j);
|
||||
switch (endpt.getDirection()) {
|
||||
case UsbConstants.USB_DIR_IN:
|
||||
if (mInputEndpoint == null) {
|
||||
mInputEndpoint = endpt;
|
||||
}
|
||||
break;
|
||||
case UsbConstants.USB_DIR_OUT:
|
||||
if (mOutputEndpoint == null) {
|
||||
mOutputEndpoint = endpt;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure the required endpoints were present. The original Steam Controller and the wireless dongle for it do NOT
|
||||
// actually have -- or require -- output endpoints, so we need to accept only an input one for them or else we'll fall
|
||||
// back to the Android system gamepad functionality (and lose our paddles et al).
|
||||
if (mInputEndpoint == null) {
|
||||
Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName());
|
||||
mConnection.releaseInterface(iface);
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Start listening for input
|
||||
mRunning = true;
|
||||
mInputThread = new InputThread();
|
||||
mInputThread.start();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int writeReport(byte[] report, boolean feature) {
|
||||
if (mConnection == null) {
|
||||
Log.w(TAG, "writeReport() called with no device connection");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!mClaimed) {
|
||||
Log.w(TAG, "writeReport() called but some other process currently owns the USB device");
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (feature) {
|
||||
int res = -1;
|
||||
int offset = 0;
|
||||
int length = report.length;
|
||||
boolean skipped_report_id = false;
|
||||
byte report_number = report[0];
|
||||
|
||||
if (report_number == 0x0) {
|
||||
++offset;
|
||||
--length;
|
||||
skipped_report_id = true;
|
||||
}
|
||||
|
||||
res = mConnection.controlTransfer(
|
||||
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT,
|
||||
0x09/*HID set_report*/,
|
||||
(3/*HID feature*/ << 8) | report_number,
|
||||
mInterface,
|
||||
report, offset, length,
|
||||
1000/*timeout millis*/);
|
||||
|
||||
if (res < 0) {
|
||||
Log.w(TAG, "writeFeatureReport() returned " + res + " on device " + getDeviceName());
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (skipped_report_id) {
|
||||
++length;
|
||||
}
|
||||
return length;
|
||||
} else {
|
||||
if (mOutputEndpoint == null)
|
||||
{
|
||||
Log.e(TAG, "Tried to write an output report to an interface with no output endpoint!");
|
||||
return -1;
|
||||
}
|
||||
int res = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000);
|
||||
if (res != report.length) {
|
||||
Log.w(TAG, "writeOutputReport() returned " + res + " on device " + getDeviceName());
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean readReport(byte[] report, boolean feature) {
|
||||
int res = -1;
|
||||
int offset = 0;
|
||||
int length = report.length;
|
||||
boolean skipped_report_id = false;
|
||||
byte report_number = report[0];
|
||||
|
||||
if (mConnection == null) {
|
||||
Log.w(TAG, "readReport() called with no device connection");
|
||||
return false;
|
||||
}
|
||||
if (!mClaimed) {
|
||||
if (feature) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (report_number == 0x0) {
|
||||
/* Offset the return buffer by 1, so that the report ID
|
||||
will remain in byte 0. */
|
||||
++offset;
|
||||
--length;
|
||||
skipped_report_id = true;
|
||||
}
|
||||
|
||||
res = mConnection.controlTransfer(
|
||||
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN,
|
||||
0x01/*HID get_report*/,
|
||||
((feature ? 3/*HID feature*/ : 1/*HID Input*/) << 8) | report_number,
|
||||
mInterface,
|
||||
report, offset, length,
|
||||
1000/*timeout millis*/);
|
||||
|
||||
if (res < 0) {
|
||||
Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (skipped_report_id) {
|
||||
++res;
|
||||
++length;
|
||||
}
|
||||
|
||||
byte[] data;
|
||||
if (res == length) {
|
||||
data = report;
|
||||
} else {
|
||||
data = Arrays.copyOfRange(report, 0, res);
|
||||
}
|
||||
mManager.HIDDeviceReportResponse(mDeviceId, data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
mRunning = false;
|
||||
if (mInputThread != null) {
|
||||
while (mInputThread.isAlive()) {
|
||||
mInputThread.interrupt();
|
||||
try {
|
||||
mInputThread.join();
|
||||
} catch (InterruptedException e) {
|
||||
// Keep trying until we're done
|
||||
}
|
||||
}
|
||||
mInputThread = null;
|
||||
}
|
||||
if (mConnection != null) {
|
||||
if (mClaimed) {
|
||||
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
|
||||
mConnection.releaseInterface(iface);
|
||||
}
|
||||
mConnection.close();
|
||||
mConnection = null;
|
||||
mClaimed = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
close();
|
||||
mManager = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFrozen(boolean frozen) {
|
||||
mFrozen = frozen;
|
||||
|
||||
/* If we have a valid device connection and the claim state doesn't match what we want, try to correct that. */
|
||||
if (mConnection != null && mClaimed == mFrozen) {
|
||||
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
|
||||
if (frozen) {
|
||||
mClaimed = !mConnection.releaseInterface(iface);
|
||||
if (mClaimed) {
|
||||
Log.e(TAG, "Tried to release claim on USB device, but failed!");
|
||||
}
|
||||
} else {
|
||||
mClaimed = mConnection.claimInterface(iface, true);
|
||||
if (!mClaimed) {
|
||||
Log.e(TAG, "Tried to regain claim on USB device, but failed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected class InputThread extends Thread {
|
||||
@Override
|
||||
public void run() {
|
||||
int packetSize = mInputEndpoint.getMaxPacketSize();
|
||||
byte[] packet = new byte[packetSize];
|
||||
while (mRunning) {
|
||||
int r;
|
||||
try
|
||||
{
|
||||
r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e);
|
||||
break;
|
||||
}
|
||||
if (r < 0) {
|
||||
// Could be a timeout or an I/O error
|
||||
}
|
||||
if (r > 0) {
|
||||
byte[] data;
|
||||
if (r == packetSize) {
|
||||
data = packet;
|
||||
} else {
|
||||
data = Arrays.copyOfRange(packet, 0, r);
|
||||
}
|
||||
|
||||
if (!mFrozen) {
|
||||
mManager.HIDDeviceInputReport(mDeviceId, data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
SDL library initialization
|
||||
*/
|
||||
public class SDL {
|
||||
|
||||
// This function should be called first and sets up the native code
|
||||
// so it can call into the Java classes
|
||||
static public void setupJNI() {
|
||||
SDLActivity.nativeSetupJNI();
|
||||
SDLAudioManager.nativeSetupJNI();
|
||||
SDLControllerManager.nativeSetupJNI();
|
||||
}
|
||||
|
||||
// This function should be called each time the activity is started
|
||||
static public void initialize() {
|
||||
setContext(null);
|
||||
|
||||
SDLActivity.initialize();
|
||||
SDLAudioManager.initialize();
|
||||
SDLControllerManager.initialize();
|
||||
}
|
||||
|
||||
// This function stores the current activity (SDL or not)
|
||||
static public void setContext(Activity context) {
|
||||
SDLAudioManager.setContext(context);
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
static public Activity getContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
|
||||
loadLibrary(libraryName, mContext);
|
||||
}
|
||||
|
||||
static void loadLibrary(String libraryName, Context context) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
|
||||
|
||||
if (libraryName == null) {
|
||||
throw new NullPointerException("No library name provided.");
|
||||
}
|
||||
|
||||
try {
|
||||
// Let's see if we have ReLinker available in the project. This is necessary for
|
||||
// some projects that have huge numbers of local libraries bundled, and thus may
|
||||
// trip a bug in Android's native library loader which ReLinker works around. (If
|
||||
// loadLibrary works properly, ReLinker will simply use the normal Android method
|
||||
// internally.)
|
||||
//
|
||||
// To use ReLinker, just add it as a dependency. For more information, see
|
||||
// https://github.com/KeepSafe/ReLinker for ReLinker's repository.
|
||||
//
|
||||
Class<?> relinkClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
|
||||
Class<?> relinkListenerClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
|
||||
Class<?> contextClass = context.getClassLoader().loadClass("android.content.Context");
|
||||
Class<?> stringClass = context.getClassLoader().loadClass("java.lang.String");
|
||||
|
||||
// Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if
|
||||
// they've changed during updates.
|
||||
Method forceMethod = relinkClass.getDeclaredMethod("force");
|
||||
Object relinkInstance = forceMethod.invoke(null);
|
||||
Class<?> relinkInstanceClass = relinkInstance.getClass();
|
||||
|
||||
// Actually load the library!
|
||||
Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass);
|
||||
loadMethod.invoke(relinkInstance, context, libraryName, null, null);
|
||||
}
|
||||
catch (final Throwable e) {
|
||||
// Fall back
|
||||
try {
|
||||
System.loadLibrary(libraryName);
|
||||
}
|
||||
catch (final UnsatisfiedLinkError ule) {
|
||||
throw ule;
|
||||
}
|
||||
catch (final SecurityException se) {
|
||||
throw se;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static Activity mContext;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user