Compare commits

...

32 Commits

Author SHA1 Message Date
gymnast86 0da58ac91d gx.h instead of dolphin/gx/GXstruct.h 2026-08-09 10:13:51 -07:00
gymnast86 c29cafa8a2 constexpr descriptions 2026-08-09 10:13:32 -07:00
gymnast86 5e669c8f0f fix mod.json 2026-08-09 09:24:49 -07:00
gymnast86 3e625cf006 Merge branch 'main' of https://github.com/TwilitRealm/dusk into basic-cosmetics-mod 2026-08-09 09:13:18 -07:00
gymnast86 c6f497b525 move option descriptions 2026-08-09 08:52:29 -07:00
gymnast86 79d3c7c76a recolor epona's eyelids 2026-08-09 08:52:16 -07:00
gymnast86 16dd1118d1 add support for ordon sword recoloring 2026-08-09 08:18:28 -07:00
gymnast86 e9918cc8ed cleanup 2026-08-09 08:17:42 -07:00
gymnast86 058304dc26 recolor heart piece models 2026-08-08 20:10:50 -07:00
gymnast86 d55b44b5a5 override colors for items on other models 2026-08-08 20:10:21 -07:00
Giorgio Mendieta 7434a0f834 Enable color diagnostics for CMake (#2273) 2026-08-08 16:14:52 -06:00
gymnast86 469575fd65 fix pane cache again 2026-08-08 07:09:28 -07:00
gymnast86 4883c11182 Merge branch 'main' of https://github.com/TwilitRealm/dusk into basic-cosmetics-mod 2026-08-07 18:34:19 -07:00
gymnast86 7ac2340312 add UI colors 2026-08-07 18:33:40 -07:00
gymnast86 3352ea3dde fix PaneCache naming conflict 2026-08-07 18:32:41 -07:00
TakaRikka 82e2fd4bea Merge pull request #2283 from encounter/camera-operator
Add camera operator API to CameraService
2026-08-07 03:44:59 -07:00
TakaRikka fcd9dd0f70 Merge pull request #2282 from encounter/mods-data-dir
Add HostService.data_dir
2026-08-07 03:42:53 -07:00
TakaRikka 8afe479d0f Merge pull request #2281 from encounter/hook-uninstall
Add mods::hook::uninstall
2026-08-07 03:39:24 -07:00
Luke Street c5933be631 Add camera operator API to CameraService 2026-08-07 00:34:57 -06:00
Luke Street 928e9b4bc7 Add HostService.data_dir 2026-08-07 00:20:09 -06:00
Luke Street 3ac0fcb156 Add mods::hook::uninstall 2026-08-06 23:59:26 -06:00
gymnast86 6481d12a3d add lantern, midna, and heart colors 2026-08-06 09:38:00 -07:00
TakaRikka 2c7ab755a0 Merge pull request #2276 from encounter/align
Fix ATTRIBUTE_ALIGN placement
2026-08-06 08:58:15 -07:00
Luke Street cf8ab52f55 Fix ATTRIBUTE_ALIGN placement 2026-08-05 21:16:06 -06:00
gymnast86 960d016113 get most colors working 2026-08-05 08:01:57 -07:00
Luke Street 13b3b68fe5 Migrate to Borealis (#2266) 2026-08-05 07:54:31 -06:00
TakaRikka 4604304305 mods: stage service (#2268)
* initial stage service ported from encounter's original impl

* Review cleanup

* initial stage service ported from encounter's original impl

---------

Co-authored-by: Luke Street <luke@street.dev>
2026-08-04 21:51:52 -06:00
Pwootage 794bb130e2 Fix for Linux distributions that use lib64: force libturbojpeg to use 'lib' (#2269) 2026-08-04 21:49:33 -06:00
gymnast86 e02251a669 template setup 2026-08-04 15:28:11 -07:00
Pieter-Jan Briers 2c9f814ec6 Make JASCalc memory helpers just use memcpy/memset (#2258) 2026-08-03 21:37:36 -06:00
TakaRikka 76079b6294 mods: save service (#2256)
* initial save service updated from encounter's impl

* Review cleanup

---------

Co-authored-by: Luke Street <luke@street.dev>
2026-08-03 20:58:09 -06:00
Pieter-Jan Briers 519305b7d4 Linux/Apple compile warnings consistency (#2255)
This re-organizes the CMakeLists.txt a lil so that some of the warning compile flags on Linux also get applied to Apple platforms.
2026-08-03 19:31:03 -06:00
211 changed files with 4845 additions and 14463 deletions
+4 -4
View File
@@ -76,7 +76,7 @@ jobs:
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: dusklight-${{env.DUSK_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}}
name: dusklight-${{env.APP_VERSION}}-linux-${{matrix.preset}}-${{matrix.artifact_arch}}
path: |
build/install/Dusklight-*.AppImage
build/install/debug.tar.*
@@ -145,7 +145,7 @@ jobs:
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: dusklight-${{env.DUSK_VERSION}}-${{matrix.artifact_name}}
name: dusklight-${{env.APP_VERSION}}-${{matrix.artifact_name}}
path: |
build/install/Dusklight.app
build/install/debug.tar.*
@@ -221,7 +221,7 @@ jobs:
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: dusklight-${{env.DUSK_VERSION}}-android-${{matrix.artifact_arch}}
name: dusklight-${{env.APP_VERSION}}-android-${{matrix.artifact_arch}}
path: upload/
build-windows:
@@ -283,7 +283,7 @@ jobs:
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: dusklight-${{env.DUSK_VERSION}}-win32-msvc-${{matrix.artifact_arch}}
name: dusklight-${{env.APP_VERSION}}-win32-msvc-${{matrix.artifact_arch}}
path: |
build/install/*.exe
build/install/*.dll
+3
View File
@@ -1,3 +1,6 @@
[submodule "extern/aurora"]
path = extern/aurora
url = https://github.com/encounter/aurora.git
[submodule "extern/borealis"]
path = extern/borealis
url = https://github.com/encounter/borealis.git
+34 -140
View File
@@ -5,10 +5,11 @@ if (NOT CMAKE_BUILD_TYPE)
"Build type options: Debug Release RelWithDebInfo MinSizeRel" FORCE)
endif ()
include(cmake/DetectVersion.cmake)
detect_version()
include(extern/borealis/cmake/DetectVersion.cmake)
borealis_detect_version()
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
project(dusklight LANGUAGES C CXX VERSION ${DUSK_VERSION_STRING})
project(dusklight LANGUAGES C CXX VERSION ${BOREALIS_APP_VERSION})
if (APPLE)
enable_language(OBJC OBJCXX)
endif ()
@@ -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
View File
@@ -28,12 +28,12 @@
"cacheVariables": {
"CMAKE_C_COMPILER_LAUNCHER": "sccache",
"CMAKE_CXX_COMPILER_LAUNCHER": "sccache",
"DUSK_ENABLE_SENTRY_NATIVE": {
"BOREALIS_ENABLE_SENTRY": {
"type": "BOOL",
"value": true
},
"DUSK_SENTRY_DSN": "$env{SENTRY_DSN}",
"DUSK_SENTRY_ENVIRONMENT": "production",
"BOREALIS_SENTRY_DSN": "$env{SENTRY_DSN}",
"BOREALIS_SENTRY_ENVIRONMENT": "production",
"Rust_RUSTUP_INSTALL_MISSING_TARGET": {
"type": "BOOL",
"value": true
@@ -448,7 +448,7 @@
"ci"
],
"cacheVariables": {
"DUSK_ENABLE_SENTRY_NATIVE": {
"BOREALIS_ENABLE_SENTRY": {
"type": "BOOL",
"value": false
}
+1 -1
View File
@@ -23,5 +23,5 @@ cp -r platforms/freedesktop/{16x16,32x32,48x48,64x64,128x128,256x256,512x512,102
cp platforms/freedesktop/dev.twilitrealm.dusk.desktop build/appdir/usr/share/applications
cd build/install
VERSION="$DUSK_VERSION" NO_STRIP=1 "$linuxdeploy" \
VERSION="$APP_VERSION" NO_STRIP=1 "$linuxdeploy" \
-l "$lib_dir/libusb-1.0.so" --appdir "$build_dir/appdir" --output appimage
-121
View File
@@ -1,121 +0,0 @@
# Version detection shared by the main build and the mod SDK (sdk/CMakeLists.txt)
include_guard(GLOBAL)
get_filename_component(_DUSK_VERSION_ROOT "${CMAKE_CURRENT_LIST_DIR}/.." ABSOLUTE)
set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN")
set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment")
set(DUSK_VERSION_OVERRIDE "" CACHE STRING "Override version string (skips git detection and format validation)")
macro(detect_version)
if (DUSK_VERSION_OVERRIDE)
set(DUSK_WC_DESCRIBE "${DUSK_VERSION_OVERRIDE}")
set(DUSK_VERSION_STRING "0.0.0.0")
set(DUSK_SHORT_VERSION_STRING "0.0.0")
set(DUSK_VERSION_CODE "1")
set(DUSK_WC_REVISION "")
set(DUSK_WC_BRANCH "")
set(DUSK_WC_DATE "")
message(STATUS "Dusklight version overridden to ${DUSK_WC_DESCRIBE}")
else ()
# obtain revision info from git
find_package(Git)
if (GIT_FOUND)
# make sure version information gets re-run when the current Git HEAD changes
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --git-path HEAD
OUTPUT_VARIABLE dusk_git_head_filename
OUTPUT_STRIP_TRAILING_WHITESPACE)
get_filename_component(dusk_git_head_filename "${dusk_git_head_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_filename}")
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --symbolic-full-name HEAD
OUTPUT_VARIABLE dusk_git_head_symbolic
OUTPUT_STRIP_TRAILING_WHITESPACE)
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT}
COMMAND ${GIT_EXECUTABLE} rev-parse --git-path ${dusk_git_head_symbolic}
OUTPUT_VARIABLE dusk_git_head_symbolic_filename
OUTPUT_STRIP_TRAILING_WHITESPACE)
get_filename_component(dusk_git_head_symbolic_filename "${dusk_git_head_symbolic_filename}" ABSOLUTE BASE_DIR "${_DUSK_VERSION_ROOT}")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${dusk_git_head_symbolic_filename}")
# defines DUSK_WC_REVISION
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse HEAD
OUTPUT_VARIABLE DUSK_WC_REVISION
OUTPUT_STRIP_TRAILING_WHITESPACE)
# defines DUSK_WC_DESCRIBE
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} describe --tags --long --dirty --match "v*"
OUTPUT_VARIABLE DUSK_WC_DESCRIBE
OUTPUT_STRIP_TRAILING_WHITESPACE)
# remove the git hash, then collapse a clean "-0" suffix only
string(REGEX REPLACE "-[^-]+(-dirty|)$" "\\1" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
string(REGEX REPLACE "-0$" "" DUSK_WC_DESCRIBE "${DUSK_WC_DESCRIBE}")
# defines DUSK_WC_BRANCH
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} rev-parse --abbrev-ref HEAD
OUTPUT_VARIABLE DUSK_WC_BRANCH
OUTPUT_STRIP_TRAILING_WHITESPACE)
# defines DUSK_WC_DATE
execute_process(WORKING_DIRECTORY ${_DUSK_VERSION_ROOT} COMMAND ${GIT_EXECUTABLE} log -1 --format=%ad
OUTPUT_VARIABLE DUSK_WC_DATE
OUTPUT_STRIP_TRAILING_WHITESPACE)
else ()
message(STATUS "Unable to find git, commit information will not be available")
endif ()
if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)([-+].*)?$")
set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
set(_ver_major ${CMAKE_MATCH_1})
set(_ver_minor ${CMAKE_MATCH_2})
set(_ver_patch ${CMAKE_MATCH_3})
set(DUSK_VERSION_TWEAK "0")
if (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-([0-9]+)(-dirty)?$")
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
elseif (DUSK_WC_DESCRIBE MATCHES "^v[0-9]+\\.[0-9]+\\.[0-9]+-[0-9A-Za-z.-]+-([0-9]+)(-dirty)?$")
set(DUSK_VERSION_TWEAK "${CMAKE_MATCH_1}")
endif ()
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${DUSK_VERSION_TWEAK}")
if (DUSK_VERSION_TWEAK GREATER 999)
set(_tweak 999)
else ()
set(_tweak ${DUSK_VERSION_TWEAK})
endif ()
# encoding: major*1e7 + minor*1e5 + patch*1e3 + tweak; collision-free for major<210, minor<100, patch<100, tweak<=999
math(EXPR DUSK_VERSION_CODE
"${_ver_major} * 10000000 + ${_ver_minor} * 100000 + ${_ver_patch} * 1000 + ${_tweak}")
else ()
set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION")
set(DUSK_VERSION_STRING "0.0.0.0")
set(DUSK_SHORT_VERSION_STRING "0.0.0")
set(DUSK_VERSION_CODE "1")
endif ()
endif ()
# Add version information to CI environment variables
if (DEFINED ENV{GITHUB_ENV})
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION=${DUSK_WC_DESCRIBE}\n")
file(APPEND "$ENV{GITHUB_ENV}" "DUSK_VERSION_CODE=${DUSK_VERSION_CODE}\n")
endif ()
message(STATUS "Dusklight version set to ${DUSK_WC_DESCRIBE}")
endmacro()
# Sets PLATFORM_NAME and configures version.h into the caller's binary dir.
macro(configure_version_header)
if (CMAKE_SYSTEM_NAME STREQUAL Windows)
set(PLATFORM_NAME win32)
elseif (CMAKE_SYSTEM_NAME STREQUAL Darwin)
if (IOS)
set(PLATFORM_NAME ios)
elseif (TVOS)
set(PLATFORM_NAME tvos)
else ()
set(PLATFORM_NAME macos)
endif ()
else ()
string(TOLOWER CMAKE_SYSTEM_NAME PLATFORM_NAME)
endif ()
configure_file(${_DUSK_VERSION_ROOT}/version.h.in ${CMAKE_CURRENT_BINARY_DIR}/version.h)
endmacro()
+101 -8
View File
@@ -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.
+1 -1
Vendored Submodule
+1
Submodule extern/borealis added at 6fd955e7e6
+7 -18
View File
@@ -1421,30 +1421,21 @@ set(DUSK_FILES
src/dusk/achievements.cpp
src/dusk/action_bindings.cpp
src/dusk/action_bindings.h
src/dusk/android_frame_rate.cpp
src/dusk/android_frame_rate.hpp
src/dusk/asserts.cpp
src/dusk/autosave.cpp
src/dusk/config.cpp
src/dusk/config.hpp
src/dusk/crash_handler.cpp
src/dusk/crash_reporting.cpp
src/dusk/data.cpp
src/dusk/data.hpp
src/dusk/discord.cpp
src/dusk/discord.hpp
src/dusk/discord_presence.cpp
src/dusk/dvd_asset.cpp
src/dusk/dvd_asset.hpp
src/dusk/extras.c
src/dusk/file_select.cpp
src/dusk/file_select.hpp
src/dusk/frame_interpolation.cpp
src/dusk/game_clock.cpp
src/dusk/gamepad_color.cpp
src/dusk/globals.cpp
src/dusk/gyro.cpp
src/dusk/http/http.hpp
src/dusk/imgui/ImGuiActorSpawner.cpp
src/dusk/imgui/ImGuiBloomWindow.cpp
src/dusk/imgui/ImGuiBloomWindow.hpp
@@ -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
)
+1 -1
View File
@@ -228,7 +228,7 @@
ninjaFlags = [ "dusklight" ];
cmakeFlags = [
"-DDUSK_VERSION_OVERRIDE=${versionSuffix}"
"-DBOREALIS_APP_VERSION_OVERRIDE=${versionSuffix}"
"-DFETCHCONTENT_FULLY_DISCONNECTED=ON"
"-DAURORA_DAWN_PROVIDER=package"
"-DAURORA_DAWN_LINKAGE=static"
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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];
+1 -29
View File
@@ -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 {
+1 -1
View File
@@ -30,7 +30,7 @@ public:
void drawSelf() { mpKanteraIcon->draw(); }
private:
// private:
/* 0x04 */ dDlst_KanteraIcon_c* mpKanteraIcon;
/* 0x08 */ CPaneMgr* mpParent;
/* 0x0C */ CPaneMgr* mpGauge;
+1 -44
View File
@@ -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;
+1 -1
View File
@@ -43,7 +43,7 @@ public:
u8 getStatus() { return mStatus; }
private:
// private:
/* 0x004 */ JKRExpHeap* mpHeap;
/* 0x008 */ JKRArchive* mpArchive;
/* 0x00C */ STControl* mpStick;
+1 -1
View File
@@ -270,7 +270,7 @@ public:
static DUSK_GAME_DATA dMenu_Fmap_c* MyClass;
private:
// private:
/* 0x004 */ JKRExpHeap* mpHeap;
/* 0x008 */ JKRExpHeap* mpTalkHeap;
/* 0x00C */ STControl* mpStick;
+1 -1
View File
@@ -60,7 +60,7 @@ public:
u8 getStatus() { return mStatus; }
private:
// private:
/* 0x04 */ JKRExpHeap* mpHeap;
/* 0x08 */ JKRArchive* mpArchive;
/* 0x0C */ STControl* mpStick;
+1 -1
View File
@@ -64,7 +64,7 @@ public:
u8 getStatus() { return mStatus; }
private:
// private:
/* 0x004 */ JKRExpHeap* mpHeap;
/* 0x008 */ JKRArchive* mpArchive;
/* 0x00C */ STControl* mpStick;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -58,7 +58,7 @@ public:
u8 getStatus() { return mStatus; }
private:
// private:
/* 0x004 */ JKRExpHeap* mpHeap;
/* 0x008 */ JKRArchive* mpArchive;
/* 0x00C */ STControl* mpStick;
+1 -1
View File
@@ -154,7 +154,7 @@ public:
}
#endif
private:
// private:
/* 0x004 */ item_params mItemParams[4];
/* 0x074 */ JKRExpHeap* heap;
/* 0x078 */ J2DScreen* mpScreen;
+2 -2
View File
@@ -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;
-28
View File
@@ -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();
+3 -3
View File
@@ -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*);
+21 -5
View File
@@ -2,6 +2,7 @@
#define JASCALC_H
#include <types.h>
#include <cstring>
#include <limits>
/**
@@ -10,16 +11,31 @@
*/
struct JASCalc {
static void imixcopy(const s16*, const s16*, s16*, u32);
static void bcopyfast(const void* src, void* dest, u32 size);
#if TARGET_PC
static void bcopyfast(const void* src, void* dest, u32 size) {
std::memcpy(dest, src, size);
}
#if TARGET_ANDROID
static void _bcopy(const void* src, void* dest, u32 size);
static void _bcopy(const void* src, void* dest, u32 size) {
#else
static void bcopy(const void* src, void* dest, u32 size);
static void bcopy(const void* src, void* dest, u32 size) {
#endif
static void bzerofast(void* dest, u32 size);
std::memcpy(dest, src, size);
}
static void bzerofast(void* dest, u32 size) {
std::memset(dest, 0, size);
}
#if TARGET_ANDROID
static void _bzero(void* dest, u32 size);
static void _bzero(void* dest, u32 size) {
#else
static void bzero(void* dest, u32 size) {
#endif
std::memset(dest, 0, size);
}
#else
static void bcopyfast(const void* src, void* dest, u32 size);
static void bcopy(const void* src, void* dest, u32 size);
static void bzerofast(void* dest, u32 size);
static void bzero(void* dest, u32 size);
#endif
static f32 pow2(f32);
+2 -2
View File
@@ -23,9 +23,9 @@ DUSK_GAME_DATA Vec J3DSys::mParentS;
DUSK_GAME_DATA J3DTexCoordScaleInfo J3DSys::sTexCoordScaleTable[8];
#if TARGET_PC // Original game bug, array is too small.
static u8 NullTexData[0x20] ATTRIBUTE_ALIGN(32) = {0};
ATTRIBUTE_ALIGN(32) static u8 NullTexData[0x20] = {0};
#else
static u8 NullTexData[0x10] ATTRIBUTE_ALIGN(32) = {0};
ATTRIBUTE_ALIGN(32) static u8 NullTexData[0x10] = {0};
#endif
static Mtx j3dIdentityMtx = {
+2 -8
View File
@@ -11,6 +11,7 @@ void JASCalc::imixcopy(const s16* s1, const s16* s2, s16* dst, u32 n) {
}
}
#if !TARGET_PC
void JASCalc::bcopyfast(const void* src, void* dest, u32 size) {
JUT_ASSERT(226, (reinterpret_cast<uintptr_t>(src) & 0x03) == 0);
JUT_ASSERT(227, (reinterpret_cast<uintptr_t>(dest) & 0x03) == 0);
@@ -33,11 +34,7 @@ void JASCalc::bcopyfast(const void* src, void* dest, u32 size) {
}
}
#if TARGET_ANDROID
void JASCalc::_bcopy(const void* src, void* dest, u32 size) {
#else
void JASCalc::bcopy(const void* src, void* dest, u32 size) {
#endif
u32* usrc;
u32* udest;
@@ -94,11 +91,7 @@ void JASCalc::bzerofast(void* dest, u32 size) {
}
}
#if TARGET_ANDROID
void JASCalc::_bzero(void* dest, u32 size) {
#else
void JASCalc::bzero(void* dest, u32 size) {
#endif
u32* udest;
u8* bdest = (u8*)dest;
if ((size & 0x1f) == 0 && (reinterpret_cast<uintptr_t>(dest) & 0x1f) == 0) {
@@ -139,6 +132,7 @@ void JASCalc::bzero(void* dest, u32 size) {
}
}
}
#endif
#if AVOID_UB
DUSK_GAME_DATA s16 const JASCalc::CUTOFF_TO_IIR_TABLE[129][4] = {
+2 -2
View File
@@ -99,14 +99,14 @@ void JASDsp::invalChannelAll() {
DCInvalidateRange(CH_BUF, sizeof(TChannel) * DSP_CHANNELS);
}
DUSK_GAME_DATA u8 const ATTRIBUTE_ALIGN(32) JASDsp::DSPADPCM_FILTER[64] = {
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA u8 const JASDsp::DSPADPCM_FILTER[64] = {
0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x04, 0x00, 0x04, 0x00,
0x10, 0x00, 0xF8, 0x00, 0x0E, 0x00, 0xFA, 0x00, 0x0C, 0x00, 0xFC, 0x00, 0x12, 0x00, 0xF6, 0x00,
0x10, 0x68, 0xF7, 0x38, 0x12, 0xC0, 0xF7, 0x04, 0x14, 0x00, 0xF4, 0x00, 0x08, 0x00, 0xF8, 0x00,
0x04, 0x00, 0xFC, 0x00, 0xFC, 0x00, 0x04, 0x00, 0xFC, 0x00, 0x00, 0x00, 0xF8, 0x00, 0x00, 0x00,
};
DUSK_GAME_DATA u32 const ATTRIBUTE_ALIGN(32) JASDsp::DSPRES_FILTER[320] = {
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA u32 const JASDsp::DSPRES_FILTER[320] = {
0x0C3966AD,
0x0D46FFDF,
0x0B396696,
+3 -3
View File
@@ -25,7 +25,7 @@ void DspHandShake(void*) {
Dsp_Running_Start();
}
static u8 jdsp[7936] ATTRIBUTE_ALIGN(32) = {
ATTRIBUTE_ALIGN(32) static u8 jdsp[7936] = {
0x02, 0x9F, 0x00, 0x12, 0x00, 0x00, 0x00, 0x00, 0x02, 0xFF, 0x00, 0x00, 0x02, 0xFF, 0x00, 0x00,
0x02, 0xFF, 0x00, 0x00, 0x02, 0xFF, 0x00, 0x00, 0x02, 0xFF, 0x00, 0x00, 0x02, 0x9F, 0x06, 0xA5,
0x02, 0x9F, 0x00, 0x4E, 0x12, 0x05, 0x02, 0xBF, 0x00, 0x57, 0x81, 0x00, 0x00, 0x9F, 0x10, 0x00,
@@ -524,9 +524,9 @@ static u8 jdsp[7936] ATTRIBUTE_ALIGN(32) = {
0x80, 0x01, 0x02, 0xBF, 0x00, 0xF4, 0x02, 0xDF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
};
static DSPTaskInfo audio_task ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static DSPTaskInfo audio_task;
static u8 AUDIO_YIELD_BUFFER[8192] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static u8 AUDIO_YIELD_BUFFER[8192];
void DspBoot(void (*requestCallback)(void*)) {
DspInitWork();
+2 -2
View File
@@ -452,13 +452,13 @@ static void dummy() {
JUTXfb::getManager()->setDisplayingXfbIndex(0);
}
static Mtx e_mtx ATTRIBUTE_ALIGN(32) = {
ATTRIBUTE_ALIGN(32) static Mtx e_mtx = {
{1.0f, 0.0f, 0.0f, 0.0f},
{0.0f, 1.0f, 0.0f, 0.0f},
{0.0f, 0.0f, 1.0f, 0.0f},
};
static u8 clear_z_TX[64] ATTRIBUTE_ALIGN(32) = {
ATTRIBUTE_ALIGN(32) static u8 clear_z_TX[64] = {
0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF,
0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+3 -3
View File
@@ -16,10 +16,10 @@ inline f64 getConst2() {
return 9.765625E-4;
}
DUSK_GAME_DATA TSinCosTable<13, f32> sincosTable_ ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA TSinCosTable<13, f32> sincosTable_;
DUSK_GAME_DATA TAtanTable<1024, f32> atanTable_ ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA TAtanTable<1024, f32> atanTable_;
DUSK_GAME_DATA TAsinAcosTable<1024, f32> asinAcosTable_ ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA TAsinAcosTable<1024, f32> asinAcosTable_;
} // namespace JMath
+2 -2
View File
@@ -46,7 +46,7 @@ JPAResource::JPAResource() {
mUsrIdx = fldNum = keyNum = texNum = mpCalcEmitterFuncListNum = mpDrawEmitterFuncListNum = mpDrawEmitterChildFuncListNum = mpCalcParticleFuncListNum = mpDrawParticleFuncListNum = mpCalcParticleChildFuncListNum = mpDrawParticleChildFuncListNum = 0;
}
static u8 jpa_pos[324] ATTRIBUTE_ALIGN(32) = {
ATTRIBUTE_ALIGN(32) static u8 jpa_pos[324] = {
0x00, 0x00, 0x00, 0x32, 0x00, 0x00, 0x32, 0xCE, 0x00, 0x00, 0xCE, 0x00, 0xE7, 0x00, 0x00, 0x19,
0x00, 0x00, 0x19, 0xCE, 0x00, 0xE7, 0xCE, 0x00, 0xCE, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xCE,
0x00, 0xCE, 0xCE, 0x00, 0x00, 0x19, 0x00, 0x32, 0x19, 0x00, 0x32, 0xE7, 0x00, 0x00, 0xE7, 0x00,
@@ -70,7 +70,7 @@ static u8 jpa_pos[324] ATTRIBUTE_ALIGN(32) = {
0x00, 0x00, 0x00, 0xCE,
};
static u8 jpa_crd[32] ATTRIBUTE_ALIGN(32) = {
ATTRIBUTE_ALIGN(32) static u8 jpa_crd[32] = {
0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x00, 0x02, 0x01, 0x00, 0x01,
0x00, 0x00, 0x01, 0x00, 0x01, 0x02, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x02, 0x02, 0x00, 0x02,
};
@@ -8,7 +8,7 @@
#endif
#include "global.h"
DUSK_GAME_DATA u8 const JUTResFONT_Ascfont_fix12[] ATTRIBUTE_ALIGN(32) = {
ATTRIBUTE_ALIGN(32) DUSK_GAME_DATA u8 const JUTResFONT_Ascfont_fix12[] = {
0x46, 0x4F, 0x4E, 0x54, 0x62, 0x66, 0x6E, 0x31, 0x00, 0x00, 0x41, 0x60, 0x00, 0x00, 0x00, 0x04,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x49, 0x4E, 0x46, 0x31, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x0C,
@@ -136,7 +136,7 @@ void TRK__read_aram(__REGISTER int c, __REGISTER u32 p2, void* p3) {
}
void TRK__write_aram(__REGISTER int c, __REGISTER u32 p2, void* p3) {
u8 buff[32] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) u8 buff[32];
u32 err;
__REGISTER int count = c;
__REGISTER u32 bf;
@@ -85,7 +85,7 @@ DSError TRKDoSupportMask(TRKBuffer*) {
}
DSError TRKDoReadMemory(TRKBuffer* buffer) {
u8 buf[0x820] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) u8 buf[0x820];
size_t tempLength;
int result;
int replyErr;
@@ -158,7 +158,7 @@ DSError TRKDoReadMemory(TRKBuffer* buffer) {
}
DSError TRKDoWriteMemory(TRKBuffer* b) {
u8 buf[0x820] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) u8 buf[0x820];
size_t tempLength;
int options;
int result;
+2 -2
View File
@@ -3,8 +3,8 @@
#include "__ax.h"
static s32 __AXBufferAuxA[3][480] ATTRIBUTE_ALIGN(32);
static s32 __AXBufferAuxB[3][480] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static s32 __AXBufferAuxA[3][480];
ATTRIBUTE_ALIGN(32) static s32 __AXBufferAuxB[3][480];
static void (* __AXCallbackAuxA)(void*, void*);
static void (* __AXCallbackAuxB)(void*, void*);
+1 -1
View File
@@ -3,7 +3,7 @@
#include "__ax.h"
static AXSPB __AXStudio ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static AXSPB __AXStudio;
static s32 __AXSpbAL;
static s32 __AXSpbAR;
+3 -3
View File
@@ -35,9 +35,9 @@ static u32 __AXAuxMixCycles[32] = {
0x000009E2, 0x00000E97
};
static AXPB __AXPB[AX_MAX_VOICES] ATTRIBUTE_ALIGN(32);
static AXPBITDBUFFER __AXITD[AX_MAX_VOICES] ATTRIBUTE_ALIGN(32);
static AXPBU __AXUpdates[AX_MAX_VOICES] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static AXPB __AXPB[AX_MAX_VOICES];
ATTRIBUTE_ALIGN(32) static AXPBITDBUFFER __AXITD[AX_MAX_VOICES];
ATTRIBUTE_ALIGN(32) static AXPBU __AXUpdates[AX_MAX_VOICES];
static AXVPB __AXVPB[AX_MAX_VOICES];
static u32 __AXMaxDspCycles;
+1 -1
View File
@@ -3,7 +3,7 @@
u16 axDspSlaveLength = (AX_DSP_SLAVE_LENGTH * 2);
u16 axDspSlave[AX_DSP_SLAVE_LENGTH] ATTRIBUTE_ALIGN(32) = {
ATTRIBUTE_ALIGN(32) u16 axDspSlave[AX_DSP_SLAVE_LENGTH] = {
0x0000, 0x0000, 0x029F, 0x0E88, 0x029F, 0x0E97,
0x029F, 0x0EB3, 0x029F, 0x0ED3, 0x029F, 0x0ED9,
0x029F, 0x0F0B, 0x029F, 0x0F11, 0x1302, 0x1303,
+1 -1
View File
@@ -4,7 +4,7 @@
#include "__card.h"
static u8 CardData[352] ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) = {
ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) static u8 CardData[352] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x02, 0xFF, 0x00, 0x21, 0x13, 0x06, 0x12, 0x03, 0x12, 0x04,
0x13, 0x05, 0x00, 0x92, 0x00, 0xFF, 0x00, 0x88, 0xFF, 0xFF, 0x00, 0x89, 0xFF, 0xFF, 0x00, 0x8A, 0xFF, 0xFF, 0x00,
+1 -1
View File
@@ -3,7 +3,7 @@
#include <dolphin/demo.h>
#include "sdk_math.h"
static s16 __AVX_internal_buffer[3200] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static s16 __AVX_internal_buffer[3200];
static void (*__AVX_save_isr)(void);
+1 -1
View File
@@ -1,7 +1,7 @@
#include <dolphin/dolphin.h>
#include <dolphin/demo.h>
u32 DEMOFontBitmap[768] ATTRIBUTE_ALIGN(32) = {
ATTRIBUTE_ALIGN(32) u32 DEMOFontBitmap[768] = {
0x00000000,
0x00000000,
0x00000000,
+1 -1
View File
@@ -43,7 +43,7 @@ void* __piReg;
GXBool __GXinBegin;
#endif
static u16 DefaultTexData[] ATTRIBUTE_ALIGN(32) = {
ATTRIBUTE_ALIGN(32) static u16 DefaultTexData[] = {
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
};
+5 -5
View File
@@ -7,11 +7,11 @@ const char* __MCCVersion = "<< Dolphin SDK - MCC\tdebug build: Apr 5 2004 03:57
const char* __MCCVersion = "<< Dolphin SDK - MCC\trelease build: Apr 5 2004 04:15:49 (0x2301) >>";
#endif
static MCC_ChannelInfo gChannelInfo[16] ATTRIBUTE_ALIGN(32);
static char gStreamWork[32] ATTRIBUTE_ALIGN(32);
static char m_szAdapterMode[32] ATTRIBUTE_ALIGN(32);
static char m_szInitCode[32] ATTRIBUTE_ALIGN(32);
static MCC_Info channelInfo[16] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static MCC_ChannelInfo gChannelInfo[16];
ATTRIBUTE_ALIGN(32) static char gStreamWork[32];
ATTRIBUTE_ALIGN(32) static char m_szAdapterMode[32];
ATTRIBUTE_ALIGN(32) static char m_szInitCode[32];
ATTRIBUTE_ALIGN(32) static MCC_Info channelInfo[16];
volatile static BOOL gIsChannelinfoDirty = TRUE;
+1 -1
View File
@@ -3,7 +3,7 @@
#include "__os.h"
static SramControl Scb ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT);
ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) static SramControl Scb;
// prototypes
static int GetRTC(u32* rtc);
+1 -1
View File
@@ -3,7 +3,7 @@
#include "__card.h"
static u8 CardData[352] ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) = {
ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) static u8 CardData[352] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0x02, 0xFF, 0x00, 0x21, 0x13, 0x06, 0x12, 0x03, 0x12, 0x04,
0x13, 0x05, 0x00, 0x92, 0x00, 0xFF, 0x00, 0x88, 0xFF, 0xFF, 0x00, 0x89, 0xFF, 0xFF, 0x00, 0x8A, 0xFF, 0xFF, 0x00,
+5 -5
View File
@@ -44,7 +44,7 @@ static u32 LastError;
static BOOL ResetRequired;
static u32 MotorState;
static volatile OSTime LastResetEnd;
static u32 __DVDNumTmdBytes ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static u32 __DVDNumTmdBytes;
static DVDGameTOC* GameToc;
static DVDPartitionInfo* PartInfo;
static DVDPartitionInfo* BootGameInfo;
@@ -80,10 +80,10 @@ static DVDCommandBlock DummyCommandBlock;
static OSAlarm ResetAlarm;
static OSAlarm CoverAlarm;
static u8 __DVDGameTocBuffer[OSRoundUp32B(sizeof(DVDGameTOC) * 4)] ATTRIBUTE_ALIGN(32);
static u8 __DVDPartInfoBuffer[OSRoundUp32B(sizeof(DVDPartitionInfo) * 4)] ATTRIBUTE_ALIGN(32);
static u8 __DVDTmdBuffer[OSRoundUp32B(sizeof(ESTitleMeta))] ATTRIBUTE_ALIGN(32);
static u8 __DVDTicketViewBuffer[OSRoundUp32B(sizeof(ESTicketView))] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static u8 __DVDGameTocBuffer[OSRoundUp32B(sizeof(DVDGameTOC) * 4)];
ATTRIBUTE_ALIGN(32) static u8 __DVDPartInfoBuffer[OSRoundUp32B(sizeof(DVDPartitionInfo) * 4)];
ATTRIBUTE_ALIGN(32) static u8 __DVDTmdBuffer[OSRoundUp32B(sizeof(ESTitleMeta))];
ATTRIBUTE_ALIGN(32) static u8 __DVDTicketViewBuffer[OSRoundUp32B(sizeof(ESTicketView))];
static OSAlarm FatalAlarm;
DVDCommandBlock __DVDStopMotorCommandBlock;
+1 -1
View File
@@ -5,7 +5,7 @@
#include "__os.h"
#include "__dvd.h"
static u8 CheckBuffer[32] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static u8 CheckBuffer[32];
static volatile BOOL lowDone = TRUE;
static volatile u32 lowIntType = 0;
+7 -7
View File
@@ -8,15 +8,15 @@ static volatile u8 requestInProgress = FALSE;
static u8 breakRequested;
static u8 callbackInProgress;
static u32 registerBuf[8] ATTRIBUTE_ALIGN(32);
static u32 statusRegister[8] ATTRIBUTE_ALIGN(32);
static u32 controlRegister[8] ATTRIBUTE_ALIGN(32);
static s32 lastTicketError[8] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static u32 registerBuf[8];
ATTRIBUTE_ALIGN(32) static u32 statusRegister[8];
ATTRIBUTE_ALIGN(32) static u32 controlRegister[8];
ATTRIBUTE_ALIGN(32) static s32 lastTicketError[8];
static u32 readLength;
static u32 spinUpValue;
static diRegVals_t diRegValCache ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static diRegVals_t diRegValCache;
static u8 DVDLowInitCalled = FALSE;
@@ -42,8 +42,8 @@ typedef struct dvdContext {
static int freeDvdContext = 0;
static u8 dvdContextsInited = FALSE;
static dvdContext_t dvdContexts[4] ATTRIBUTE_ALIGN(32);
static IOSIoVector ioVec[10] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static dvdContext_t dvdContexts[4];
ATTRIBUTE_ALIGN(32) static IOSIoVector ioVec[10];
static void* ddrAllocAligned32(const int size) {
void* low, *high;
+2 -2
View File
@@ -11,8 +11,8 @@ static NANDCommandBlock NandCb;
static NANDFileInfo NandInfo;
static DVDCBCallback Callback;
static u32 NextOffset;
DVDErrorInfo __ErrorInfo ATTRIBUTE_ALIGN(32);
DVDErrorInfo __FirstErrorInfo ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) DVDErrorInfo __ErrorInfo;
ATTRIBUTE_ALIGN(32) DVDErrorInfo __FirstErrorInfo;
void cbForNandClose(s32 result, NANDCommandBlock* block) {
if (Callback) {
+8 -8
View File
@@ -33,7 +33,7 @@ s32 ESP_CloseLib(void) {
s32 ESP_LaunchTitle(u64 titleID, ESTicketView* pTicketView) {
s32 ret = 0;
u8 buf[256] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) u8 buf[256];
IOSIoVector* vec = (IOSIoVector*)(buf + 208);
u64* id = (u64*)buf;
@@ -63,7 +63,7 @@ out:
s32 ESP_GetTicketViews(ESTitleId titleId, ESTicketView* ticketViewList, u32* ticketViewCnt) {
s32 rv = 0;
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
ESTitleId* p1 = (ESTitleId*)__esBuf;
u32* p2 = (u32*)(__esBuf + 32);
@@ -111,7 +111,7 @@ out:
s32 ESP_DiGetTicketView(const void* ticket, ESTicketView* ticketView) {
s32 rv = 0;
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
if (__esFd < 0 || ticketView == NULL) {
@@ -142,7 +142,7 @@ out:
s32 ESP_DiGetTmd(ESTitleMeta* tmd, u32* tmdSize) {
s32 rv = 0;
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
u32* p1 = (u32*)__esBuf;
@@ -184,7 +184,7 @@ out:
s32 ESP_GetTmdView(ESTitleId titleId, ESTmdView* tmdView, u32* size) {
s32 rv = 0;
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
ESTitleId* p1 = (ESTitleId*)__esBuf;
u32* p2 = (u32*)(__esBuf + 32);
@@ -233,7 +233,7 @@ out:
s32 ESP_GetDataDir(ESTitleId titleId, char* dataDir) {
s32 rv = 0;
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
ESTitleId* p1 = (ESTitleId*)__esBuf;
@@ -260,7 +260,7 @@ out:
s32 ESP_GetTitleId(ESTitleId* titleId) {
s32 rv = 0;
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
if (__esFd < 0 || titleId == NULL) {
@@ -283,7 +283,7 @@ out:
s32 ESP_GetConsumption(ESTicketId ticketId, ESLpEntry* entries, u32* nEntries) {
s32 rv = 0;
u8 __esBuf[256] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) u8 __esBuf[256];
IOSIoVector* v = (IOSIoVector*)(__esBuf + 256 - 6 * sizeof(IOSIoVector));
ESTicketId* p1 = (ESTicketId*)__esBuf;
u32* p2 = (u32*)(__esBuf + 32);
+1 -1
View File
@@ -25,7 +25,7 @@ typedef struct isfs_GetUsage {
} isfs_GetUsage;
typedef struct __isfsCtxt {
u8 ioBuf[ROUNDUP(256)] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) u8 ioBuf[ROUNDUP(256)];
ISFSCallback cb;
void* ctxt;
u32 func;
+1 -1
View File
@@ -49,7 +49,7 @@ volatile void* __piReg;
GXBool __GXinBegin;
#endif
static u16 DefaultTexData[] ATTRIBUTE_ALIGN(32) = {
ATTRIBUTE_ALIGN(32) static u16 DefaultTexData[] = {
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF,
};
@@ -34,7 +34,7 @@ namespace nw4hbm {
private:
/* 0x00 (base) */
/* 0xD4 */ ut::Color mVtxColors[VERTEXCOLOR_MAX] ATTRIBUTE_ALIGN(4);
/* 0xD4 */ ATTRIBUTE_ALIGN(4) ut::Color mVtxColors[VERTEXCOLOR_MAX];
/* 0xE4 */ detail::TexCoordAry mTexCoordAry;
};
@@ -58,7 +58,7 @@ namespace nw4hbm {
public:
/* 0x14 */ ut::LinkListNode mLinkNode;
static u8 mMramBuf[LOAD_BUFFER_SIZE] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static u8 mMramBuf[LOAD_BUFFER_SIZE];
};
typedef ut::LinkList<LoadCommand, offsetof(LoadCommand, mLinkNode)> LoadCommandList;
+2 -2
View File
@@ -24,7 +24,7 @@ static u32 __relnchFl = 0;
typedef struct IOSRpcRequest {
IOSResourceRequest request;
IOSIpcCb cb ATTRIBUTE_ALIGN(32); // I am assuming this is aligned due to where cbArg is stored, and I see nothing between cb and callback_arg?
ATTRIBUTE_ALIGN(32) IOSIpcCb cb; // I am assuming this is aligned due to where cbArg is stored, and I see nothing between cb and callback_arg?
void* callback_arg;
u32 relaunch_flag;
OSThreadQueue thread_queue;
@@ -37,7 +37,7 @@ static IOSRpcRequest* __relnchRpcSave = 0;
#define ROUNDUP(sz) (((u32)(sz) + (IPC_BUF_CNT / 2 - 1)) & ~(u32)(IPC_BUF_CNT / 2 - 1))
static u8 __rpcBuf[ROUNDUP(sizeof(IOSRpcRequest))] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static u8 __rpcBuf[ROUNDUP(sizeof(IOSRpcRequest))];
static struct {
u32 rcount;
+4 -4
View File
@@ -33,8 +33,8 @@ enum LibState {
};
static enum LibState s_libState = STATE_NOT_INITIALIZED;
static char s_currentDir[64] ATTRIBUTE_ALIGN(32) = "/";
static char s_homeDir[64] ATTRIBUTE_ALIGN(32) = "";
ATTRIBUTE_ALIGN(32) static char s_currentDir[64] = "/";
ATTRIBUTE_ALIGN(32) static char s_homeDir[64] = "";
static BOOL nandOnShutdown(BOOL final, u32 event);
void nandConvertPath(char* abspath, const char* wd, const char* relpath);
@@ -262,7 +262,7 @@ s32 nandConvertErrorCode(const ISFSError err) {
for (; i < sizeof(ERRMAP) / 4; i = i + 2) {
if (ERRMAP[i] == err) {
if (err == ISFS_ERROR_ECC_CRIT || err == ISFS_ERROR_HMAC || err == ISFS_ERROR_UNKNOWN || err == IOS_ERROR_UNKNOWN || err == IOS_ERROR_ECC_CRIT) {
char buf[128] ATTRIBUTE_ALIGN(64);
ATTRIBUTE_ALIGN(64) char buf[128];
sprintf(buf, "ISFS error code: %d", err);
NANDLoggingAddMessageAsync(nandLoggingCallback, err, buf);
}
@@ -279,7 +279,7 @@ s32 nandConvertErrorCode(const ISFSError err) {
OSReport("CAUTION! Unexpected error code [%d] was found.\n", err);
{
char buf[128] ATTRIBUTE_ALIGN(64);
ATTRIBUTE_ALIGN(64) char buf[128];
sprintf(buf, "ISFS unexpected error code: %d", err);
NANDLoggingAddMessageAsync(nandLoggingCallback, err, buf);
}
+3 -3
View File
@@ -7,7 +7,7 @@
static IOSFd s_fd = -255;
static IOSError s_err = ISFS_ERROR_UNKNOWN;
static int s_stage;
static char s_message[256] ATTRIBUTE_ALIGN(64);
ATTRIBUTE_ALIGN(64) static char s_message[256];
static NANDLoggingCallback s_callback = 0;
static void asyncRoutine(ISFSError, void*);
@@ -70,8 +70,8 @@ static void callbackRoutine(BOOL result) {
static void asyncRoutine(ISFSError result, void *ctxt) {
ISFSError ret = ISFS_ERROR_UNKNOWN;
static char s_rBuf[256] ATTRIBUTE_ALIGN(64);
static char s_wBuf[256] ATTRIBUTE_ALIGN(64);
ATTRIBUTE_ALIGN(64) static char s_rBuf[256];
ATTRIBUTE_ALIGN(64) static char s_wBuf[256];
++s_stage;
if (s_stage == 2) {
+1 -1
View File
@@ -293,7 +293,7 @@ s32 NANDMove(const char* path, const char* destDir) {
}
static ISFSError nandGetFileStatus(IOSFd fd, u32* length, u32* pos) {
ISFSFileStats fstat ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) ISFSFileStats fstat;
ISFSError result = ISFS_GetFileStats(fd, &fstat);
if (result == ISFS_ERROR_OK) {
if (length) {
+1 -1
View File
@@ -8,7 +8,7 @@ void __OSRelaunchTitle(u32 resetCode) {
s32 rc = 0;
u32 ticketCnt = 1;
ESTicketView* tik = NULL;
ESTitleId titleId ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) ESTitleId titleId;
__OSPlayTimeType type = OSPLAYTIME_PERMANENT;
u32 remain = 0;
u8* bi2 = NULL;
+1 -1
View File
@@ -85,7 +85,7 @@ NWC24Err NWC24iSynchronizeRtcCounter(BOOL param_0) {
}
NWC24Err NWC24SuspendScheduler(void) {
static u8 susResult[0x20] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static u8 susResult[0x20];
NWC24Err rt = NWC24_OK;
NWC24Err closeRt = NWC24_OK;
IOSFd fd;
+1 -1
View File
@@ -2,7 +2,7 @@
#include <revolution/nand.h>
static BOOL PlayRecordGet = FALSE;
static OSPlayRecord PlayRecord ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static OSPlayRecord PlayRecord;
static NANDFileInfo FileInfo;
static NANDCommandBlock Block;
static s32 PlayRecordState = 9;
+6 -6
View File
@@ -7,7 +7,7 @@
#include "__os.h"
OSAlarm __OSExpireAlarm ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) OSAlarm __OSExpireAlarm;
OSTime __OSExpireTime;
OSPlayTimeCallbackFunc __OSExpireCallback;
BOOL __OSExpireSetExpiredFlag;
@@ -103,7 +103,7 @@ BOOL __OSWriteExpiredFlag(void) {
s32 rv = 0;
NANDFileInfo nInfo;
BOOL openNInfo = FALSE;
u8 titleId[32] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) u8 titleId[32];
rv = NANDPrivateCreate("/shared2/expired", 63, 0);
@@ -162,7 +162,7 @@ BOOL __OSWriteExpiredFlagIfSet(void) {
void* __OSPlayTimeRebootThread(void* args) {
BOOL enabled;
u32 frames, fadeShift = 1;
__OSExpireAIFadeStruct aiFade ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) __OSExpireAIFadeStruct aiFade;
__OSExpireAIFade = &aiFade;
memset(__OSExpireAIFade, 0, sizeof(__OSExpireAIFadeStruct));
@@ -227,9 +227,9 @@ out:
s32 __OSGetPlayTime(ESTicketView* ticket, __OSPlayTimeType* type, u32* playTime) {
s32 rv;
u32 i;
ESLpEntry lpEntry[8] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) ESLpEntry lpEntry[8];
u32 numCc = 0, seenOther = 0;
ESTicketView ticketAligned ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) ESTicketView ticketAligned;
ASSERTLINE(601, ticket && type && playTime);
@@ -288,7 +288,7 @@ out:
s32 __OSGetPlayTimeCurrent(__OSPlayTimeType* type, u32* playTime) {
s32 rv;
ESTicketView ticket ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) ESTicketView ticket;
ASSERTLINE(676, type && playTime);
+1 -1
View File
@@ -3,7 +3,7 @@
#include "__os.h"
static SramControl Scb ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT);
ATTRIBUTE_ALIGN(DOLPHIN_ALIGNMENT) static SramControl Scb;
// prototypes
static int GetRTC(u32* rtc);
+1 -1
View File
@@ -2,7 +2,7 @@
#include <revolution/nand.h>
#include <cstring>
static OSStateFlags StateFlags ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static OSStateFlags StateFlags;
static u32 CheckSum(OSStateFlags* flags) {
u32* ptr, i, sum;
+6 -6
View File
@@ -5,14 +5,14 @@
#include <revolution/private/iosrestypes.h>
static u32 StmImInBuf[8] ATTRIBUTE_ALIGN(32);
static u32 StmImOutBuf[8] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static u32 StmImInBuf[8];
ATTRIBUTE_ALIGN(32) static u32 StmImOutBuf[8];
static u32 StmVdInBuf[8] ATTRIBUTE_ALIGN(32);
static u32 StmVdOutBuf[8] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static u32 StmVdInBuf[8];
ATTRIBUTE_ALIGN(32) static u32 StmVdOutBuf[8];
static u32 StmEhInBuf[8] ATTRIBUTE_ALIGN(32);
static u32 StmEhOutBuf[8] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static u32 StmEhInBuf[8];
ATTRIBUTE_ALIGN(32) static u32 StmEhOutBuf[8];
static OSResetCallback ResetCallback;
static OSPowerCallback PowerCallback;
+1 -1
View File
@@ -873,4 +873,4 @@ void* OSGetThreadSpecific(s32 index) {
#include "global.h"
extern u8 Debug_BBA_804516D0;
u8 Debug_BBA_804516D0 ATTRIBUTE_ALIGN(8);
ATTRIBUTE_ALIGN(8) u8 Debug_BBA_804516D0;
+2 -2
View File
@@ -75,8 +75,8 @@ static const char ConfDirName[] = "/shared2/sys";
static const char ConfFileName[] = "/shared2/sys/SYSCONF";
static const char ProductInfoFileName[] = "/title/00000001/00000002/data/setting.txt";
static u8 ConfBuf[0x4000] ATTRIBUTE_ALIGN(32);
static u8 ConfBufForFlush[0x4000] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) static u8 ConfBuf[0x4000];
ATTRIBUTE_ALIGN(32) static u8 ConfBufForFlush[0x4000];
static u8 Initialized;
static u8 DirtyFlag;
+1 -1
View File
@@ -262,7 +262,7 @@ void WPADiManageHandler(OSAlarm*, OSContext*) {
BTA_HhGetAclQueueInfo();
}
u8 __WPADiManageHandlerStack[4096] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) u8 __WPADiManageHandlerStack[4096];
void WPADiManageHandler0(OSAlarm* alarm, OSContext* context) {
OSSwitchFiberEx((u32)alarm, (u32)context, 0, 0, (u32)WPADiManageHandler, (u32)(__WPADiManageHandlerStack + sizeof(__WPADiManageHandlerStack)));
+1 -1
View File
@@ -41,7 +41,7 @@ WUDControlBlock _wcb;
WUDDevInfo _work;
static WUDDiscResp _discResp;
SCBtDeviceInfoArray _scArray;
u8 __WUDHandlerStack[0x1000] ATTRIBUTE_ALIGN(32);
ATTRIBUTE_ALIGN(32) u8 __WUDHandlerStack[0x1000];
extern u8 _scFlush;
+22
View File
@@ -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
)
+7
View File
@@ -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);
+694
View File
@@ -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;
}
+5
View File
@@ -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 &currentMidnaHairColors.l_normalColor;
}
const GXColor* get_midna_hair_normalKColor() {
return &currentMidnaHairColors.l_normalKColor;
}
const GXColor* get_midna_hair_normalKColor2() {
return &currentMidnaHairColors.l_normalKColor2;
}
const GXColorS10* get_midna_hair_bigColor() {
return &currentMidnaHairColors.l_bigColor;
}
const GXColor* get_midna_hair_bigKColor() {
return &currentMidnaHairColors.l_bigKColor;
}
const GXColor* get_midna_hair_lNormalKColor() {
return &currentMidnaHairColors.l_lNormalKColor;
}
const GXColor* get_midna_hair_lNormalKColor2() {
return &currentMidnaHairColors.l_lNormalKColor2;
}
const GXColorS10* get_midna_hair_lBigColor() {
return &currentMidnaHairColors.l_lBigColor;
}
const GXColor* get_midna_hair_lBigKColor2() {
return &currentMidnaHairColors.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();
+282
View File
@@ -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;
}
}
+44
View File
@@ -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
View File
@@ -1,6 +1,6 @@
# Android Shell
This directory contains a minimal SDLActivity-based Android app wrapper for Dusklight.
This directory contains Dusklight's Android shell built on top of Borealis.
## Prerequisites
@@ -25,24 +25,19 @@ cmake --build --preset android-arm64
This build produces `build/android-arm64/libmain.so`
## Refresh SDL Java Shim (Optional)
If you update SDL and want to refresh the embedded Java shim files:
```bash
./android/scripts/sync-sdl-java.sh
```
## Build APK
```bash
cd android
cd platforms/android
./gradlew :app:assembleDebug
```
Output APK:
- `android/app/build/outputs/apk/debug/app-debug.apk`
- `app/build/outputs/apk/debug/app-arm64-v8a-debug.apk`
Aurora needs a hardware-backed graphics adapter. If an AVD has GPU
acceleration disabled, launch it with `-gpu host`.
## Launch With Runtime Args (adb)
@@ -50,10 +45,13 @@ You can pass command-line args through the activity intent:
```bash
adb shell am start -n dev.twilitrealm.dusk/.DuskActivity \
--es dusk_args "--backend vulkan"
--es borealis_args "--backend vulkan"
```
Supported extras:
- `dusk_args`: single shell-like argument string
- `dusk_argv`: string-array argv
- `borealis_args`: single shell-like argument string
- `borealis_argv`: string-array argv
The legacy `dusk_args` and `dusk_argv` names remain accepted during the shell
transition.
+22 -153
View File
@@ -2,163 +2,32 @@ plugins {
id 'com.android.application'
}
def versionNameStr = (System.getenv("DUSK_VERSION") ?: "v0.1.0").replaceFirst("^v", "")
def versionCodeInt = (System.getenv("DUSK_VERSION_CODE") ?: "100000").toInteger()
def duskRepoDir = rootProject.projectDir.parentFile.parentFile
def androidNativeBuildDir = new File(duskRepoDir, 'build/android-arm64')
def nativeLibrary = new File(androidNativeBuildDir, 'libmain.so')
def stageStripValue = providers.gradleProperty('ANDROID_STAGE_STRIP')
.orElse(providers.gradleProperty('androidStageStrip'))
.orElse(providers.environmentVariable('ANDROID_STAGE_STRIP'))
.orElse('1')
def androidHome = {
def sdkPath = System.getenv('ANDROID_HOME')
if (!sdkPath) {
throw new GradleException('ANDROID_HOME is not available')
}
def sdkDir = new File(sdkPath)
if (!sdkDir.isDirectory()) {
throw new GradleException("ANDROID_HOME points to a missing directory: ${sdkDir}")
}
sdkDir
}.memoize()
def borealisDir = new File(duskRepoDir, 'extern/borealis')
def androidNdkVersion = {
def ndkVersion = System.getenv('ANDROID_NDK_VERSION')
if (!ndkVersion) {
throw new GradleException('ANDROID_NDK_VERSION is not available')
}
ndkVersion
}.memoize()
ext.borealisAndroid = [
borealisDir: borealisDir,
propertiesFile: new File(duskRepoDir, 'build/android-arm64/borealis-android.properties'),
namespace: 'dev.twilitrealm.dusk',
applicationId: 'dev.twilitrealm.dusk',
abis: ['arm64-v8a'],
assets: [
[
from: new File(duskRepoDir, 'res'),
into: 'res',
excludes: ['**/.DS_Store']
],
[
from: new File(duskRepoDir, 'build/android-arm64/bundled_mods'),
into: 'mods',
includes: ['*.dusk']
]
],
proguardRules: file('proguard-rules.pro')
]
def androidNdkDir = {
def ndkDir = new File(androidHome(), "ndk/${androidNdkVersion()}")
if (!new File(ndkDir, 'build/cmake/android.toolchain.cmake').isFile()) {
throw new GradleException(
"Android NDK ${androidNdkVersion()} is missing or invalid at ${ndkDir}"
)
}
ndkDir
}.memoize()
def llvmPrebuiltDir = {
def prebuiltRoot = new File(androidNdkDir(), 'toolchains/llvm/prebuilt')
def prebuiltDir = (prebuiltRoot.listFiles()?.findAll { it.isDirectory() } ?: [])
.sort { it.name }
.find { candidate ->
new File(
candidate,
'sysroot/usr/lib/aarch64-linux-android/libc++_shared.so'
).isFile()
}
if (!prebuiltDir) {
throw new GradleException("Cannot find NDK libc++_shared.so under ${prebuiltRoot}")
}
prebuiltDir
}.memoize()
def stlLibrary = {
def library = new File(
llvmPrebuiltDir(),
'sysroot/usr/lib/aarch64-linux-android/libc++_shared.so'
)
if (!library.isFile()) {
throw new GradleException("libc++_shared.so is missing")
}
library
}.memoize()
def stagedJniLibsDir = layout.buildDirectory.dir('generated/jniLibs/dusklight')
def stageJniLibs = tasks.register('stageJniLibs', Sync) {
group = 'build'
from(nativeLibrary) {
rename { 'libmain.so' }
into 'arm64-v8a'
}
from(providers.provider { stlLibrary() }) {
rename { 'libc++_shared.so' }
into 'arm64-v8a'
}
into(stagedJniLibsDir)
doFirst {
if (!nativeLibrary.isFile()) {
throw new GradleException("Native library is missing")
}
}
}
def duskGeneratedAssetsDir = layout.buildDirectory.dir('generated/assets/dusklight')
def syncDuskAssets = tasks.register('syncDuskAssets', Sync) {
from(new File(duskRepoDir, 'res')) {
into 'res'
exclude '**/.DS_Store'
}
from(new File(androidNativeBuildDir, 'bundled_mods')) {
into 'mods'
include '*.dusk'
}
into(duskGeneratedAssetsDir)
}
android {
namespace 'dev.twilitrealm.dusk'
compileSdk 36
ndkVersion androidNdkVersion()
defaultConfig {
applicationId 'dev.twilitrealm.dusk'
minSdk 26
targetSdk 36
versionCode versionCodeInt
versionName versionNameStr
}
buildTypes {
debug {
minifyEnabled false
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main {
jniLibs.srcDirs = [stagedJniLibsDir]
assets.srcDirs = [duskGeneratedAssetsDir]
}
}
packaging {
jniLibs {
if (stageStripValue.get() == '0') {
keepDebugSymbols += '**/libmain.so'
}
}
}
splits {
abi {
enable true
reset()
include 'arm64-v8a'
universalApk false
}
}
lint {
abortOnError false
}
}
apply from: new File(borealisDir, 'platforms/android/gradle/borealis-application.gradle')
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
tasks.named('preBuild').configure {
dependsOn(stageJniLibs, syncDuskAssets)
}
+1 -4
View File
@@ -1,4 +1 @@
# Keep SDL activity and related JNI bridge methods.
-keep class org.libsdl.app.** { *; }
-keep class dev.twilitrealm.dusk.DuskHttpClient { *; }
-keep class dev.twilitrealm.dusk.DuskHttpClient$Response { *; }
-keep class dev.twilitrealm.dusk.DuskActivity { *; }
@@ -26,8 +26,8 @@
android:resource="@xml/game_mode_config" />
<provider
android:name="dev.twilitrealm.dusk.DuskDocumentsProvider"
android:authorities="dev.twilitrealm.dusk.documents"
android:name="dev.encounter.borealis.BorealisDocumentsProvider"
android:authorities="${applicationId}.documents"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
@@ -1,102 +1,23 @@
package dev.twilitrealm.dusk;
import android.app.ActionBar;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ClipData;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.OpenableColumns;
import android.provider.Settings;
import android.util.Log;
import android.view.Display;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.View;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import org.libsdl.app.SDLActivity;
import org.libsdl.app.SDLSurface;
import dev.encounter.borealis.BorealisActivity;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
public class DuskActivity extends SDLActivity {
public class DuskActivity extends BorealisActivity {
private static final String TAG = "DuskActivity";
private static final float DEFAULT_SURFACE_FRAME_RATE = 60.0f;
private static final int FOLDER_DIALOG_REQUEST_CODE = 0x4455;
private static final int MANAGE_STORAGE_REQUEST_CODE = 0x4456;
private static final String EXTERNAL_STORAGE_AUTHORITY =
"com.android.externalstorage.documents";
private long folderDialogUserdata = 0;
private boolean awaitingManageStoragePermission = false;
private static native void nativeFolderDialogResult(long userdata, String path, String error);
private static String[] splitArgs(String raw) {
List<String> out = new ArrayList<>();
StringBuilder current = new StringBuilder();
boolean inSingle = false;
boolean inDouble = false;
boolean escaped = false;
for (int i = 0; i < raw.length(); ++i) {
char c = raw.charAt(i);
if (escaped) {
current.append(c);
escaped = false;
continue;
}
if (c == '\\' && !inSingle) {
escaped = true;
continue;
}
if (c == '"' && !inSingle) {
inDouble = !inDouble;
continue;
}
if (c == '\'' && !inDouble) {
inSingle = !inSingle;
continue;
}
if (!inSingle && !inDouble && Character.isWhitespace(c)) {
if (current.length() > 0) {
out.add(current.toString());
current.setLength(0);
}
continue;
}
current.append(c);
}
if (escaped) {
current.append('\\');
}
if (current.length() > 0) {
out.add(current.toString());
}
return out.toArray(new String[0]);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
extractBundledMods();
super.onCreate(savedInstanceState);
hideSystemBars();
}
// Bundled mod packages ship as APK assets, which the native loader cannot read directly;
@@ -143,459 +64,23 @@ public class DuskActivity extends SDLActivity {
file.delete();
}
@Override
protected SDLSurface createSDLSurface(Context context) {
return new DuskSurface(context);
}
@Override
protected void onResume() {
super.onResume();
hideSystemBars();
if (awaitingManageStoragePermission) {
resumeFolderDialogAfterPermissionGrant();
}
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
hideSystemBars();
}
}
private void hideSystemBars() {
Window window = getWindow();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.setDecorFitsSystemWindows(false);
WindowInsetsController ctrl = window.getDecorView().getWindowInsetsController();
if (ctrl != null) {
ctrl.setSystemBarsBehavior(
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE);
ctrl.hide(WindowInsets.Type.systemBars());
}
} else {
View decorView = window.getDecorView();
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN |
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
View.SYSTEM_UI_FLAG_LAYOUT_STABLE;
decorView.setSystemUiVisibility(uiOptions);
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.hide();
}
}
}
@Override
protected String[] getLibraries() {
// SDL3 is statically linked into libmain.so in this build.
return new String[] {
"main"
};
}
public void setPreferredSurfaceFrameRate(float frameRate) {
runOnUiThread(() -> {
if (mSurface instanceof DuskSurface) {
((DuskSurface)mSurface).setPreferredFrameRate(frameRate);
}
});
}
private static final class DuskSurface extends SDLSurface {
private float preferredFrameRate = DEFAULT_SURFACE_FRAME_RATE;
DuskSurface(Context context) {
super(context);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
super.surfaceChanged(holder, format, width, height);
setTargetFrameRate(holder);
}
void setPreferredFrameRate(float frameRate) {
preferredFrameRate = frameRate;
setTargetFrameRate(getHolder());
}
private void setTargetFrameRate(SurfaceHolder holder) {
if (!mIsSurfaceReady || Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
return;
}
Surface surface = holder != null ? holder.getSurface() : getHolder().getSurface();
if (surface == null || !surface.isValid()) {
return;
}
float targetFrameRate = getMaxSupportedFrameRate();
if (preferredFrameRate > 0.0f) {
targetFrameRate = preferredFrameRate;
}
if (targetFrameRate <= 0.0f) {
return;
}
try {
surface.setFrameRate(
targetFrameRate, Surface.FRAME_RATE_COMPATIBILITY_DEFAULT);
Log.v(TAG, "Requested surface frame rate " + targetFrameRate + " fps");
} catch (RuntimeException e) {
Log.w(TAG, "Failed to request surface frame rate", e);
}
}
private float getMaxSupportedFrameRate() {
if (mDisplay == null) {
return 0.0f;
}
float maxFrameRate = mDisplay.getRefreshRate();
Display.Mode[] modes = mDisplay.getSupportedModes();
if (modes == null) {
return maxFrameRate;
}
for (Display.Mode mode : modes) {
maxFrameRate = Math.max(maxFrameRate, mode.getRefreshRate());
}
return maxFrameRate;
}
}
@Override
protected String[] getArguments() {
String[] arguments = super.getArguments();
if (arguments.length > 0) {
return arguments;
}
Intent intent = getIntent();
if (intent != null) {
String[] argv = intent.getStringArrayExtra("dusk_argv");
if (argv != null && argv.length > 0) {
return argv;
}
String rawArgs = intent.getStringExtra("dusk_args");
if (rawArgs != null) {
String trimmed = rawArgs.trim();
if (!trimmed.isEmpty()) {
return splitArgs(trimmed);
}
}
if (intent == null) {
return arguments;
}
return new String[0];
String[] argv = intent.getStringArrayExtra("dusk_argv");
if (argv != null && argv.length > 0) {
return argv;
}
String rawArgs = intent.getStringExtra("dusk_args");
return rawArgs == null ? arguments : splitArguments(rawArgs.trim());
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == RESULT_OK) {
persistUriPermissions(data);
}
if (requestCode == FOLDER_DIALOG_REQUEST_CODE) {
finishFolderDialog(resultCode, data);
return;
}
super.onActivityResult(requestCode, resultCode, data);
}
public boolean showFolderDialog(long userdata) {
if (userdata == 0 || folderDialogUserdata != 0) {
return false;
}
folderDialogUserdata = userdata;
if (requiresManageStoragePermission() && !hasManageStoragePermission()) {
if (!requestManageStoragePermission()) {
finishFolderDialogWithError("Unable to request Android file access permission");
return false;
}
return true;
}
openFolderDialog();
return true;
}
private void openFolderDialog() {
runOnUiThread(() -> {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION |
Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION |
Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
try {
startActivityForResult(intent, FOLDER_DIALOG_REQUEST_CODE);
} catch (ActivityNotFoundException e) {
Log.w(TAG, "Unable to open folder dialog.", e);
finishFolderDialog(Activity.RESULT_CANCELED, null);
}
});
}
private boolean requiresManageStoragePermission() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R;
}
private boolean hasManageStoragePermission() {
return !requiresManageStoragePermission() || Environment.isExternalStorageManager();
}
private boolean requestManageStoragePermission() {
if (!requiresManageStoragePermission()) {
return true;
}
awaitingManageStoragePermission = true;
runOnUiThread(() -> {
if (tryStartManageStorageIntent(
new Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
.setData(Uri.parse("package:" + getPackageName()))) ||
tryStartManageStorageIntent(
new Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)))
{
return;
}
finishFolderDialogWithError("Unable to request Android file access permission");
});
return true;
}
private boolean tryStartManageStorageIntent(Intent intent) {
try {
startActivityForResult(intent, MANAGE_STORAGE_REQUEST_CODE);
return true;
} catch (ActivityNotFoundException e) {
Log.w(TAG, "Unable to open all-files access settings.", e);
return false;
}
}
private void resumeFolderDialogAfterPermissionGrant() {
awaitingManageStoragePermission = false;
if (folderDialogUserdata == 0) {
return;
}
if (hasManageStoragePermission()) {
openFolderDialog();
return;
}
finishFolderDialogWithError(
"Allow \"All files access\" for Dusklight before choosing a custom data folder");
}
private void finishFolderDialogWithError(String error) {
long userdata = folderDialogUserdata;
folderDialogUserdata = 0;
awaitingManageStoragePermission = false;
if (userdata != 0) {
nativeFolderDialogResult(userdata, null, error);
}
}
private void finishFolderDialog(int resultCode, Intent data) {
long userdata = folderDialogUserdata;
folderDialogUserdata = 0;
if (userdata == 0) {
return;
}
if (resultCode == RESULT_OK && data != null && data.getData() != null) {
String path = getRealPathForUri(data.getData());
if (path != null && !path.isEmpty()) {
nativeFolderDialogResult(userdata, path, null);
} else {
nativeFolderDialogResult(
userdata, null, "Selected folder is not available as a filesystem path");
}
return;
}
nativeFolderDialogResult(userdata, null, null);
}
private String getRealPathForUri(Uri uri) {
if (uri == null) {
return null;
}
String scheme = uri.getScheme();
if ("file".equals(scheme)) {
return uri.getPath();
}
if (!"content".equals(scheme) ||
!EXTERNAL_STORAGE_AUTHORITY.equals(uri.getAuthority()) ||
Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
{
return null;
}
try {
return getExternalStoragePathForDocumentId(getExternalStorageDocumentId(uri));
} catch (IllegalArgumentException e) {
Log.w(TAG, "Unable to resolve URI: " + uri, e);
return null;
}
}
private static String getExternalStorageDocumentId(Uri uri) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && isTreeDocumentUri(uri)) {
return DocumentsContract.getTreeDocumentId(uri);
}
return DocumentsContract.getDocumentId(uri);
}
private static boolean isTreeDocumentUri(Uri uri) {
List<String> segments = uri.getPathSegments();
return segments.size() >= 2 && "tree".equals(segments.get(0));
}
private String getExternalStoragePathForDocumentId(String documentId) {
if (documentId == null || documentId.isEmpty()) {
return null;
}
if (documentId.startsWith("raw:")) {
return documentId.substring("raw:".length());
}
String[] parts = documentId.split(":", 2);
String volumeId = parts[0];
String relativePath = parts.length > 1 ? parts[1] : "";
File root = getExternalStorageRoot(volumeId);
if (root == null) {
return null;
}
return relativePath.isEmpty()
? root.getAbsolutePath()
: new File(root, relativePath).getAbsolutePath();
}
private File getExternalStorageRoot(String volumeId) {
if ("primary".equalsIgnoreCase(volumeId)) {
return Environment.getExternalStorageDirectory();
}
if ("home".equalsIgnoreCase(volumeId)) {
return new File(
Environment.getExternalStorageDirectory(), Environment.DIRECTORY_DOCUMENTS);
}
File[] externalFilesDirs = getExternalFilesDirs(null);
if (externalFilesDirs != null) {
for (File externalFilesDir : externalFilesDirs) {
File root = getStorageRootForExternalFilesDir(externalFilesDir);
if (root != null && volumeId.equalsIgnoreCase(root.getName())) {
return root;
}
}
}
File fallback = new File("/storage", volumeId);
return fallback.exists() ? fallback : null;
}
private File getStorageRootForExternalFilesDir(File externalFilesDir) {
if (externalFilesDir == null) {
return null;
}
String path = externalFilesDir.getAbsolutePath();
int androidDir = path.indexOf("/Android/");
if (androidDir <= 0) {
return null;
}
return new File(path.substring(0, androidDir));
}
private void persistUriPermissions(Intent data) {
if (data == null) {
return;
}
int permissionFlags =
data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
if (permissionFlags == 0) {
return;
}
Uri uri = data.getData();
if (uri != null) {
persistUriPermission(uri, permissionFlags);
}
ClipData clipData = data.getClipData();
if (clipData == null) {
return;
}
for (int i = 0; i < clipData.getItemCount(); ++i) {
Uri itemUri = clipData.getItemAt(i).getUri();
if (itemUri != null) {
persistUriPermission(itemUri, permissionFlags);
}
}
}
private void persistUriPermission(Uri uri, int permissionFlags) {
if ((permissionFlags & Intent.FLAG_GRANT_READ_URI_PERMISSION) != 0) {
persistUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION, "read");
}
if ((permissionFlags & Intent.FLAG_GRANT_WRITE_URI_PERMISSION) != 0) {
persistUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION, "write");
}
}
private void persistUriPermission(Uri uri, int permissionFlag, String permissionName) {
try {
getContentResolver().takePersistableUriPermission(uri, permissionFlag);
} catch (SecurityException | IllegalArgumentException e) {
Log.w(TAG, "Unable to persist " + permissionName + " URI permission for " + uri, e);
}
}
public String getDisplayNameForUri(String uriString) {
if (uriString == null || uriString.isEmpty()) {
return "";
}
Uri uri = Uri.parse(uriString);
if ("content".equals(uri.getScheme())) {
try (Cursor cursor = getContentResolver().query(
uri, new String[] { OpenableColumns.DISPLAY_NAME }, null, null, null))
{
if (cursor != null && cursor.moveToFirst()) {
int displayNameColumn = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (displayNameColumn >= 0) {
String displayName = cursor.getString(displayNameColumn);
if (displayName != null && !displayName.isEmpty()) {
return displayName;
}
}
}
} catch (SecurityException | IllegalArgumentException e) {
Log.w(TAG, "Unable to query display name for " + uri, e);
}
} else if ("file".equals(uri.getScheme())) {
String path = uri.getPath();
if (path != null && !path.isEmpty()) {
String name = new File(path).getName();
if (!name.isEmpty()) {
return name;
}
}
}
String lastSegment = uri.getLastPathSegment();
return lastSegment != null ? lastSegment : "";
}
}
@@ -1,467 +0,0 @@
package dev.twilitrealm.dusk;
import android.content.ContentResolver;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.webkit.MimeTypeMap;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class DuskDocumentsProvider extends DocumentsProvider {
public static final String AUTHORITY = "dev.twilitrealm.dusk.documents";
private static final String ROOT_ID = "dusk";
private static final String ROOT_DOCUMENT_ID = "root";
private static final String LOCATION_DESCRIPTOR_NAME = "data_location.json";
private static final String DIRECTORY_MIME_TYPE = Document.MIME_TYPE_DIR;
private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
Root.COLUMN_ROOT_ID,
Root.COLUMN_FLAGS,
Root.COLUMN_TITLE,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_ICON,
Root.COLUMN_AVAILABLE_BYTES,
Root.COLUMN_SUMMARY
};
private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_FLAGS,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_SIZE
};
@Override
public boolean onCreate() {
if (!isCustomDataPathEnabled()) {
ensureUserDirectories();
}
return true;
}
@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(resolveRootProjection(projection));
if (isCustomDataPathEnabled()) {
return result;
}
final File root = getRootDirectory();
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Root.COLUMN_ROOT_ID, ROOT_ID);
row.add(Root.COLUMN_FLAGS,
Root.FLAG_LOCAL_ONLY |
Root.FLAG_SUPPORTS_CREATE |
Root.FLAG_SUPPORTS_IS_CHILD);
row.add(Root.COLUMN_TITLE, getContext().getString(R.string.app_name));
row.add(Root.COLUMN_DOCUMENT_ID, ROOT_DOCUMENT_ID);
row.add(Root.COLUMN_ICON, R.mipmap.icon);
row.add(Root.COLUMN_AVAILABLE_BYTES, root.getFreeSpace());
row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.documents_provider_summary));
return result;
}
@Override
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
includeDocument(result, documentId, getFileForDocumentId(documentId));
return result;
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
throws FileNotFoundException
{
return queryChildDocumentsInternal(parentDocumentId, projection);
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, Bundle queryArgs)
throws FileNotFoundException
{
return queryChildDocumentsInternal(parentDocumentId, projection);
}
private Cursor queryChildDocumentsInternal(String parentDocumentId, String[] projection)
throws FileNotFoundException
{
final MatrixCursor result = new MatrixCursor(resolveDocumentProjection(projection));
final File parent = getFileForDocumentId(parentDocumentId);
final File[] files = parent.listFiles();
result.setNotificationUri(getContext().getContentResolver(), getChildDocumentsUri(parentDocumentId));
if (files == null) {
return result;
}
for (File file : files) {
includeDocument(result, getDocumentIdForFile(file), file);
}
return result;
}
@Override
public boolean isChildDocument(String parentDocumentId, String documentId) {
try {
final File parent = getFileForDocumentId(parentDocumentId);
final File child = getFileForDocumentId(documentId);
return isInside(parent, child);
} catch (FileNotFoundException e) {
return false;
}
}
@Override
public String createDocument(String parentDocumentId, String mimeType, String displayName)
throws FileNotFoundException
{
final File parent = getFileForDocumentId(parentDocumentId);
if (!parent.isDirectory()) {
throw new FileNotFoundException("Parent is not a directory: " + parentDocumentId);
}
final String safeDisplayName = sanitizeDisplayName(displayName);
final File file = buildUniqueFile(parent, safeDisplayName);
final boolean created;
if (DIRECTORY_MIME_TYPE.equals(mimeType)) {
created = file.mkdir();
} else {
try {
created = file.createNewFile();
} catch (IOException e) {
throw asFileNotFound("Unable to create document", e);
}
}
if (!created) {
throw new FileNotFoundException("Unable to create document: " + displayName);
}
notifyChildrenChanged(parentDocumentId);
return getDocumentIdForFile(file);
}
@Override
public String renameDocument(String documentId, String displayName) throws FileNotFoundException {
final File file = getFileForDocumentId(documentId);
if (ROOT_DOCUMENT_ID.equals(documentId)) {
throw new FileNotFoundException("Cannot rename root document");
}
final File target = buildUniqueFile(file.getParentFile(), sanitizeDisplayName(displayName));
final String parentDocumentId = getDocumentIdForFile(file.getParentFile());
if (!file.renameTo(target)) {
throw new FileNotFoundException("Unable to rename document: " + documentId);
}
notifyDocumentChanged(documentId);
notifyDocumentChanged(getDocumentIdForFile(target));
notifyChildrenChanged(parentDocumentId);
return getDocumentIdForFile(target);
}
@Override
public void deleteDocument(String documentId) throws FileNotFoundException {
if (ROOT_DOCUMENT_ID.equals(documentId)) {
throw new FileNotFoundException("Cannot delete root document");
}
final File file = getFileForDocumentId(documentId);
final String parentDocumentId = getDocumentIdForFile(file.getParentFile());
deleteRecursively(file);
notifyDocumentChanged(documentId);
notifyChildrenChanged(parentDocumentId);
}
@Override
public ParcelFileDescriptor openDocument(String documentId, String mode, CancellationSignal signal)
throws FileNotFoundException
{
return ParcelFileDescriptor.open(getFileForDocumentId(documentId), modeToParcelMode(mode));
}
@Override
public AssetFileDescriptor openDocumentThumbnail(String documentId, android.graphics.Point sizeHint,
CancellationSignal signal) throws FileNotFoundException
{
throw new FileNotFoundException("Thumbnails are not supported");
}
private void includeDocument(MatrixCursor result, String documentId, File file) throws FileNotFoundException {
final MatrixCursor.RowBuilder row = result.newRow();
final boolean isDirectory = file.isDirectory();
final String displayName = ROOT_DOCUMENT_ID.equals(documentId)
? getContext().getString(R.string.documents_provider_root_name)
: file.getName();
int flags = Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME;
if (isDirectory) {
flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
} else if (file.canWrite()) {
flags |= Document.FLAG_SUPPORTS_WRITE;
}
if (ROOT_DOCUMENT_ID.equals(documentId)) {
flags &= ~(Document.FLAG_SUPPORTS_DELETE | Document.FLAG_SUPPORTS_RENAME);
}
row.add(Document.COLUMN_DOCUMENT_ID, documentId);
row.add(Document.COLUMN_DISPLAY_NAME, displayName);
row.add(Document.COLUMN_FLAGS, flags);
row.add(Document.COLUMN_MIME_TYPE, isDirectory ? DIRECTORY_MIME_TYPE : getMimeType(file));
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
row.add(Document.COLUMN_SIZE, isDirectory ? null : file.length());
}
private File getRootDirectory() throws FileNotFoundException {
if (isCustomDataPathEnabled()) {
throw new FileNotFoundException(
"Dusk DocumentsProvider is disabled while a custom data path is configured");
}
final File root = getContext().getFilesDir();
if (root == null) {
throw new FileNotFoundException("Dusklight files directory is unavailable");
}
return root;
}
private File getFileForDocumentId(String documentId) throws FileNotFoundException {
final File root = getRootDirectory();
if (ROOT_DOCUMENT_ID.equals(documentId)) {
return root;
}
if (!documentId.startsWith(ROOT_DOCUMENT_ID + "/")) {
throw new FileNotFoundException("Invalid document id: " + documentId);
}
final String relativePath = documentId.substring(ROOT_DOCUMENT_ID.length() + 1);
final File file = new File(root, relativePath);
if (!isInside(root, file)) {
throw new FileNotFoundException("Document escapes Dusklight files directory: " + documentId);
}
if (!file.exists()) {
throw new FileNotFoundException("Document does not exist: " + documentId);
}
return file;
}
private String getDocumentIdForFile(File file) throws FileNotFoundException {
final File root = getRootDirectory();
if (sameFile(root, file)) {
return ROOT_DOCUMENT_ID;
}
if (!isInside(root, file)) {
throw new FileNotFoundException("File escapes Dusklight files directory: " + file);
}
final String rootPath = canonicalPath(root);
final String filePath = canonicalPath(file);
return ROOT_DOCUMENT_ID + "/" + filePath.substring(rootPath.length() + 1);
}
private void ensureUserDirectories() {
final File root = getContext().getFilesDir();
if (root == null) {
return;
}
new File(root, "texture_replacements").mkdirs();
new File(root, "USA/Card A").mkdirs();
new File(root, "EUR/Card A").mkdirs();
}
private boolean isCustomDataPathEnabled() {
if (getContext() == null) {
return false;
}
final File filesDir = getContext().getFilesDir();
if (filesDir == null) {
return false;
}
final File descriptor = new File(filesDir, LOCATION_DESCRIPTOR_NAME);
if (!descriptor.isFile()) {
return false;
}
try {
final JSONObject json = new JSONObject(readText(descriptor));
return "custom".equals(json.optString("mode", "default"));
} catch (IOException | JSONException e) {
return false;
}
}
private static String readText(File file) throws IOException {
try (FileInputStream input = new FileInputStream(file);
ByteArrayOutputStream output = new ByteArrayOutputStream())
{
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = input.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
return output.toString(StandardCharsets.UTF_8.name());
}
}
private static String[] resolveRootProjection(String[] projection) {
return projection != null ? projection : DEFAULT_ROOT_PROJECTION;
}
private static String[] resolveDocumentProjection(String[] projection) {
return projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION;
}
private static String sanitizeDisplayName(String displayName) throws FileNotFoundException {
if (displayName == null) {
throw new FileNotFoundException("Document name is empty");
}
final String sanitized = displayName.trim();
if (sanitized.isEmpty() || ".".equals(sanitized) || "..".equals(sanitized) ||
sanitized.contains("/") || sanitized.contains("\\"))
{
throw new FileNotFoundException("Invalid document name: " + displayName);
}
return sanitized;
}
private static File buildUniqueFile(File parent, String displayName) {
File file = new File(parent, displayName);
if (!file.exists()) {
return file;
}
final int dot = displayName.lastIndexOf('.');
final String baseName = dot > 0 ? displayName.substring(0, dot) : displayName;
final String extension = dot > 0 ? displayName.substring(dot) : "";
for (int i = 1; i < 100; ++i) {
file = new File(parent, baseName + " (" + i + ")" + extension);
if (!file.exists()) {
return file;
}
}
return new File(parent, baseName + " (" + System.currentTimeMillis() + ")" + extension);
}
private static int modeToParcelMode(String mode) {
if ("r".equals(mode)) {
return ParcelFileDescriptor.MODE_READ_ONLY;
}
if ("w".equals(mode) || "wt".equals(mode)) {
return ParcelFileDescriptor.MODE_WRITE_ONLY |
ParcelFileDescriptor.MODE_CREATE |
ParcelFileDescriptor.MODE_TRUNCATE;
}
if ("wa".equals(mode)) {
return ParcelFileDescriptor.MODE_WRITE_ONLY |
ParcelFileDescriptor.MODE_CREATE |
ParcelFileDescriptor.MODE_APPEND;
}
if ("rw".equals(mode)) {
return ParcelFileDescriptor.MODE_READ_WRITE |
ParcelFileDescriptor.MODE_CREATE;
}
if ("rwt".equals(mode)) {
return ParcelFileDescriptor.MODE_READ_WRITE |
ParcelFileDescriptor.MODE_CREATE |
ParcelFileDescriptor.MODE_TRUNCATE;
}
return ParcelFileDescriptor.MODE_READ_ONLY;
}
private static String getMimeType(File file) {
final int dot = file.getName().lastIndexOf('.');
if (dot >= 0) {
final String extension = file.getName().substring(dot + 1).toLowerCase();
final String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mimeType != null) {
return mimeType;
}
}
return "application/octet-stream";
}
private Uri getChildDocumentsUri(String parentDocumentId) {
return DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId);
}
private void notifyChildrenChanged(String parentDocumentId) {
final ContentResolver resolver = getContext().getContentResolver();
resolver.notifyChange(getChildDocumentsUri(parentDocumentId), null, false);
}
private void notifyDocumentChanged(String documentId) {
final ContentResolver resolver = getContext().getContentResolver();
resolver.notifyChange(DocumentsContract.buildDocumentUri(AUTHORITY, documentId), null, false);
}
private static void deleteRecursively(File file) throws FileNotFoundException {
if (file.isDirectory()) {
final File[] children = file.listFiles();
if (children != null) {
for (File child : children) {
deleteRecursively(child);
}
}
}
if (!file.delete()) {
throw new FileNotFoundException("Unable to delete document: " + file);
}
}
private static boolean isInside(File parent, File child) {
try {
final String parentPath = canonicalPath(parent);
final String childPath = canonicalPath(child);
return childPath.equals(parentPath) || childPath.startsWith(parentPath + File.separator);
} catch (FileNotFoundException e) {
return false;
}
}
private static boolean sameFile(File a, File b) {
try {
return canonicalPath(a).equals(canonicalPath(b));
} catch (FileNotFoundException e) {
return false;
}
}
private static String canonicalPath(File file) throws FileNotFoundException {
try {
return file.getCanonicalPath();
} catch (IOException e) {
throw asFileNotFound("Unable to resolve path", e);
}
}
private static FileNotFoundException asFileNotFound(String message, IOException cause) {
final FileNotFoundException exception = new FileNotFoundException(message + ": " + cause.getMessage());
exception.initCause(cause);
return exception;
}
}
@@ -1,237 +0,0 @@
package dev.twilitrealm.dusk;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.net.ssl.HttpsURLConnection;
public final class DuskHttpClient {
public static final int ERROR_NONE = 0;
public static final int ERROR_INVALID_URL = 1;
public static final int ERROR_UNSUPPORTED_SCHEME = 2;
public static final int ERROR_TIMEOUT = 3;
public static final int ERROR_TOO_LARGE = 4;
public static final int ERROR_NETWORK = 5;
private static final int MAX_REDIRECTS = 5;
public static final class Response {
public int error;
public String message;
public int statusCode;
public String[] headerNames;
public String[] headerValues;
public byte[] body;
Response(int error, String message, int statusCode, String[] headerNames,
String[] headerValues, byte[] body) {
this.error = error;
this.message = message;
this.statusCode = statusCode;
this.headerNames = headerNames != null ? headerNames : new String[0];
this.headerValues = headerValues != null ? headerValues : new String[0];
this.body = body != null ? body : new byte[0];
}
}
private DuskHttpClient() {
}
public static Response get(String url, String[] headerNames, String[] headerValues,
int timeoutMs, long maxBodyBytes) {
if (url == null || url.isEmpty()) {
return fail(ERROR_INVALID_URL, "URL is empty");
}
try {
URL currentUrl = new URL(url);
if (!isHttps(currentUrl)) {
return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported");
}
for (int redirect = 0; redirect <= MAX_REDIRECTS; ++redirect) {
HttpsURLConnection connection =
(HttpsURLConnection) currentUrl.openConnection();
try {
connection.setRequestMethod("GET");
connection.setConnectTimeout(timeoutMs);
connection.setReadTimeout(timeoutMs);
connection.setUseCaches(false);
connection.setInstanceFollowRedirects(false);
applyHeaders(connection, headerNames, headerValues);
int statusCode = connection.getResponseCode();
if (isRedirect(statusCode)) {
String location = connection.getHeaderField("Location");
if (location == null || location.isEmpty()) {
return fail(ERROR_NETWORK, "Redirect response did not include Location",
statusCode, connection, new byte[0]);
}
URL nextUrl = new URL(currentUrl, location);
if (!isHttps(nextUrl)) {
return fail(ERROR_UNSUPPORTED_SCHEME,
"Only https:// redirects are supported", statusCode,
connection, new byte[0]);
}
currentUrl = nextUrl;
continue;
}
byte[] body = readBody(connection, statusCode, maxBodyBytes);
return success(statusCode, connection, body);
} catch (ResponseTooLargeException e) {
return fail(ERROR_TOO_LARGE, "Response body exceeded the configured limit",
safeStatusCode(connection), connection, e.partialBody);
} finally {
connection.disconnect();
}
}
return fail(ERROR_NETWORK, "Too many redirects");
} catch (MalformedURLException e) {
return fail(ERROR_INVALID_URL, "Failed to parse URL");
} catch (SocketTimeoutException e) {
return fail(ERROR_TIMEOUT, "Request timed out");
} catch (IOException e) {
String message = e.getMessage();
return fail(ERROR_NETWORK, message != null ? message : e.toString());
} catch (ClassCastException e) {
return fail(ERROR_UNSUPPORTED_SCHEME, "Only https:// URLs are supported");
}
}
private static void applyHeaders(HttpsURLConnection connection, String[] names,
String[] values) {
if (names == null || values == null) {
return;
}
int count = Math.min(names.length, values.length);
for (int i = 0; i < count; ++i) {
if (names[i] != null && values[i] != null) {
connection.setRequestProperty(names[i], values[i]);
}
}
}
private static boolean isHttps(URL url) {
return "https".equalsIgnoreCase(url.getProtocol());
}
private static boolean isRedirect(int statusCode) {
return statusCode == HttpURLConnection.HTTP_MOVED_PERM ||
statusCode == HttpURLConnection.HTTP_MOVED_TEMP ||
statusCode == HttpURLConnection.HTTP_SEE_OTHER ||
statusCode == 307 ||
statusCode == 308;
}
private static byte[] readBody(HttpsURLConnection connection, int statusCode,
long maxBodyBytes) throws IOException,
ResponseTooLargeException {
InputStream stream = statusCode >= HttpURLConnection.HTTP_BAD_REQUEST ?
connection.getErrorStream() : connection.getInputStream();
if (stream == null) {
return new byte[0];
}
try (InputStream bodyStream = stream;
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
byte[] buffer = new byte[8192];
long total = 0;
while (true) {
int read = bodyStream.read(buffer);
if (read < 0) {
return out.toByteArray();
}
if (read == 0) {
continue;
}
if (read > maxBodyBytes || total > maxBodyBytes - read) {
throw new ResponseTooLargeException(out.toByteArray());
}
out.write(buffer, 0, read);
total += read;
}
}
}
private static int safeStatusCode(HttpsURLConnection connection) {
try {
return connection.getResponseCode();
} catch (IOException e) {
return 0;
}
}
private static Response success(int statusCode, HttpsURLConnection connection, byte[] body) {
HeaderLists headers = readHeaders(connection);
return new Response(ERROR_NONE, "", statusCode, headers.names, headers.values, body);
}
private static Response fail(int error, String message) {
return new Response(error, message, 0, null, null, null);
}
private static Response fail(int error, String message, int statusCode,
HttpsURLConnection connection, byte[] body) {
HeaderLists headers = readHeaders(connection);
return new Response(error, message, statusCode, headers.names, headers.values, body);
}
private static HeaderLists readHeaders(HttpsURLConnection connection) {
List<String> names = new ArrayList<>();
List<String> values = new ArrayList<>();
Map<String, List<String>> headerFields = connection.getHeaderFields();
if (headerFields == null) {
return new HeaderLists(new String[0], new String[0]);
}
for (Map.Entry<String, List<String>> entry : headerFields.entrySet()) {
String name = entry.getKey();
if (name == null) {
continue;
}
List<String> entryValues = entry.getValue();
if (entryValues == null || entryValues.isEmpty()) {
names.add(name);
values.add("");
continue;
}
for (String value : entryValues) {
names.add(name);
values.add(value != null ? value : "");
}
}
return new HeaderLists(names.toArray(new String[0]), values.toArray(new String[0]));
}
private static final class HeaderLists {
final String[] names;
final String[] values;
HeaderLists(String[] names, String[] values) {
this.names = names;
this.values = values;
}
}
private static final class ResponseTooLargeException extends Exception {
final byte[] partialBody;
ResponseTooLargeException(byte[] partialBody) {
this.partialBody = partialBody;
}
}
}
@@ -1,21 +0,0 @@
package org.libsdl.app;
import android.hardware.usb.UsbDevice;
interface HIDDevice
{
public int getId();
public int getVendorId();
public int getProductId();
public String getSerialNumber();
public int getVersion();
public String getManufacturerName();
public String getProductName();
public UsbDevice getDevice();
public boolean open();
public int writeReport(byte[] report, boolean feature);
public boolean readReport(byte[] report, boolean feature);
public void setFrozen(boolean frozen);
public void close();
public void shutdown();
}
@@ -1,829 +0,0 @@
package org.libsdl.app;
import android.content.Context;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothGattService;
import android.hardware.usb.UsbDevice;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.os.*;
//import com.android.internal.util.HexDump;
import java.lang.Runnable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.UUID;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice {
private static final String TAG = "hidapi";
private HIDDeviceManager mManager;
private BluetoothDevice mDevice;
private int mDeviceId;
private BluetoothGatt mGatt;
private boolean mIsRegistered = false;
private boolean mIsConnected = false;
private boolean mIsChromebook = false;
private boolean mIsReconnecting = false;
private boolean mHasEnabledNotifications = false;
private boolean mHasSeenInputUpdate = false;
private boolean mFrozen = false;
private LinkedList<GattOperation> mOperations;
GattOperation mCurrentOperation = null;
private Handler mHandler;
private int mProductId = -1;
private int mReportId = 0;
private UUID mInputCharacteristic;
private static final int D0G_BLE2_PID = 0x1106;
private static final int TRITON_BLE_PID = 0x1303;
private static final int TRANSPORT_AUTO = 0;
private static final int TRANSPORT_BREDR = 1;
private static final int TRANSPORT_LE = 2;
private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000;
static final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3");
static final UUID inputCharacteristicD0G = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3");
static final UUID inputCharacteristicTriton_0x45 = UUID.fromString("100F6C7A-1735-4313-B402-38567131E5F3");
static final UUID inputCharacteristicTriton_0x47 = UUID.fromString("100F6C7C-1735-4313-B402-38567131E5F3");
static final UUID reportCharacteristic = UUID.fromString("100F6C34-1735-4313-B402-38567131E5F3");
static private final byte[] enterValveMode = new byte[] { (byte)0xC0, (byte)0x87, 0x03, 0x08, 0x07, 0x00 };
private HashMap<Integer, BluetoothGattCharacteristic> mOutputReportChars = new HashMap<Integer, BluetoothGattCharacteristic>();
static class GattOperation {
private enum Operation {
CHR_READ,
CHR_WRITE,
ENABLE_NOTIFICATION
}
Operation mOp;
UUID mUuid;
byte[] mValue;
BluetoothGatt mGatt;
boolean mResult = true;
int mDelayMs = 0;
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
mGatt = gatt;
mOp = operation;
mUuid = uuid;
}
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, int delayMs) {
mGatt = gatt;
mOp = operation;
mUuid = uuid;
mDelayMs = delayMs;
}
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
mGatt = gatt;
mOp = operation;
mUuid = uuid;
mValue = value;
}
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value, int delayMs) {
mGatt = gatt;
mOp = operation;
mUuid = uuid;
mValue = value;
mDelayMs = delayMs;
}
public void run() {
// This is executed in main thread
BluetoothGattCharacteristic chr;
switch (mOp) {
case CHR_READ:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Reading characteristic " + chr.getUuid());
if (!mGatt.readCharacteristic(chr)) {
Log.e(TAG, "Unable to read characteristic " + mUuid.toString());
mResult = false;
break;
}
mResult = true;
break;
case CHR_WRITE:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Writing characteristic " + chr.getUuid() + " value=" + HexDump.toHexString(value));
chr.setValue(mValue);
if (!mGatt.writeCharacteristic(chr)) {
Log.e(TAG, "Unable to write characteristic " + mUuid.toString());
mResult = false;
break;
}
mResult = true;
break;
case ENABLE_NOTIFICATION:
chr = getCharacteristic(mUuid);
//Log.v(TAG, "Writing descriptor of " + chr.getUuid());
if (chr != null) {
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if (cccd != null) {
int properties = chr.getProperties();
byte[] value;
if ((properties & BluetoothGattCharacteristic.PROPERTY_NOTIFY) == BluetoothGattCharacteristic.PROPERTY_NOTIFY) {
value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE;
} else if ((properties & BluetoothGattCharacteristic.PROPERTY_INDICATE) == BluetoothGattCharacteristic.PROPERTY_INDICATE) {
value = BluetoothGattDescriptor.ENABLE_INDICATION_VALUE;
} else {
Log.e(TAG, "Unable to start notifications on input characteristic");
mResult = false;
return;
}
mGatt.setCharacteristicNotification(chr, true);
cccd.setValue(value);
if (!mGatt.writeDescriptor(cccd)) {
Log.e(TAG, "Unable to write descriptor " + mUuid.toString());
mResult = false;
return;
}
mResult = true;
}
}
}
}
public boolean finish() {
return mResult;
}
public int getDelayMs() { return mDelayMs; }
private BluetoothGattCharacteristic getCharacteristic(UUID uuid) {
BluetoothGattService valveService = mGatt.getService(steamControllerService);
if (valveService == null)
return null;
return valveService.getCharacteristic(uuid);
}
static public GattOperation readCharacteristic(BluetoothGatt gatt, UUID uuid) {
return new GattOperation(gatt, Operation.CHR_READ, uuid);
}
static public GattOperation writeCharacteristic(BluetoothGatt gatt, UUID uuid, byte[] value) {
return new GattOperation(gatt, Operation.CHR_WRITE, uuid, value);
}
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid) {
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid);
}
static public GattOperation enableNotification(BluetoothGatt gatt, UUID uuid, int delayMs) {
return new GattOperation(gatt, Operation.ENABLE_NOTIFICATION, uuid, delayMs);
}
}
HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) {
mManager = manager;
mDevice = device;
mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier());
mIsRegistered = false;
mIsChromebook = SDLActivity.isChromebook();
mOperations = new LinkedList<GattOperation>();
mHandler = new Handler(Looper.getMainLooper());
mGatt = connectGatt();
mHasEnabledNotifications = false;
mHasSeenInputUpdate = false;
// final HIDDeviceBLESteamController finalThis = this;
// mHandler.postDelayed(new Runnable() {
// @Override
// void run() {
// finalThis.checkConnectionForChromebookIssue();
// }
// }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
}
String getIdentifier() {
return String.format("SteamController.%s", mDevice.getAddress());
}
BluetoothGatt getGatt() {
return mGatt;
}
// Because on Chromebooks we show up as a dual-mode device, it will attempt to connect TRANSPORT_AUTO, which will use TRANSPORT_BREDR instead
// of TRANSPORT_LE. Let's force ourselves to connect low energy.
private BluetoothGatt connectGatt(boolean managed) {
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
try {
return mDevice.connectGatt(mManager.getContext(), managed, this, TRANSPORT_LE);
} catch (Exception e) {
return mDevice.connectGatt(mManager.getContext(), managed, this);
}
} else {
return mDevice.connectGatt(mManager.getContext(), managed, this);
}
}
private BluetoothGatt connectGatt() {
return connectGatt(false);
}
protected int getConnectionState() {
Context context = mManager.getContext();
if (context == null) {
// We are lacking any context to get our Bluetooth information. We'll just assume disconnected.
return BluetoothProfile.STATE_DISCONNECTED;
}
BluetoothManager btManager = (BluetoothManager)context.getSystemService(Context.BLUETOOTH_SERVICE);
if (btManager == null) {
// This device doesn't support Bluetooth. We should never be here, because how did
// we instantiate a device to start with?
return BluetoothProfile.STATE_DISCONNECTED;
}
return btManager.getConnectionState(mDevice, BluetoothProfile.GATT);
}
void reconnect() {
if (getConnectionState() != BluetoothProfile.STATE_CONNECTED) {
mGatt.disconnect();
mGatt = connectGatt();
}
}
protected void checkConnectionForChromebookIssue() {
if (!mIsChromebook) {
// We only do this on Chromebooks, because otherwise it's really annoying to just attempt
// over and over.
return;
}
int connectionState = getConnectionState();
switch (connectionState) {
case BluetoothProfile.STATE_CONNECTED:
if (!mIsConnected) {
// We are in the Bad Chromebook Place. We can force a disconnect
// to try to recover.
Log.v(TAG, "Chromebook: We are in a very bad state; the controller shows as connected in the underlying Bluetooth layer, but we never received a callback. Forcing a reconnect.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
}
else if (!isRegistered()) {
if (mGatt.getServices().size() > 0) {
Log.v(TAG, "Chromebook: We are connected to a controller, but never got our registration. Trying to recover.");
probeService(this);
}
else {
Log.v(TAG, "Chromebook: We are connected to a controller, but never discovered services. Trying to recover.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
}
}
else {
Log.v(TAG, "Chromebook: We are connected, and registered. Everything's good!");
return;
}
break;
case BluetoothProfile.STATE_DISCONNECTED:
Log.v(TAG, "Chromebook: We have either been disconnected, or the Chromebook BtGatt.ContextMap bug has bitten us. Attempting a disconnect/reconnect, but we may not be able to recover.");
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
break;
case BluetoothProfile.STATE_CONNECTING:
Log.v(TAG, "Chromebook: We're still trying to connect. Waiting a bit longer.");
break;
}
final HIDDeviceBLESteamController finalThis = this;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
finalThis.checkConnectionForChromebookIssue();
}
}, CHROMEBOOK_CONNECTION_CHECK_INTERVAL);
}
private boolean isRegistered() {
return mIsRegistered;
}
private void setRegistered() {
mIsRegistered = true;
}
private boolean probeService(HIDDeviceBLESteamController controller) {
if (isRegistered()) {
return true;
}
if (!mIsConnected) {
return false;
}
Log.v(TAG, "probeService controller=" + controller);
for (BluetoothGattService service : mGatt.getServices()) {
if (service.getUuid().equals(steamControllerService)) {
Log.v(TAG, "Found Valve steam controller service " + service.getUuid());
for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
if (chr.getUuid().equals(inputCharacteristicTriton_0x45)) {
Log.v(TAG, "Found Triton input characteristic 0x45");
mProductId = TRITON_BLE_PID;
mReportId = 0x45;
mInputCharacteristic = chr.getUuid();
} else if (chr.getUuid().equals(inputCharacteristicTriton_0x47)) {
Log.v(TAG, "Found Triton input characteristic 0x47");
mProductId = TRITON_BLE_PID;
mReportId = 0x47;
mInputCharacteristic = chr.getUuid();
} else if (chr.getUuid().equals(inputCharacteristicD0G)) {
Log.v(TAG, "Found D0G input characteristic");
mProductId = D0G_BLE2_PID;
mReportId = 0x03;
mInputCharacteristic = chr.getUuid();
} else {
Pattern reportPattern = Pattern.compile("100F6C([0-9A-Z]{2})", Pattern.CASE_INSENSITIVE);
Matcher matcher = reportPattern.matcher(chr.getUuid().toString());
if (matcher.find()) {
try {
int reportId = Integer.parseInt(matcher.group(1), 16);
reportId -= 0x35;
if (reportId >= 0x80) {
// This is a Triton output report characteristic that we need to care about.
Log.v(TAG, "Found Triton output report 0x" + Integer.toString(reportId, 16));
mOutputReportChars.put(reportId, chr);
}
}
catch (NumberFormatException nfe) {
Log.w(TAG, "Could not parse report characteristic " + chr.getUuid().toString() + ": " + nfe.toString());
}
}
}
}
for (BluetoothGattCharacteristic chr : service.getCharacteristics()) {
if (chr.getUuid().equals(mInputCharacteristic)) {
// Start notifications
BluetoothGattDescriptor cccd = chr.getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
if (cccd != null) {
enableNotification(chr.getUuid());
}
}
}
return true;
}
}
if ((mGatt.getServices().size() == 0) && mIsChromebook && !mIsReconnecting) {
Log.e(TAG, "Chromebook: Discovered services were empty; this almost certainly means the BtGatt.ContextMap bug has bitten us.");
mIsConnected = false;
mIsReconnecting = true;
mGatt.disconnect();
mGatt = connectGatt(false);
}
return false;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
private void finishCurrentGattOperation() {
GattOperation op = null;
synchronized (mOperations) {
if (mCurrentOperation != null) {
op = mCurrentOperation;
mCurrentOperation = null;
}
}
if (op != null) {
boolean result = op.finish(); // TODO: Maybe in main thread as well?
// Our operation failed, let's add it back to the beginning of our queue.
if (!result) {
mOperations.addFirst(op);
}
}
executeNextGattOperation();
}
private void executeNextGattOperation() {
synchronized (mOperations) {
if (mCurrentOperation != null)
return;
if (mOperations.isEmpty())
return;
mCurrentOperation = mOperations.removeFirst();
}
Runnable gattOperationRunnable = new Runnable() {
@Override
public void run() {
synchronized (mOperations) {
if (mCurrentOperation == null) {
Log.e(TAG, "Current operation null in executor?");
return;
}
mCurrentOperation.run();
// now wait for the GATT callback and when it comes, finish this operation
}
}
};
if (mCurrentOperation.getDelayMs() == 0) {
// Run in main thread
mHandler.post(gattOperationRunnable);
}
else {
// If we have a delay on this operation, wait before we post it.
mHandler.postDelayed(gattOperationRunnable, mCurrentOperation.getDelayMs());
}
}
private void queueGattOperation(GattOperation op) {
synchronized (mOperations) {
mOperations.add(op);
}
executeNextGattOperation();
}
private void enableNotification(UUID chrUuid) {
// Add a 500ms delay to notification write for Amazon Fire TV devices, as otherwise if we do this too quickly after connecting
// it will return success and then silently drop the operation on the floor.
GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid, 500);
queueGattOperation(op);
// Amazon Fire devices can also silently timeout on writeDescriptor, so
// set up a little delayed check that will attempt to write a second time.
//
// While this only seems to be needed on Amazon Fire TV devices at present, it
// doesn't hurt to have a retry on other devices as well.
//
final HIDDeviceBLESteamController finalThis = this;
final UUID finalUuid = chrUuid;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
if (!finalThis.mHasEnabledNotifications) {
if (finalThis.mHasSeenInputUpdate) {
// Amazon Five devices may have enabled notifications on the input characteristic and not given us a callback. If we've seen
// input reports, though, somewhat by definition notifications are enabled.
Log.w(TAG, "WriteDescriptor has never returned, but we've seen input reports. Moving on with controller initialization.");
finalThis.mHasEnabledNotifications = true;
finalThis.enableValveMode();
return;
}
// Give one more try.
GattOperation retry = HIDDeviceBLESteamController.GattOperation.enableNotification(finalThis.mGatt, finalUuid, 500);
finalThis.queueGattOperation(retry);
}
}
}, 1000);
}
void writeCharacteristic(UUID uuid, byte[] value) {
GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value);
queueGattOperation(op);
}
void readCharacteristic(UUID uuid) {
GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid);
queueGattOperation(op);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
////////////// BluetoothGattCallback overridden methods
//////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onConnectionStateChange(BluetoothGatt g, int status, int newState) {
//Log.v(TAG, "onConnectionStateChange status=" + status + " newState=" + newState);
mIsReconnecting = false;
if (newState == 2) {
mIsConnected = true;
// Run directly, without GattOperation
if (!isRegistered()) {
mHandler.post(new Runnable() {
@Override
public void run() {
mGatt.discoverServices();
}
});
}
}
else if (newState == 0) {
mIsConnected = false;
}
// Disconnection is handled in SteamLink using the ACTION_ACL_DISCONNECTED Intent.
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
//Log.v(TAG, "onServicesDiscovered status=" + status);
if (status == 0) {
if (gatt.getServices().size() == 0) {
Log.v(TAG, "onServicesDiscovered returned zero services; something has gone horribly wrong down in Android's Bluetooth stack.");
mIsReconnecting = true;
mIsConnected = false;
gatt.disconnect();
mGatt = connectGatt(false);
} else {
if (getProductId() == TRITON_BLE_PID) {
// Android will not properly play well with Data Length Extensions without manually requesting a large MTU,
// and Triton controllers require DLE support.
//
// 517 is basically a "magic number" as far as Android's bluetooth code is concerned, so do not change
// this value. It is functionally "please enable data length extensions" on some Android builds.
mGatt.requestMtu(517);
}
probeService(this);
}
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
//Log.v(TAG, "onCharacteristicRead status=" + status + " uuid=" + characteristic.getUuid());
if (characteristic.getUuid().equals(reportCharacteristic) && !mFrozen) {
mManager.HIDDeviceReportResponse(getId(), characteristic.getValue());
}
finishCurrentGattOperation();
}
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
//Log.v(TAG, "onCharacteristicWrite status=" + status + " uuid=" + characteristic.getUuid());
if (characteristic.getUuid().equals(reportCharacteristic)) {
// Only register controller with the native side once it has been fully configured
if (!isRegistered()) {
Log.v(TAG, "Registering Steam Controller with ID: " + getId());
mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true, mReportId);
setRegistered();
}
}
finishCurrentGattOperation();
}
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
// Enable this for verbose logging of controller input reports
//Log.v(TAG, "onCharacteristicChanged uuid=" + characteristic.getUuid() + " data=" + HexDump.dumpHexString(characteristic.getValue()));
if (characteristic.getUuid().equals(mInputCharacteristic) && !mFrozen) {
mHasSeenInputUpdate = true;
mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
}
}
@Override
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
//Log.v(TAG, "onDescriptorRead status=" + status);
}
private void enableValveMode()
{
BluetoothGattService valveService = mGatt.getService(steamControllerService);
if (valveService == null)
return;
BluetoothGattCharacteristic reportChr = valveService.getCharacteristic(reportCharacteristic);
if (reportChr != null) {
if (getProductId() == TRITON_BLE_PID) {
// For Triton we just mark things registered.
Log.v(TAG, "Registering Triton Steam Controller with ID: " + getId());
mManager.HIDDeviceConnected(getId(), getIdentifier(), getVendorId(), getProductId(), getSerialNumber(), getVersion(), getManufacturerName(), getProductName(), 0, 0, 0, 0, true, mReportId);
setRegistered();
} else {
// For the original controller, we need to manually enter Valve mode.
Log.v(TAG, "Writing report characteristic to enter valve mode");
reportChr.setValue(enterValveMode);
mGatt.writeCharacteristic(reportChr);
}
}
}
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
BluetoothGattCharacteristic chr = descriptor.getCharacteristic();
//Log.v(TAG, "onDescriptorWrite status=" + status + " uuid=" + chr.getUuid() + " descriptor=" + descriptor.getUuid());
if (chr.getUuid().equals(mInputCharacteristic)) {
mHasEnabledNotifications = true;
enableValveMode();
}
finishCurrentGattOperation();
}
@Override
public void onReliableWriteCompleted(BluetoothGatt gatt, int status) {
//Log.v(TAG, "onReliableWriteCompleted status=" + status);
}
@Override
public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) {
//Log.v(TAG, "onReadRemoteRssi status=" + status);
}
@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
//Log.v(TAG, "onMtuChanged status=" + status);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////// Public API
//////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public int getId() {
return mDeviceId;
}
@Override
public int getVendorId() {
// Valve Corporation
final int VALVE_USB_VID = 0x28DE;
return VALVE_USB_VID;
}
@Override
public int getProductId() {
if (mProductId > 0) {
// We've already set a product ID.
return mProductId;
}
if (mDevice.getName().startsWith("Steam Ctrl")) {
// We're a newer Triton device
mProductId = TRITON_BLE_PID;
} else {
// We're an OG Steam Controller
mProductId = D0G_BLE2_PID;
}
return mProductId;
}
@Override
public String getSerialNumber() {
// This will be read later via feature report by Steam
return "12345";
}
@Override
public int getVersion() {
return 0;
}
@Override
public String getManufacturerName() {
return "Valve Corporation";
}
@Override
public String getProductName() {
return "Steam Controller";
}
@Override
public UsbDevice getDevice() {
return null;
}
@Override
public boolean open() {
return true;
}
@Override
public int writeReport(byte[] report, boolean feature) {
if (!isRegistered()) {
Log.e(TAG, "Attempted writeReport before Steam Controller is registered!");
if (mIsConnected) {
probeService(this);
}
return -1;
}
if (feature) {
// We need to skip the first byte, as that doesn't go over the air
byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
//Log.v(TAG, "writeFeatureReport " + HexDump.dumpHexString(actual_report));
writeCharacteristic(reportCharacteristic, actual_report);
return report.length;
} else {
// If we're an original-recipe Steam Controller we just write to the characteristic directly.
if (getProductId() == D0G_BLE2_PID) {
//Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report));
writeCharacteristic(reportCharacteristic, report);
return report.length;
}
// If we're a Triton, we need to find the correct report characteristic.
if (report.length > 0) {
int reportId = report[0] & 0xFF;
BluetoothGattCharacteristic targetedReportCharacteristic = mOutputReportChars.get(reportId);
if (targetedReportCharacteristic != null) {
byte[] actual_report = Arrays.copyOfRange(report, 1, report.length - 1);
//Log.v(TAG, "writeOutputReport 0x" + Integer.toString(reportId, 16) + " " + HexDump.dumpHexString(report));
writeCharacteristic(targetedReportCharacteristic.getUuid(), actual_report);
return report.length;
} else {
Log.w(TAG, "Got report write request for unknown report type 0x" + Integer.toString(reportId, 16));
}
}
}
return -1;
}
@Override
public boolean readReport(byte[] report, boolean feature) {
if (!isRegistered()) {
Log.e(TAG, "Attempted readReport before Steam Controller is registered!");
if (mIsConnected) {
probeService(this);
}
return false;
}
if (feature) {
readCharacteristic(reportCharacteristic);
return true;
} else {
// Not implemented
return false;
}
}
@Override
public void close() {
}
@Override
public void setFrozen(boolean frozen) {
mFrozen = frozen;
}
@Override
public void shutdown() {
close();
BluetoothGatt g = mGatt;
if (g != null) {
g.disconnect();
g.close();
mGatt = null;
}
mManager = null;
mIsRegistered = false;
mIsConnected = false;
mOperations.clear();
}
}
@@ -1,698 +0,0 @@
package org.libsdl.app;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.os.Build;
import android.util.Log;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.hardware.usb.*;
import android.os.Handler;
import android.os.Looper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
public class HIDDeviceManager {
private static final String TAG = "hidapi";
private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION";
private static HIDDeviceManager sManager;
private static int sManagerRefCount = 0;
static public HIDDeviceManager acquire(Context context) {
if (sManagerRefCount == 0) {
sManager = new HIDDeviceManager(context);
}
++sManagerRefCount;
return sManager;
}
static public void release(HIDDeviceManager manager) {
if (manager == sManager) {
--sManagerRefCount;
if (sManagerRefCount == 0) {
sManager.close();
sManager = null;
}
}
}
private Context mContext;
private HashMap<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>();
private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>();
private int mNextDeviceId = 0;
private SharedPreferences mSharedPreferences = null;
private boolean mIsChromebook = false;
private UsbManager mUsbManager;
private Handler mHandler;
private BluetoothManager mBluetoothManager;
private List<BluetoothDevice> mLastBluetoothDevices;
private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDeviceAttached(usbDevice);
} else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDeviceDetached(usbDevice);
} else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false));
}
}
};
private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// Bluetooth device was connected. If it was a Steam Controller, handle it
if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Log.d(TAG, "Bluetooth device connected: " + device);
if (isSteamController(device)) {
connectBluetoothDevice(device);
}
}
// Bluetooth device was disconnected, remove from controller manager (if any)
if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Log.d(TAG, "Bluetooth device disconnected: " + device);
disconnectBluetoothDevice(device);
}
}
};
private HIDDeviceManager(final Context context) {
mContext = context;
HIDDeviceRegisterCallback();
mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE);
mIsChromebook = SDLActivity.isChromebook();
// if (shouldClear) {
// SharedPreferences.Editor spedit = mSharedPreferences.edit();
// spedit.clear();
// spedit.apply();
// }
// else
{
mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0);
}
}
Context getContext() {
return mContext;
}
int getDeviceIDForIdentifier(String identifier) {
SharedPreferences.Editor spedit = mSharedPreferences.edit();
int result = mSharedPreferences.getInt(identifier, 0);
if (result == 0) {
result = mNextDeviceId++;
spedit.putInt("next_device_id", mNextDeviceId);
}
spedit.putInt(identifier, result);
spedit.apply();
return result;
}
private void initializeUSB() {
mUsbManager = (UsbManager)mContext.getSystemService(Context.USB_SERVICE);
if (mUsbManager == null) {
return;
}
/*
// Logging
for (UsbDevice device : mUsbManager.getDeviceList().values()) {
Log.i(TAG,"Path: " + device.getDeviceName());
Log.i(TAG,"Manufacturer: " + device.getManufacturerName());
Log.i(TAG,"Product: " + device.getProductName());
Log.i(TAG,"ID: " + device.getDeviceId());
Log.i(TAG,"Class: " + device.getDeviceClass());
Log.i(TAG,"Protocol: " + device.getDeviceProtocol());
Log.i(TAG,"Vendor ID " + device.getVendorId());
Log.i(TAG,"Product ID: " + device.getProductId());
Log.i(TAG,"Interface count: " + device.getInterfaceCount());
Log.i(TAG,"---------------------------------------");
// Get interface details
for (int index = 0; index < device.getInterfaceCount(); index++) {
UsbInterface mUsbInterface = device.getInterface(index);
Log.i(TAG," ***** *****");
Log.i(TAG," Interface index: " + index);
Log.i(TAG," Interface ID: " + mUsbInterface.getId());
Log.i(TAG," Interface class: " + mUsbInterface.getInterfaceClass());
Log.i(TAG," Interface subclass: " + mUsbInterface.getInterfaceSubclass());
Log.i(TAG," Interface protocol: " + mUsbInterface.getInterfaceProtocol());
Log.i(TAG," Endpoint count: " + mUsbInterface.getEndpointCount());
// Get endpoint details
for (int epi = 0; epi < mUsbInterface.getEndpointCount(); epi++)
{
UsbEndpoint mEndpoint = mUsbInterface.getEndpoint(epi);
Log.i(TAG," ++++ ++++ ++++");
Log.i(TAG," Endpoint index: " + epi);
Log.i(TAG," Attributes: " + mEndpoint.getAttributes());
Log.i(TAG," Direction: " + mEndpoint.getDirection());
Log.i(TAG," Number: " + mEndpoint.getEndpointNumber());
Log.i(TAG," Interval: " + mEndpoint.getInterval());
Log.i(TAG," Packet size: " + mEndpoint.getMaxPacketSize());
Log.i(TAG," Type: " + mEndpoint.getType());
}
}
}
Log.i(TAG," No more devices connected.");
*/
// Register for USB broadcasts and permission completions
IntentFilter filter = new IntentFilter();
filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
filter.addAction(HIDDeviceManager.ACTION_USB_PERMISSION);
if (Build.VERSION.SDK_INT >= 33) { /* Android 13.0 (TIRAMISU) */
mContext.registerReceiver(mUsbBroadcast, filter, Context.RECEIVER_EXPORTED);
} else {
mContext.registerReceiver(mUsbBroadcast, filter);
}
for (UsbDevice usbDevice : mUsbManager.getDeviceList().values()) {
handleUsbDeviceAttached(usbDevice);
}
}
UsbManager getUSBManager() {
return mUsbManager;
}
private void shutdownUSB() {
try {
mContext.unregisterReceiver(mUsbBroadcast);
} catch (Exception e) {
// We may not have registered, that's okay
}
}
private boolean isHIDDeviceInterface(UsbDevice usbDevice, UsbInterface usbInterface) {
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_HID) {
return true;
}
if (isXbox360Controller(usbDevice, usbInterface) || isXboxOneController(usbDevice, usbInterface)) {
return true;
}
return false;
}
private boolean isXbox360Controller(UsbDevice usbDevice, UsbInterface usbInterface) {
final int XB360_IFACE_SUBCLASS = 93;
final int XB360_IFACE_PROTOCOL = 1; // Wired
final int XB360W_IFACE_PROTOCOL = 129; // Wireless
final int[] SUPPORTED_VENDORS = {
0x0079, // GPD Win 2
0x044f, // Thrustmaster
0x045e, // Microsoft
0x046d, // Logitech
0x056e, // Elecom
0x06a3, // Saitek
0x0738, // Mad Catz
0x07ff, // Mad Catz
0x0e6f, // PDP
0x0f0d, // Hori
0x1038, // SteelSeries
0x11c9, // Nacon
0x12ab, // Unknown
0x1430, // RedOctane
0x146b, // BigBen
0x1532, // Razer Sabertooth
0x15e4, // Numark
0x162e, // Joytech
0x1689, // Razer Onza
0x1949, // Lab126, Inc.
0x1bad, // Harmonix
0x20d6, // PowerA
0x24c6, // PowerA
0x2c22, // Qanba
0x2dc8, // 8BitDo
0x3537, // GameSir
0x37d7, // Flydigi
0x9886, // ASTRO Gaming
};
if (usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
usbInterface.getInterfaceSubclass() == XB360_IFACE_SUBCLASS &&
(usbInterface.getInterfaceProtocol() == XB360_IFACE_PROTOCOL ||
usbInterface.getInterfaceProtocol() == XB360W_IFACE_PROTOCOL)) {
int vendor_id = usbDevice.getVendorId();
for (int supportedVid : SUPPORTED_VENDORS) {
if (vendor_id == supportedVid) {
return true;
}
}
}
return false;
}
private boolean isXboxOneController(UsbDevice usbDevice, UsbInterface usbInterface) {
final int XB1_IFACE_SUBCLASS = 71;
final int XB1_IFACE_PROTOCOL = 208;
final int[] SUPPORTED_VENDORS = {
0x03f0, // HP
0x044f, // Thrustmaster
0x045e, // Microsoft
0x0738, // Mad Catz
0x0b05, // ASUS
0x0e6f, // PDP
0x0f0d, // Hori
0x10f5, // Turtle Beach
0x1532, // Razer Wildcat
0x20d6, // PowerA
0x24c6, // PowerA
0x294b, // Snakebyte
0x2dc8, // 8BitDo
0x2e24, // Hyperkin
0x2e95, // SCUF
0x3285, // Nacon
0x3537, // GameSir
0x366c, // ByoWave
};
if (usbInterface.getId() == 0 &&
usbInterface.getInterfaceClass() == UsbConstants.USB_CLASS_VENDOR_SPEC &&
usbInterface.getInterfaceSubclass() == XB1_IFACE_SUBCLASS &&
usbInterface.getInterfaceProtocol() == XB1_IFACE_PROTOCOL) {
int vendor_id = usbDevice.getVendorId();
for (int supportedVid : SUPPORTED_VENDORS) {
if (vendor_id == supportedVid) {
return true;
}
}
}
return false;
}
private void handleUsbDeviceAttached(UsbDevice usbDevice) {
connectHIDDeviceUSB(usbDevice);
}
private void handleUsbDeviceDetached(UsbDevice usbDevice) {
List<Integer> devices = new ArrayList<Integer>();
for (HIDDevice device : mDevicesById.values()) {
if (usbDevice.equals(device.getDevice())) {
devices.add(device.getId());
}
}
for (int id : devices) {
HIDDevice device = mDevicesById.get(id);
mDevicesById.remove(id);
device.shutdown();
HIDDeviceDisconnected(id);
}
}
private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) {
for (HIDDevice device : mDevicesById.values()) {
if (usbDevice.equals(device.getDevice())) {
boolean opened = false;
if (permission_granted) {
opened = device.open();
}
HIDDeviceOpenResult(device.getId(), opened);
}
}
}
private void connectHIDDeviceUSB(UsbDevice usbDevice) {
synchronized (this) {
int interface_mask = 0;
for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) {
UsbInterface usbInterface = usbDevice.getInterface(interface_index);
if (isHIDDeviceInterface(usbDevice, usbInterface)) {
// Check to see if we've already added this interface
// This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive
int interface_id = usbInterface.getId();
if ((interface_mask & (1 << interface_id)) != 0) {
continue;
}
interface_mask |= (1 << interface_id);
HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index);
int id = device.getId();
mDevicesById.put(id, device);
HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol(), false, 0);
}
}
}
}
private void initializeBluetooth() {
Log.d(TAG, "Initializing Bluetooth");
if (Build.VERSION.SDK_INT >= 31 /* Android 12 */ &&
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH_CONNECT, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH_CONNECT");
return;
}
if (Build.VERSION.SDK_INT <= 30 /* Android 11.0 (R) */ &&
mContext.getPackageManager().checkPermission(android.Manifest.permission.BLUETOOTH, mContext.getPackageName()) != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Couldn't initialize Bluetooth, missing android.permission.BLUETOOTH");
return;
}
if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
Log.d(TAG, "Couldn't initialize Bluetooth, this version of Android does not support Bluetooth LE");
return;
}
// Find bonded bluetooth controllers and create SteamControllers for them
mBluetoothManager = (BluetoothManager)mContext.getSystemService(Context.BLUETOOTH_SERVICE);
if (mBluetoothManager == null) {
// This device doesn't support Bluetooth.
return;
}
BluetoothAdapter btAdapter = mBluetoothManager.getAdapter();
if (btAdapter == null) {
// This device has Bluetooth support in the codebase, but has no available adapters.
return;
}
// Get our bonded devices.
for (BluetoothDevice device : btAdapter.getBondedDevices()) {
Log.d(TAG, "Bluetooth device available: " + device);
if (isSteamController(device)) {
connectBluetoothDevice(device);
}
}
// NOTE: These don't work on Chromebooks, to my undying dismay.
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED);
filter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED);
if (Build.VERSION.SDK_INT >= 33) { /* Android 13.0 (TIRAMISU) */
mContext.registerReceiver(mBluetoothBroadcast, filter, Context.RECEIVER_EXPORTED);
} else {
mContext.registerReceiver(mBluetoothBroadcast, filter);
}
if (mIsChromebook) {
mHandler = new Handler(Looper.getMainLooper());
mLastBluetoothDevices = new ArrayList<BluetoothDevice>();
// final HIDDeviceManager finalThis = this;
// mHandler.postDelayed(new Runnable() {
// @Override
// public void run() {
// finalThis.chromebookConnectionHandler();
// }
// }, 5000);
}
}
private void shutdownBluetooth() {
try {
mContext.unregisterReceiver(mBluetoothBroadcast);
} catch (Exception e) {
// We may not have registered, that's okay
}
}
// Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly.
// This function provides a sort of dummy version of that, watching for changes in the
// connected devices and attempting to add controllers as things change.
void chromebookConnectionHandler() {
if (!mIsChromebook) {
return;
}
ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>();
ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>();
List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
for (BluetoothDevice bluetoothDevice : currentConnected) {
if (!mLastBluetoothDevices.contains(bluetoothDevice)) {
connected.add(bluetoothDevice);
}
}
for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) {
if (!currentConnected.contains(bluetoothDevice)) {
disconnected.add(bluetoothDevice);
}
}
mLastBluetoothDevices = currentConnected;
for (BluetoothDevice bluetoothDevice : disconnected) {
disconnectBluetoothDevice(bluetoothDevice);
}
for (BluetoothDevice bluetoothDevice : connected) {
connectBluetoothDevice(bluetoothDevice);
}
final HIDDeviceManager finalThis = this;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
finalThis.chromebookConnectionHandler();
}
}, 10000);
}
boolean connectBluetoothDevice(BluetoothDevice bluetoothDevice) {
Log.v(TAG, "connectBluetoothDevice device=" + bluetoothDevice);
synchronized (this) {
if (mBluetoothDevices.containsKey(bluetoothDevice)) {
Log.v(TAG, "Steam controller with address " + bluetoothDevice + " already exists, attempting reconnect");
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
device.reconnect();
return false;
}
HIDDeviceBLESteamController device = new HIDDeviceBLESteamController(this, bluetoothDevice);
int id = device.getId();
mBluetoothDevices.put(bluetoothDevice, device);
mDevicesById.put(id, device);
// The Steam Controller will mark itself connected once initialization is complete
}
return true;
}
void disconnectBluetoothDevice(BluetoothDevice bluetoothDevice) {
synchronized (this) {
HIDDeviceBLESteamController device = mBluetoothDevices.get(bluetoothDevice);
if (device == null)
return;
int id = device.getId();
mBluetoothDevices.remove(bluetoothDevice);
mDevicesById.remove(id);
device.shutdown();
HIDDeviceDisconnected(id);
}
}
boolean isSteamController(BluetoothDevice bluetoothDevice) {
// Sanity check. If you pass in a null device, by definition it is never a Steam Controller.
if (bluetoothDevice == null) {
return false;
}
// If the device has no local name, we really don't want to try an equality check against it.
if (bluetoothDevice.getName() == null) {
return false;
}
// Steam Controllers will always support Bluetooth Low Energy
if ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) == 0) {
return false;
}
// Match on the name either the original Steam Controller or the new second-generation one advertise with.
return bluetoothDevice.getName().equals("SteamController") || bluetoothDevice.getName().startsWith("Steam Ctrl");
}
private void close() {
shutdownUSB();
shutdownBluetooth();
synchronized (this) {
for (HIDDevice device : mDevicesById.values()) {
device.shutdown();
}
mDevicesById.clear();
mBluetoothDevices.clear();
HIDDeviceReleaseCallback();
}
}
public void setFrozen(boolean frozen) {
synchronized (this) {
for (HIDDevice device : mDevicesById.values()) {
device.setFrozen(frozen);
}
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////
private HIDDevice getDevice(int id) {
synchronized (this) {
HIDDevice result = mDevicesById.get(id);
if (result == null) {
Log.v(TAG, "No device for id: " + id);
Log.v(TAG, "Available devices: " + mDevicesById.keySet());
}
return result;
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
////////// JNI interface functions
//////////////////////////////////////////////////////////////////////////////////////////////////////
boolean initialize(boolean usb, boolean bluetooth) {
Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")");
if (usb) {
initializeUSB();
}
if (bluetooth) {
initializeBluetooth();
}
return true;
}
boolean openDevice(int deviceID) {
Log.v(TAG, "openDevice deviceID=" + deviceID);
HIDDevice device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return false;
}
// Look to see if this is a USB device and we have permission to access it
UsbDevice usbDevice = device.getDevice();
if (usbDevice != null && !mUsbManager.hasPermission(usbDevice)) {
HIDDeviceOpenPending(deviceID);
try {
final int FLAG_MUTABLE = 0x02000000; // PendingIntent.FLAG_MUTABLE, but don't require SDK 31
int flags;
if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
flags = FLAG_MUTABLE;
} else {
flags = 0;
}
Intent intent = new Intent(HIDDeviceManager.ACTION_USB_PERMISSION);
intent.setPackage(mContext.getPackageName());
mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, intent, flags));
} catch (Exception e) {
Log.v(TAG, "Couldn't request permission for USB device " + usbDevice);
HIDDeviceOpenResult(deviceID, false);
}
return false;
}
try {
return device.open();
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return false;
}
int writeReport(int deviceID, byte[] report, boolean feature) {
try {
//Log.v(TAG, "writeReport deviceID=" + deviceID + " length=" + report.length);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return -1;
}
return device.writeReport(report, feature);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return -1;
}
boolean readReport(int deviceID, byte[] report, boolean feature) {
try {
//Log.v(TAG, "readReport deviceID=" + deviceID);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return false;
}
return device.readReport(report, feature);
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
return false;
}
void closeDevice(int deviceID) {
try {
Log.v(TAG, "closeDevice deviceID=" + deviceID);
HIDDevice device;
device = getDevice(deviceID);
if (device == null) {
HIDDeviceDisconnected(deviceID);
return;
}
device.close();
} catch (Exception e) {
Log.e(TAG, "Got exception: " + Log.getStackTraceString(e));
}
}
//////////////////////////////////////////////////////////////////////////////////////////////////////
/////////////// Native methods
//////////////////////////////////////////////////////////////////////////////////////////////////////
private native void HIDDeviceRegisterCallback();
private native void HIDDeviceReleaseCallback();
native void HIDDeviceConnected(int deviceID, String identifier, int vendorId, int productId, String serial_number, int release_number, String manufacturer_string, String product_string, int interface_number, int interface_class, int interface_subclass, int interface_protocol, boolean bBluetooth, int reportID);
native void HIDDeviceOpenPending(int deviceID);
native void HIDDeviceOpenResult(int deviceID, boolean opened);
native void HIDDeviceDisconnected(int deviceID);
native void HIDDeviceInputReport(int deviceID, byte[] report);
native void HIDDeviceReportResponse(int deviceID, byte[] report);
}
@@ -1,354 +0,0 @@
package org.libsdl.app;
import android.hardware.usb.*;
import android.os.Build;
import android.util.Log;
import java.util.Arrays;
import java.util.Locale;
class HIDDeviceUSB implements HIDDevice {
private static final String TAG = "hidapi";
protected HIDDeviceManager mManager;
protected UsbDevice mDevice;
protected int mInterfaceIndex;
protected int mInterface;
protected int mDeviceId;
protected UsbDeviceConnection mConnection;
protected UsbEndpoint mInputEndpoint;
protected UsbEndpoint mOutputEndpoint;
protected InputThread mInputThread;
protected boolean mRunning;
protected boolean mFrozen;
protected boolean mClaimed;
public HIDDeviceUSB(HIDDeviceManager manager, UsbDevice usbDevice, int interface_index) {
mManager = manager;
mDevice = usbDevice;
mInterfaceIndex = interface_index;
mInterface = mDevice.getInterface(mInterfaceIndex).getId();
mDeviceId = manager.getDeviceIDForIdentifier(getIdentifier());
mRunning = false;
mClaimed = false;
}
String getIdentifier() {
return String.format(Locale.ENGLISH, "%s/%x/%x/%d", mDevice.getDeviceName(), mDevice.getVendorId(), mDevice.getProductId(), mInterfaceIndex);
}
@Override
public int getId() {
return mDeviceId;
}
@Override
public int getVendorId() {
return mDevice.getVendorId();
}
@Override
public int getProductId() {
return mDevice.getProductId();
}
@Override
public String getSerialNumber() {
String result = null;
try {
result = mDevice.getSerialNumber();
}
catch (SecurityException exception) {
//Log.w(TAG, "App permissions mean we cannot get serial number for device " + getDeviceName() + " message: " + exception.getMessage());
}
if (result == null) {
result = "";
}
return result;
}
@Override
public int getVersion() {
return 0;
}
@Override
public String getManufacturerName() {
String result;
result = mDevice.getManufacturerName();
if (result == null) {
result = String.format("%x", getVendorId());
}
return result;
}
@Override
public String getProductName() {
String result;
result = mDevice.getProductName();
if (result == null) {
result = String.format("%x", getProductId());
}
return result;
}
@Override
public UsbDevice getDevice() {
return mDevice;
}
String getDeviceName() {
return getManufacturerName() + " " + getProductName() + "(0x" + String.format("%x", getVendorId()) + "/0x" + String.format("%x", getProductId()) + ")";
}
@Override
public boolean open() {
mConnection = mManager.getUSBManager().openDevice(mDevice);
if (mConnection == null) {
Log.w(TAG, "Unable to open USB device " + getDeviceName());
return false;
}
// Force claim our interface
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
if (!mConnection.claimInterface(iface, true)) {
Log.w(TAG, "Failed to claim interfaces on USB device " + getDeviceName());
close();
return false;
}
mClaimed = true;
// Find the endpoints
for (int j = 0; j < iface.getEndpointCount(); j++) {
UsbEndpoint endpt = iface.getEndpoint(j);
switch (endpt.getDirection()) {
case UsbConstants.USB_DIR_IN:
if (mInputEndpoint == null) {
mInputEndpoint = endpt;
}
break;
case UsbConstants.USB_DIR_OUT:
if (mOutputEndpoint == null) {
mOutputEndpoint = endpt;
}
break;
}
}
// Make sure the required endpoints were present. The original Steam Controller and the wireless dongle for it do NOT
// actually have -- or require -- output endpoints, so we need to accept only an input one for them or else we'll fall
// back to the Android system gamepad functionality (and lose our paddles et al).
if (mInputEndpoint == null) {
Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName());
mConnection.releaseInterface(iface);
close();
return false;
}
// Start listening for input
mRunning = true;
mInputThread = new InputThread();
mInputThread.start();
return true;
}
@Override
public int writeReport(byte[] report, boolean feature) {
if (mConnection == null) {
Log.w(TAG, "writeReport() called with no device connection");
return -1;
}
if (!mClaimed) {
Log.w(TAG, "writeReport() called but some other process currently owns the USB device");
return -1;
}
if (feature) {
int res = -1;
int offset = 0;
int length = report.length;
boolean skipped_report_id = false;
byte report_number = report[0];
if (report_number == 0x0) {
++offset;
--length;
skipped_report_id = true;
}
res = mConnection.controlTransfer(
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_OUT,
0x09/*HID set_report*/,
(3/*HID feature*/ << 8) | report_number,
mInterface,
report, offset, length,
1000/*timeout millis*/);
if (res < 0) {
Log.w(TAG, "writeFeatureReport() returned " + res + " on device " + getDeviceName());
return -1;
}
if (skipped_report_id) {
++length;
}
return length;
} else {
if (mOutputEndpoint == null)
{
Log.e(TAG, "Tried to write an output report to an interface with no output endpoint!");
return -1;
}
int res = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000);
if (res != report.length) {
Log.w(TAG, "writeOutputReport() returned " + res + " on device " + getDeviceName());
}
return res;
}
}
@Override
public boolean readReport(byte[] report, boolean feature) {
int res = -1;
int offset = 0;
int length = report.length;
boolean skipped_report_id = false;
byte report_number = report[0];
if (mConnection == null) {
Log.w(TAG, "readReport() called with no device connection");
return false;
}
if (!mClaimed) {
if (feature) {
return false;
}
return true;
}
if (report_number == 0x0) {
/* Offset the return buffer by 1, so that the report ID
will remain in byte 0. */
++offset;
--length;
skipped_report_id = true;
}
res = mConnection.controlTransfer(
UsbConstants.USB_TYPE_CLASS | 0x01 /*RECIPIENT_INTERFACE*/ | UsbConstants.USB_DIR_IN,
0x01/*HID get_report*/,
((feature ? 3/*HID feature*/ : 1/*HID Input*/) << 8) | report_number,
mInterface,
report, offset, length,
1000/*timeout millis*/);
if (res < 0) {
Log.w(TAG, "getFeatureReport() returned " + res + " on device " + getDeviceName());
return false;
}
if (skipped_report_id) {
++res;
++length;
}
byte[] data;
if (res == length) {
data = report;
} else {
data = Arrays.copyOfRange(report, 0, res);
}
mManager.HIDDeviceReportResponse(mDeviceId, data);
return true;
}
@Override
public void close() {
mRunning = false;
if (mInputThread != null) {
while (mInputThread.isAlive()) {
mInputThread.interrupt();
try {
mInputThread.join();
} catch (InterruptedException e) {
// Keep trying until we're done
}
}
mInputThread = null;
}
if (mConnection != null) {
if (mClaimed) {
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
mConnection.releaseInterface(iface);
}
mConnection.close();
mConnection = null;
mClaimed = false;
}
}
@Override
public void shutdown() {
close();
mManager = null;
}
@Override
public void setFrozen(boolean frozen) {
mFrozen = frozen;
/* If we have a valid device connection and the claim state doesn't match what we want, try to correct that. */
if (mConnection != null && mClaimed == mFrozen) {
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
if (frozen) {
mClaimed = !mConnection.releaseInterface(iface);
if (mClaimed) {
Log.e(TAG, "Tried to release claim on USB device, but failed!");
}
} else {
mClaimed = mConnection.claimInterface(iface, true);
if (!mClaimed) {
Log.e(TAG, "Tried to regain claim on USB device, but failed!");
}
}
}
}
protected class InputThread extends Thread {
@Override
public void run() {
int packetSize = mInputEndpoint.getMaxPacketSize();
byte[] packet = new byte[packetSize];
while (mRunning) {
int r;
try
{
r = mConnection.bulkTransfer(mInputEndpoint, packet, packetSize, 1000);
}
catch (Exception e)
{
Log.v(TAG, "Exception in UsbDeviceConnection bulktransfer: " + e);
break;
}
if (r < 0) {
// Could be a timeout or an I/O error
}
if (r > 0) {
byte[] data;
if (r == packetSize) {
data = packet;
} else {
data = Arrays.copyOfRange(packet, 0, r);
}
if (!mFrozen) {
mManager.HIDDeviceInputReport(mDeviceId, data);
}
}
}
}
}
}
@@ -1,90 +0,0 @@
package org.libsdl.app;
import android.app.Activity;
import android.content.Context;
import java.lang.reflect.Method;
/**
SDL library initialization
*/
public class SDL {
// This function should be called first and sets up the native code
// so it can call into the Java classes
static public void setupJNI() {
SDLActivity.nativeSetupJNI();
SDLAudioManager.nativeSetupJNI();
SDLControllerManager.nativeSetupJNI();
}
// This function should be called each time the activity is started
static public void initialize() {
setContext(null);
SDLActivity.initialize();
SDLAudioManager.initialize();
SDLControllerManager.initialize();
}
// This function stores the current activity (SDL or not)
static public void setContext(Activity context) {
SDLAudioManager.setContext(context);
mContext = context;
}
static public Activity getContext() {
return mContext;
}
static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
loadLibrary(libraryName, mContext);
}
static void loadLibrary(String libraryName, Context context) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
if (libraryName == null) {
throw new NullPointerException("No library name provided.");
}
try {
// Let's see if we have ReLinker available in the project. This is necessary for
// some projects that have huge numbers of local libraries bundled, and thus may
// trip a bug in Android's native library loader which ReLinker works around. (If
// loadLibrary works properly, ReLinker will simply use the normal Android method
// internally.)
//
// To use ReLinker, just add it as a dependency. For more information, see
// https://github.com/KeepSafe/ReLinker for ReLinker's repository.
//
Class<?> relinkClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
Class<?> relinkListenerClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
Class<?> contextClass = context.getClassLoader().loadClass("android.content.Context");
Class<?> stringClass = context.getClassLoader().loadClass("java.lang.String");
// Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if
// they've changed during updates.
Method forceMethod = relinkClass.getDeclaredMethod("force");
Object relinkInstance = forceMethod.invoke(null);
Class<?> relinkInstanceClass = relinkInstance.getClass();
// Actually load the library!
Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass);
loadMethod.invoke(relinkInstance, context, libraryName, null, null);
}
catch (final Throwable e) {
// Fall back
try {
System.loadLibrary(libraryName);
}
catch (final UnsatisfiedLinkError ule) {
throw ule;
}
catch (final SecurityException se) {
throw se;
}
}
}
protected static Activity mContext;
}

Some files were not shown because too many files have changed in this diff Show More