mirror of
https://github.com/TwilitRealm/dusklight
synced 2026-07-03 19:10:19 -04:00
Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3bcb07dba4 | |||
| 86bea34bb2 | |||
| 09e9c6ad34 | |||
| 54db8e4b5b | |||
| ec091ac3af | |||
| 35650784c3 | |||
| 86c1e21ac1 | |||
| a2f8fffc8d | |||
| ddfe21b18a | |||
| 93450dce7b | |||
| 38cfaae940 | |||
| 7e56747e33 | |||
| 72a97d62d8 | |||
| 130e5c9502 | |||
| 505ef363fd | |||
| b7f3dbb8b3 | |||
| 27d95c37d5 | |||
| 588910c642 | |||
| 00803b53ab | |||
| 4b8248b130 | |||
| d32dc7481e | |||
| e08807f44f | |||
| 8ea0352fed | |||
| 7f6696715a | |||
| c27a2a3b75 | |||
| 863ce9117d | |||
| c5c7e89b13 | |||
| 0e25434c17 | |||
| 48c9f1e984 | |||
| 62fc79425e | |||
| 266f36118f | |||
| c6e67daeac | |||
| 03a46635a0 | |||
| 3f92989af8 | |||
| b5d2ded2ca | |||
| 5155270fd3 | |||
| 7136c90336 | |||
| 50a8ebab28 | |||
| d865c82f76 | |||
| 3252d70276 | |||
| b3f2ae63b3 | |||
| ad94d9656f | |||
| 49215dbc7b | |||
| fba3114d4f | |||
| b3cc9ba02e | |||
| a3a36508d6 | |||
| 143aa51eb3 | |||
| e3b3eabfeb | |||
| f15a4f07e5 | |||
| 277d16c110 | |||
| 18995f3d7c | |||
| 5e6d240d0f | |||
| fbec777b0a | |||
| 8cd8d1b90c | |||
| fc659b966f | |||
| 7e1e193f64 | |||
| 75a4fe4429 | |||
| 6a07f82425 | |||
| 1a91c77a64 | |||
| 0649aa4828 | |||
| 4ec4cd363e | |||
| d01f6bafff | |||
| 627cf559c9 | |||
| 7e797b2320 | |||
| 228a305110 |
+134
-14
@@ -48,12 +48,17 @@ else ()
|
||||
message(STATUS "Unable to find git, commit information will not be available")
|
||||
endif ()
|
||||
|
||||
if (DUSK_WC_DESCRIBE)
|
||||
string(REGEX REPLACE "v([0-9]+)\.([0-9]+)\.([0-9]+)\-([0-9]+).*" "\\1.\\2.\\3.\\4" DUSK_VERSION_STRING "${DUSK_WC_DESCRIBE}")
|
||||
string(REGEX REPLACE "v([0-9]+)\.([0-9]+)\.([0-9]+).*" "\\1.\\2.\\3" DUSK_SHORT_VERSION_STRING "${DUSK_WC_DESCRIBE}")
|
||||
if (DUSK_WC_DESCRIBE MATCHES "^v([0-9]+)\\.([0-9]+)\\.([0-9]+)(-([0-9]+).*)?$")
|
||||
set(DUSK_SHORT_VERSION_STRING "${CMAKE_MATCH_1}.${CMAKE_MATCH_2}.${CMAKE_MATCH_3}")
|
||||
if (CMAKE_MATCH_5)
|
||||
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.${CMAKE_MATCH_5}")
|
||||
else ()
|
||||
set(DUSK_VERSION_STRING "${DUSK_SHORT_VERSION_STRING}.0")
|
||||
endif ()
|
||||
else ()
|
||||
set(DUSK_WC_DESCRIBE "UNKNOWN-VERSION")
|
||||
set(DUSK_VERSION_STRING "0.0.0")
|
||||
set(DUSK_VERSION_STRING "0.0.0.0")
|
||||
set(DUSK_SHORT_VERSION_STRING "0.0.0")
|
||||
endif ()
|
||||
|
||||
# Add version information to CI environment variables
|
||||
@@ -95,6 +100,18 @@ 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)
|
||||
|
||||
if(ANDROID)
|
||||
set(DUSK_MOVIE_SUPPORT OFF)
|
||||
set(NOD_COMPRESS_BZIP2 OFF CACHE BOOL "" FORCE)
|
||||
set(NOD_COMPRESS_LZMA OFF CACHE BOOL "" FORCE)
|
||||
set(NOD_COMPRESS_ZLIB OFF CACHE BOOL "" FORCE)
|
||||
set(NOD_COMPRESS_ZSTD OFF CACHE BOOL "" FORCE)
|
||||
endif ()
|
||||
|
||||
option(DUSK_ENABLE_SENTRY_NATIVE "Enable sentry-native crash reporting support" OFF)
|
||||
set(DUSK_SENTRY_DSN "" CACHE STRING "Sentry DSN")
|
||||
set(DUSK_SENTRY_ENVIRONMENT "development" CACHE STRING "Sentry environment")
|
||||
|
||||
if (DUSK_MOVIE_SUPPORT)
|
||||
find_package(libjpeg-turbo QUIET)
|
||||
if (libjpeg-turbo_FOUND)
|
||||
@@ -148,21 +165,23 @@ elseif (APPLE)
|
||||
set(CMAKE_INSTALL_RPATH "$ORIGIN")
|
||||
set(CMAKE_BUILD_RPATH "$ORIGIN")
|
||||
elseif (MSVC)
|
||||
add_compile_options(/bigobj)
|
||||
add_compile_options(/Zc:strictStrings-)
|
||||
add_compile_options(/MP)
|
||||
add_compile_options(/FS)
|
||||
add_compile_options(
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:/bigobj>
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:/Zc:strictStrings->
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:/MP>
|
||||
$<$<COMPILE_LANGUAGE:C,CXX>:/FS>
|
||||
)
|
||||
|
||||
if (NOT DUSK_BUILD_WARNINGS)
|
||||
add_compile_options(/W0)
|
||||
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:/W0>)
|
||||
else ()
|
||||
# Disable warnings
|
||||
add_compile_options(/wd4068) # unknown pragma
|
||||
add_compile_options(/wd4291) # no matching delete operator, leaks if exception thrown
|
||||
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:/wd4068>) # unknown pragma
|
||||
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:/wd4291>) # no matching delete operator, leaks if exception thrown
|
||||
# Only show warnings once
|
||||
add_compile_options(/wo4244) # narrowing conversion, possible data loss
|
||||
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:/wo4244>) # narrowing conversion, possible data loss
|
||||
endif ()
|
||||
add_compile_options(/utf-8)
|
||||
add_compile_options($<$<COMPILE_LANGUAGE:C,CXX>:/utf-8>)
|
||||
endif ()
|
||||
|
||||
|
||||
@@ -183,6 +202,46 @@ FetchContent_Declare(json
|
||||
)
|
||||
FetchContent_MakeAvailable(cxxopts json)
|
||||
|
||||
if (DUSK_ENABLE_SENTRY_NATIVE)
|
||||
message(STATUS "dusk: 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(_dusk_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 ${_dusk_skip_install_rules})
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
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(${CMAKE_SOURCE_DIR}/version.h.in ${CMAKE_BINARY_DIR}/version.h)
|
||||
|
||||
include(files.cmake)
|
||||
@@ -191,6 +250,10 @@ include(files.cmake)
|
||||
|
||||
set(DUSK_BUNDLE_NAME Dusk)
|
||||
set(DUSK_BUNDLE_IDENTIFIER dev.decomp.dusk)
|
||||
set(DUSK_COMPANY_NAME "Twilit Realm")
|
||||
set(DUSK_FILE_DESCRIPTION "Dusk")
|
||||
set(DUSK_PRODUCT_NAME "Dusk")
|
||||
set(DUSK_COPYRIGHT "Copyright (C) Twilit Realm contributors")
|
||||
set(DUSK_GAME_NAME "GZ2E")
|
||||
set(DUSK_GAME_VERSION "01")
|
||||
set(DUSK_TP_VERSION ${DUSK_GAME_NAME}${DUSK_GAME_VERSION})
|
||||
@@ -216,6 +279,13 @@ set(GAME_INCLUDE_DIRS
|
||||
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)
|
||||
|
||||
list(APPEND GAME_LIBS libzstd_static)
|
||||
|
||||
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 (DUSK_MOVIE_SUPPORT)
|
||||
if (TARGET libjpeg-turbo::turbojpeg-static)
|
||||
list(APPEND GAME_LIBS libjpeg-turbo::turbojpeg-static)
|
||||
@@ -231,6 +301,10 @@ if (MSVC)
|
||||
add_link_options("/INCREMENTAL")
|
||||
endif ()
|
||||
|
||||
if(ANDROID)
|
||||
list(APPEND GAME_COMPILE_DEFS TARGET_ANDROID=1)
|
||||
endif ()
|
||||
|
||||
# game_debug is for game code files that we know work when compiled with DEBUG=1
|
||||
# Of course, if building a release build, this distinction is irrelevant
|
||||
add_library(game_debug OBJECT ${JSYSTEM_DEBUG_FILES} ${SSYSTEM_FILES}
|
||||
@@ -264,10 +338,25 @@ add_library(game STATIC
|
||||
$<TARGET_OBJECTS:game_debug>)
|
||||
target_link_libraries(game PUBLIC ${GAME_LIBS})
|
||||
|
||||
add_executable(dusk src/dusk/main.cpp)
|
||||
if(ANDROID)
|
||||
add_library(dusk SHARED src/dusk/main.cpp)
|
||||
set_target_properties(dusk PROPERTIES OUTPUT_NAME main)
|
||||
else ()
|
||||
add_executable(dusk src/dusk/main.cpp)
|
||||
endif ()
|
||||
|
||||
target_compile_definitions(dusk PRIVATE TARGET_PC AVOID_UB=1 VERSION=0)
|
||||
target_include_directories(dusk PRIVATE include)
|
||||
target_link_libraries(dusk PRIVATE game aurora::main)
|
||||
if (TARGET crashpad_handler)
|
||||
add_dependencies(dusk 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(dusk PRIVATE "-Wl,-u,SDL_main")
|
||||
endif ()
|
||||
|
||||
add_custom_command(TARGET dusk POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
@@ -276,6 +365,35 @@ add_custom_command(TARGET dusk POST_BUILD
|
||||
COMMENT "Copying resources"
|
||||
)
|
||||
|
||||
if (WIN32)
|
||||
set(DUSK_WINDOWS_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/windows)
|
||||
set(DUSK_WINDOWS_ICON_PNG ${CMAKE_CURRENT_SOURCE_DIR}/res/icon.png)
|
||||
set(DUSK_WINDOWS_ICON_ICO ${CMAKE_CURRENT_BINARY_DIR}/dusk.ico)
|
||||
set(DUSK_WINDOWS_RC ${CMAKE_CURRENT_BINARY_DIR}/dusk.rc)
|
||||
set(DUSK_WINDOWS_MANIFEST ${CMAKE_CURRENT_BINARY_DIR}/dusk.manifest)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT ${DUSK_WINDOWS_ICON_ICO}
|
||||
COMMAND powershell -ExecutionPolicy Bypass -File
|
||||
${DUSK_WINDOWS_RESOURCE_DIR}/Create-IcoFromPng.ps1
|
||||
-InputPng ${DUSK_WINDOWS_ICON_PNG}
|
||||
-OutputIco ${DUSK_WINDOWS_ICON_ICO}
|
||||
DEPENDS ${DUSK_WINDOWS_ICON_PNG} ${DUSK_WINDOWS_RESOURCE_DIR}/Create-IcoFromPng.ps1
|
||||
VERBATIM
|
||||
COMMENT "Generating Windows icon"
|
||||
)
|
||||
|
||||
configure_file(${DUSK_WINDOWS_RESOURCE_DIR}/dusk.manifest.in ${DUSK_WINDOWS_MANIFEST} @ONLY)
|
||||
configure_file(${DUSK_WINDOWS_RESOURCE_DIR}/dusk.rc.in ${DUSK_WINDOWS_RC} @ONLY)
|
||||
|
||||
target_sources(dusk PRIVATE ${DUSK_WINDOWS_ICON_ICO} ${DUSK_WINDOWS_RC})
|
||||
set_target_properties(dusk PROPERTIES WIN32_EXECUTABLE TRUE)
|
||||
|
||||
if (MSVC)
|
||||
target_link_options(dusk PRIVATE /MANIFEST:NO)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (APPLE)
|
||||
if (IOS)
|
||||
set(DUSK_RESOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/platforms/ios)
|
||||
@@ -364,6 +482,8 @@ if (TARGET crashpad_handler)
|
||||
list(APPEND EXTRA_TARGETS crashpad_handler)
|
||||
endif ()
|
||||
install(TARGETS ${BINARY_TARGETS} ${EXTRA_TARGETS} DESTINATION ${CMAKE_INSTALL_PREFIX})
|
||||
aurora_install_runtime_dlls(dusk ${CMAKE_INSTALL_PREFIX})
|
||||
install(DIRECTORY ${CMAKE_SOURCE_DIR}/res DESTINATION ${CMAKE_INSTALL_PREFIX})
|
||||
if (CMAKE_BUILD_TYPE STREQUAL Debug OR CMAKE_BUILD_TYPE STREQUAL RelWithDebInfo)
|
||||
set(DEBUG_FILES_LIST "")
|
||||
foreach (target IN LISTS BINARY_TARGETS EXTRA_TARGETS)
|
||||
|
||||
+5
-5
@@ -88,7 +88,7 @@
|
||||
"name": "windows-msvc",
|
||||
"displayName": "Windows (MSVC)",
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/out/build/${presetName}",
|
||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||
"architecture": {
|
||||
"value": "x64",
|
||||
"strategy": "external"
|
||||
@@ -96,7 +96,7 @@
|
||||
"cacheVariables": {
|
||||
"CMAKE_C_COMPILER": "cl",
|
||||
"CMAKE_CXX_COMPILER": "cl",
|
||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install"
|
||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install"
|
||||
},
|
||||
"vendor": {
|
||||
"microsoft.com/VisualStudioSettings/CMake/1.0": {
|
||||
@@ -126,7 +126,7 @@
|
||||
"name": "windows-arm64-msvc",
|
||||
"displayName": "Windows ARM64 (MSVC)",
|
||||
"generator": "Ninja",
|
||||
"binaryDir": "${sourceDir}/out/build/${presetName}",
|
||||
"binaryDir": "${sourceDir}/build/${presetName}",
|
||||
"architecture": {
|
||||
"value": "arm64",
|
||||
"strategy": "external"
|
||||
@@ -134,7 +134,7 @@
|
||||
"cacheVariables": {
|
||||
"CMAKE_C_COMPILER": "cl",
|
||||
"CMAKE_CXX_COMPILER": "cl",
|
||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/out/install",
|
||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
|
||||
"AURORA_DAWN_PROVIDER": "vendor"
|
||||
},
|
||||
"vendor": {
|
||||
@@ -316,7 +316,7 @@
|
||||
"cacheVariables": {
|
||||
"CMAKE_INSTALL_PREFIX": "${sourceDir}/build/install",
|
||||
"CMAKE_TOOLCHAIN_FILE": "$env{ANDROID_HOME}/ndk/$env{ANDROID_NDK_VERSION}/build/cmake/android.toolchain.cmake",
|
||||
"ANDROID_PLATFORM": "android-24"
|
||||
"ANDROID_PLATFORM": "android-28"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Vendored
+1
-1
Submodule extern/aurora updated: 4c0d0feb02...4dd23c74d8
@@ -1338,6 +1338,7 @@ set(DUSK_FILES
|
||||
src/d/actor/d_a_alink_dusk.cpp
|
||||
src/dusk/asserts.cpp
|
||||
src/dusk/config.cpp
|
||||
src/dusk/crash_reporting.cpp
|
||||
src/dusk/endian.cpp
|
||||
src/dusk/extras.c
|
||||
src/dusk/extras.cpp
|
||||
@@ -1373,6 +1374,8 @@ set(DUSK_FILES
|
||||
src/dusk/imgui/ImGuiStubLog.cpp
|
||||
src/dusk/imgui/ImGuiMapLoader.cpp
|
||||
src/dusk/imgui/ImGuiSaveEditor.cpp
|
||||
src/dusk/imgui/ImGuiStateShare.hpp
|
||||
src/dusk/imgui/ImGuiStateShare.cpp
|
||||
src/dusk/offset_ptr.cpp
|
||||
src/dusk/OSContext.cpp
|
||||
src/dusk/OSThread.cpp
|
||||
|
||||
Generated
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1775710090,
|
||||
"narHash": "sha256-ar3rofg+awPB8QXDaFJhJ2jJhu+KqN/PRCXeyuXR76E=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4c1018dae018162ec878d42fec712642d214fdfa",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
};
|
||||
outputs = { self, nixpkgs }:
|
||||
let
|
||||
pkgs = import nixpkgs { system = "x86_64-linux"; };
|
||||
dusk = pkgs.stdenv.mkDerivation {
|
||||
name = "dusk";
|
||||
src = ./.;
|
||||
nativeBuildInputs = [
|
||||
pkgs.cmake
|
||||
pkgs.pkg-config
|
||||
pkgs.wayland
|
||||
];
|
||||
buildInputs = [
|
||||
pkgs.libGL
|
||||
pkgs.libX11
|
||||
pkgs.libXcursor
|
||||
pkgs.libxi
|
||||
pkgs.libxcb
|
||||
pkgs.libxrandr
|
||||
pkgs.libxscrnsaver
|
||||
pkgs.libxtst
|
||||
pkgs.libjpeg8
|
||||
pkgs.libxkbcommon
|
||||
pkgs.libglvnd
|
||||
];
|
||||
};
|
||||
in {
|
||||
packages.x86_64-linux.default = dusk;
|
||||
};
|
||||
}
|
||||
@@ -39,6 +39,10 @@ enum Z2WolfHowlCurveID {
|
||||
Z2WOLFHOWL_NEWSONG2,
|
||||
Z2WOLFHOWL_NEWSONG3,
|
||||
|
||||
#if TARGET_PC
|
||||
Z2WOLFHOWL_TIMESONG,
|
||||
#endif
|
||||
|
||||
Z2WOLFHOWL_MAX
|
||||
};
|
||||
|
||||
|
||||
@@ -4549,6 +4549,7 @@ public:
|
||||
/* 0x03850 */ daAlink_procFunc mpProcFunc;
|
||||
|
||||
#if TARGET_PC
|
||||
void handleWolfHowl();
|
||||
void handleQuickTransform();
|
||||
bool checkGyroAimItemContext();
|
||||
#endif
|
||||
|
||||
@@ -308,8 +308,11 @@ private:
|
||||
/* 0x0000C */ dDlst_shadowSimple_c mSimple[128];
|
||||
/* 0x0340C */ int mNextID;
|
||||
/* 0x03410 */ dDlst_shadowReal_c mReal[8];
|
||||
/* 0x15EB0 */ TGXTexObj field_0x15eb0[2];
|
||||
/* 0x15EF0 */ void* field_0x15ef0[2];
|
||||
/* 0x15EB0 */ TGXTexObj mShadowTexObj[2];
|
||||
/* 0x15EF0 */ void* mShadowTexData[2];
|
||||
#if TARGET_PC
|
||||
int mTexResScale;
|
||||
#endif
|
||||
};
|
||||
|
||||
class dDlst_window_c {
|
||||
|
||||
@@ -146,6 +146,12 @@ concept ConfigValue =
|
||||
template <ConfigValue T>
|
||||
const ConfigImplBase* GetConfigImpl();
|
||||
|
||||
template <typename T>
|
||||
struct ConfigEnumRange {
|
||||
static constexpr auto min = std::numeric_limits<std::underlying_type_t<T>>::min();
|
||||
static constexpr auto max = std::numeric_limits<std::underlying_type_t<T>>::max();
|
||||
};
|
||||
|
||||
/**
|
||||
* \brief A CVar storing values.
|
||||
*
|
||||
@@ -189,6 +195,11 @@ public:
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] constexpr const T& getDefaultValue() const noexcept {
|
||||
checkRegistered();
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Change the value of a CVar.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
namespace dusk {
|
||||
|
||||
void InitializeCrashReporting();
|
||||
void ShutdownCrashReporting();
|
||||
|
||||
} // namespace dusk
|
||||
@@ -20,6 +20,11 @@ void begin_record();
|
||||
void end_record();
|
||||
void interpolate(float step);
|
||||
float get_interpolation_step();
|
||||
|
||||
void notify_presentation_frame();
|
||||
void request_presentation_sync();
|
||||
bool presentation_sync_active();
|
||||
|
||||
void notify_sim_tick_complete();
|
||||
uint32_t begin_presentation_ui_pass();
|
||||
uint32_t get_presentation_ui_advance_ticks();
|
||||
|
||||
@@ -9,14 +9,17 @@ constexpr const char* DO_RESET = "Cmd+R";
|
||||
constexpr const char* DO_RESET = "Ctrl+R";
|
||||
#endif
|
||||
|
||||
constexpr const char* TOGGLE_FULLSCREEN = "F11";
|
||||
|
||||
constexpr const char* SHOW_PROCESS_MANAGEMENT = "F2";
|
||||
constexpr const char* SHOW_DEBUG_OVERLAY = "F3";
|
||||
constexpr const char* SHOW_HEAP_VIEWER = "F4";
|
||||
constexpr const char* SHOW_STUB_LOG = "F5";
|
||||
constexpr const char* SHOW_CAMERA_DEBUG = "F6";
|
||||
constexpr const char* SHOW_AUDIO_DEBUG = "F7";
|
||||
constexpr const char* SHOW_PLAYER_INFO = "F5";
|
||||
constexpr const char* SHOW_SAVE_EDITOR = "F6";
|
||||
constexpr const char* SHOW_MAP_LOADER = "F7";
|
||||
constexpr const char* SHOW_STATE_SHARE = "F8";
|
||||
constexpr const char* SHOW_DEBUG_CAMERA = "F9";
|
||||
constexpr const char* SHOW_AUDIO_DEBUG = "F10";
|
||||
|
||||
constexpr const char* TOGGLE_FULLSCREEN = "F11";
|
||||
|
||||
constexpr const char* TURBO = "Tab";
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
void aurora_log_callback(AuroraLogLevel level, const char* module, const char* message, unsigned int len);
|
||||
|
||||
namespace dusk {
|
||||
void InitializeFileLogging(const char* configDir, AuroraLogLevel logLevel);
|
||||
void ShutdownFileLogging();
|
||||
const char* GetLogFilePath();
|
||||
void SendToStubLog(AuroraLogLevel level, const char* module, const char* message);
|
||||
}
|
||||
|
||||
|
||||
+18
-1
@@ -7,6 +7,20 @@ namespace dusk {
|
||||
|
||||
using namespace config;
|
||||
|
||||
enum class BloomMode : int {
|
||||
Off = 0,
|
||||
Classic = 1,
|
||||
Dusk = 2,
|
||||
};
|
||||
|
||||
namespace config {
|
||||
template <>
|
||||
struct ConfigEnumRange<BloomMode> {
|
||||
static constexpr auto min = BloomMode::Off;
|
||||
static constexpr auto max = BloomMode::Dusk;
|
||||
};
|
||||
}
|
||||
|
||||
// Persistent user settings
|
||||
|
||||
struct UserSettings {
|
||||
@@ -47,13 +61,15 @@ struct UserSettings {
|
||||
ConfigVar<bool> noMissClimbing;
|
||||
ConfigVar<bool> fastTears;
|
||||
ConfigVar<bool> instantSaves;
|
||||
ConfigVar<bool> sunsSong;
|
||||
|
||||
// Preferences
|
||||
ConfigVar<bool> enableMirrorMode;
|
||||
ConfigVar<bool> invertCameraXAxis;
|
||||
|
||||
// Graphics
|
||||
ConfigVar<bool> enableBloom;
|
||||
ConfigVar<BloomMode> bloomMode;
|
||||
ConfigVar<float> bloomMultiplier;
|
||||
ConfigVar<bool> enableWaterRefraction;
|
||||
ConfigVar<bool> enableFrameInterpolation;
|
||||
ConfigVar<int> shadowResolutionMultiplier;
|
||||
@@ -88,6 +104,7 @@ struct UserSettings {
|
||||
ConfigVar<bool> skipPreLaunchUI;
|
||||
ConfigVar<bool> showPipelineCompilation;
|
||||
ConfigVar<bool> wasPresetChosen;
|
||||
ConfigVar<bool> enableCrashReporting;
|
||||
} backend;
|
||||
};
|
||||
|
||||
|
||||
@@ -220,10 +220,12 @@ using std::isnan;
|
||||
// Some basic macros that are more convenient than putting down #if blocks for one-line changes.
|
||||
#if TARGET_PC
|
||||
#define IF_DUSK(statement) statement
|
||||
#define IF_DUSK_ARG(expr) , expr
|
||||
#define IF_NOT_DUSK(statement)
|
||||
#define DUSK_IF_ELSE(dusk, orig) dusk
|
||||
#else
|
||||
#define IF_DUSK(statement)
|
||||
#define IF_DUSK_ARG(expr)
|
||||
#define IF_NOT_DUSK(statement) statement
|
||||
#define DUSK_IF_ELSE(dusk, orig) orig
|
||||
#endif
|
||||
|
||||
@@ -11,9 +11,17 @@
|
||||
struct JASCalc {
|
||||
static void imixcopy(const s16*, const s16*, s16*, u32);
|
||||
static void bcopyfast(const void* src, void* dest, u32 size);
|
||||
#if TARGET_ANDROID
|
||||
static void _bcopy(const void* src, void* dest, u32 size);
|
||||
#else
|
||||
static void bcopy(const void* src, void* dest, u32 size);
|
||||
#endif
|
||||
static void bzerofast(void* dest, u32 size);
|
||||
#if TARGET_ANDROID
|
||||
static void _bzero(void* dest, u32 size);
|
||||
#else
|
||||
static void bzero(void* dest, u32 size);
|
||||
#endif
|
||||
static f32 pow2(f32);
|
||||
|
||||
template <typename A, typename B>
|
||||
|
||||
@@ -5,6 +5,17 @@
|
||||
#include <cstring>
|
||||
#include "dusk/endian.h"
|
||||
|
||||
#if TARGET_PC
|
||||
struct FontDrawContext {
|
||||
bool isTextureLoaded = false;
|
||||
};
|
||||
#define FONT_DRAW_CTX , FontDrawContext* context
|
||||
#define FONT_DRAW_CTX_ARG , context
|
||||
#else
|
||||
#define FONT_DRAW_CTX
|
||||
#define FONT_DRAW_CTX_ARG
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @ingroup jsystem-jutility
|
||||
*
|
||||
@@ -84,7 +95,12 @@ public:
|
||||
|
||||
/* 0x0C */ virtual void setGX() = 0;
|
||||
/* 0x10 */ virtual void setGX(JUtility::TColor col1, JUtility::TColor col2) { setGX(); }
|
||||
/* 0x14 */ virtual f32 drawChar_scale(f32 a1, f32 a2, f32 a3, f32 a4, int a5, bool a6) = 0;
|
||||
/* 0x14 */ virtual f32 drawChar_scale(f32 a1, f32 a2, f32 a3, f32 a4, int a5, bool a6 FONT_DRAW_CTX) = 0;
|
||||
#if TARGET_PC
|
||||
f32 drawChar_scale(f32 a1, f32 a2, f32 a3, f32 a4, int a5, bool a6) {
|
||||
return drawChar_scale(a1, a2, a3, a4, a5, a6, nullptr);
|
||||
}
|
||||
#endif
|
||||
/* 0x18 */ virtual int getLeading() const = 0;
|
||||
/* 0x1C */ virtual s32 getAscent() const = 0;
|
||||
/* 0x20 */ virtual s32 getDescent() const = 0;
|
||||
@@ -97,6 +113,11 @@ public:
|
||||
/* 0x3C */ virtual ResFONT* getResFont() const = 0;
|
||||
/* 0x40 */ virtual bool isLeadByte(int a1) const = 0;
|
||||
|
||||
#if TARGET_PC
|
||||
virtual void pushDrawState() = 0;
|
||||
virtual void popDrawState() = 0;
|
||||
#endif
|
||||
|
||||
static bool isLeadByte_1Byte(int b) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -18,10 +18,6 @@ struct BlockHeader {
|
||||
BE(u32) size;
|
||||
};
|
||||
|
||||
#if TARGET_PC
|
||||
struct GlyphTextures;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* @ingroup jsystem-jutility
|
||||
*
|
||||
@@ -31,7 +27,7 @@ public:
|
||||
virtual ~JUTResFont();
|
||||
virtual void setGX();
|
||||
virtual void setGX(JUtility::TColor, JUtility::TColor);
|
||||
virtual f32 drawChar_scale(f32, f32, f32, f32, int, bool);
|
||||
virtual f32 drawChar_scale(f32, f32, f32, f32, int, bool FONT_DRAW_CTX);
|
||||
virtual int getLeading() const;
|
||||
virtual s32 getAscent() const;
|
||||
virtual s32 getDescent() const;
|
||||
@@ -43,7 +39,7 @@ public:
|
||||
virtual int getFontType() const;
|
||||
virtual ResFONT* getResFont() const;
|
||||
virtual bool isLeadByte(int) const;
|
||||
virtual void loadImage(int, GXTexMapID);
|
||||
virtual void loadImage(int, GXTexMapID FONT_DRAW_CTX);
|
||||
virtual void setBlock();
|
||||
|
||||
JUTResFont(ResFONT const*, JKRHeap*);
|
||||
@@ -53,10 +49,15 @@ public:
|
||||
bool initiate(ResFONT const*, JKRHeap*);
|
||||
bool protected_initiate(ResFONT const*, JKRHeap*);
|
||||
void countBlock();
|
||||
void loadFont(int, GXTexMapID, JUTFont::TWidth*);
|
||||
void loadFont(int, GXTexMapID, JUTFont::TWidth* FONT_DRAW_CTX);
|
||||
int getFontCode(int) const;
|
||||
int convertSjis(int, BE(u16)*) const;
|
||||
|
||||
#if TARGET_PC
|
||||
void pushDrawState() override;
|
||||
void popDrawState() override;
|
||||
#endif
|
||||
|
||||
inline void delete_and_initialize() {
|
||||
deleteMemBlocks_ResFont();
|
||||
initialize_state();
|
||||
@@ -68,11 +69,7 @@ public:
|
||||
// some types uncertain, may need to be fixed
|
||||
/* 0x1C */ int mWidth;
|
||||
/* 0x20 */ int mHeight;
|
||||
#if TARGET_PC
|
||||
GlyphTextures* mGlyphTextures;
|
||||
#else
|
||||
/* 0x24 */ TGXTexObj mTexObj;
|
||||
#endif
|
||||
/* 0x44 */ int mTexPageIdx;
|
||||
/* 0x48 */ const ResFONT* mResFont;
|
||||
/* 0x4C */ ResFONT::INF1* mInf1Ptr;
|
||||
@@ -86,6 +83,16 @@ public:
|
||||
/* 0x66 */ u16 field_0x66;
|
||||
/* 0x68 */ u16 mMaxCode;
|
||||
/* 0x6C */ const IsLeadByte_func* mIsLeadByte;
|
||||
|
||||
#if TARGET_PC
|
||||
// Dusk change: we use a single large texture for all characters.
|
||||
// This enables better draw call merging, ideally enabling entire blocks of
|
||||
// text to be one draw call.
|
||||
TGXTexObj mJoinedTextureObject;
|
||||
u16 mJoinedTextureHeight;
|
||||
|
||||
void initJoinedTexture();
|
||||
#endif
|
||||
};
|
||||
|
||||
extern u8 const JUTResFONT_Ascfont_fix12[];
|
||||
|
||||
@@ -211,6 +211,11 @@ f32 J2DPrint::parse(const u8* pString, int length, int param_2, u16* param_3,
|
||||
local_bc.a = local_bc.a * alpha / 0xFF;
|
||||
mFont->setGradColor(local_b8, field_0x22 ? local_bc : local_b8);
|
||||
|
||||
#if TARGET_PC
|
||||
FontDrawContext context;
|
||||
mFont->pushDrawState();
|
||||
#endif
|
||||
|
||||
do {
|
||||
bool b2ByteCharacter = false;
|
||||
bool r25;
|
||||
@@ -312,9 +317,9 @@ f32 J2DPrint::parse(const u8* pString, int length, int param_2, u16* param_3,
|
||||
} else {
|
||||
if (param_6) {
|
||||
if (param_3 != NULL) {
|
||||
mFont->drawChar_scale(mCursorH + (s16)param_3[someIndex], mCursorV, (s32)mScaleX, (s32)mScaleY, iCharacter, true);
|
||||
mFont->drawChar_scale(mCursorH + (s16)param_3[someIndex], mCursorV, (s32)mScaleX, (s32)mScaleY, iCharacter, true IF_DUSK_ARG(&context));
|
||||
} else {
|
||||
mFont->drawChar_scale(mCursorH, mCursorV, (s32)mScaleX, (s32)mScaleY, iCharacter, true);
|
||||
mFont->drawChar_scale(mCursorH, mCursorV, (s32)mScaleX, (s32)mScaleY, iCharacter, true IF_DUSK_ARG(&context));
|
||||
}
|
||||
}
|
||||
mCursorH += field_0x34;
|
||||
@@ -353,6 +358,8 @@ f32 J2DPrint::parse(const u8* pString, int length, int param_2, u16* param_3,
|
||||
iCharacter = *(pString++);
|
||||
} while (true);
|
||||
|
||||
IF_DUSK(mFont->popDrawState());
|
||||
|
||||
if (param_3 != NULL) {
|
||||
param_3[someIndex] = 0xFFFF;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,11 @@ void JASDriver::initAI(void (*param_0)(void)) {
|
||||
for (int i = 0; i < 3; i++) {
|
||||
sDmaDacBuffer[i] = JKR_NEW_ARRAY_ARGS(s16, dacSize, JASDram, 0x20);
|
||||
JUT_ASSERT(102, sDmaDacBuffer[i])
|
||||
#if TARGET_ANDROID
|
||||
JASCalc::_bzero(sDmaDacBuffer[i], size);
|
||||
#else
|
||||
JASCalc::bzero(sDmaDacBuffer[i], size);
|
||||
#endif
|
||||
DCStoreRange(sDmaDacBuffer[i], size);
|
||||
}
|
||||
sDspDacBuffer = JKR_NEW_ARRAY_ARGS(s16*, data_804507A8, JASDram, 0);
|
||||
@@ -63,7 +67,11 @@ void JASDriver::initAI(void (*param_0)(void)) {
|
||||
for (int i = 0; i < data_804507A8; i++) {
|
||||
sDspDacBuffer[i] = JKR_NEW_ARRAY_ARGS(s16, getDacSize(), JASDram, 0x20);
|
||||
JUT_ASSERT(119, sDspDacBuffer[i]);
|
||||
#if TARGET_ANDROID
|
||||
JASCalc::_bzero(sDspDacBuffer[i], size);
|
||||
#else
|
||||
JASCalc::bzero(sDspDacBuffer[i], size);
|
||||
#endif
|
||||
DCStoreRange(sDspDacBuffer[i], size);
|
||||
}
|
||||
sDspDacWriteBuffer = data_804507A8 - 1;
|
||||
|
||||
@@ -69,7 +69,11 @@ JASBasicBank* JASBNKParser::Ver1::createBasicBank(void const* stream, JKRHeap* h
|
||||
JUT_ASSERT(145, list_chunk);
|
||||
|
||||
u8* envt = JKR_NEW_ARRAY_ARGS(u8, envt_chunk->mSize, heap, 2);
|
||||
#if TARGET_ANDROID
|
||||
JASCalc::_bcopy(envt_chunk->mData, envt, envt_chunk->mSize);
|
||||
#else
|
||||
JASCalc::bcopy(envt_chunk->mData, envt, envt_chunk->mSize);
|
||||
#endif
|
||||
|
||||
BE(u32)* ptr = &osc_chunk->mCount;
|
||||
u32 count = *ptr++;
|
||||
@@ -215,7 +219,11 @@ JASBasicBank* JASBNKParser::Ver0::createBasicBank(void const* stream, JKRHeap* h
|
||||
int size = endPtr - points;
|
||||
JASOscillator::Point* table = JKR_NEW_ARRAY_ARGS(JASOscillator::Point, size, heap, 0);
|
||||
JUT_ASSERT(396, table != NULL);
|
||||
#if TARGET_ANDROID
|
||||
JASCalc::_bcopy(points, table, size * sizeof(JASOscillator::Point));
|
||||
#else
|
||||
JASCalc::bcopy(points, table, size * sizeof(JASOscillator::Point));
|
||||
#endif
|
||||
osc->mTable = table;
|
||||
} else {
|
||||
osc->mTable = NULL;
|
||||
@@ -227,7 +235,11 @@ JASBasicBank* JASBNKParser::Ver0::createBasicBank(void const* stream, JKRHeap* h
|
||||
int size = endPtr - points;
|
||||
JASOscillator::Point* table = JKR_NEW_ARRAY_ARGS(JASOscillator::Point, size, heap, 0);
|
||||
JUT_ASSERT(409, table != NULL);
|
||||
#if TARGET_ANDROID
|
||||
JASCalc::_bcopy(points, table, size * sizeof(JASOscillator::Point));
|
||||
#else
|
||||
JASCalc::bcopy(points, table, size * sizeof(JASOscillator::Point));
|
||||
#endif
|
||||
osc->rel_table = table;
|
||||
} else {
|
||||
osc->rel_table = NULL;
|
||||
|
||||
@@ -13,7 +13,11 @@ void JASBasicBank::newInstTable(u8 num, JKRHeap* heap) {
|
||||
JUT_ASSERT(31, num <= JASBank::PRG_OSC);
|
||||
mInstNumMax = num;
|
||||
mInstTable = JKR_NEW_ARRAY_ARGS(JASInst*, mInstNumMax, heap, 0);
|
||||
#if TARGET_ANDROID
|
||||
JASCalc::_bzero(mInstTable, mInstNumMax * 4);
|
||||
#else
|
||||
JASCalc::bzero(mInstTable, mInstNumMax * 4);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,11 @@ JASBasicInst::JASBasicInst() {
|
||||
mPitch = 1.0;
|
||||
mKeymapCount = 0;
|
||||
mKeymap = NULL;
|
||||
#if TARGET_ANDROID
|
||||
JASCalc::_bzero(field_0xc, sizeof(field_0xc));
|
||||
#else
|
||||
JASCalc::bzero(field_0xc, sizeof(field_0xc));
|
||||
#endif
|
||||
}
|
||||
|
||||
JASBasicInst::~JASBasicInst() {
|
||||
|
||||
@@ -33,7 +33,11 @@ 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;
|
||||
|
||||
@@ -90,7 +94,11 @@ 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) {
|
||||
|
||||
@@ -434,8 +434,14 @@ void JASDsp::initBuffer() {
|
||||
JUT_ASSERT(354, CH_BUF);
|
||||
FX_BUF = JKR_NEW_ARRAY_ARGS(FxBuf, 4, JASDram, 0x20);
|
||||
JUT_ASSERT(356, FX_BUF);
|
||||
#if TARGET_ANDROID
|
||||
JASCalc::_bzero(CH_BUF, sizeof(TChannel) * DSP_CHANNELS);
|
||||
JASCalc::_bzero(FX_BUF, sizeof(FxBuf) * 4);
|
||||
#else
|
||||
JASCalc::bzero(CH_BUF, sizeof(TChannel) * DSP_CHANNELS);
|
||||
JASCalc::bzero(FX_BUF, sizeof(FxBuf) * 4);
|
||||
#endif
|
||||
|
||||
for (u8 i = 0; i < 4; i++) {
|
||||
setFXLine(i, NULL, NULL);
|
||||
}
|
||||
@@ -459,7 +465,11 @@ int JASDsp::setFXLine(u8 param_0, s16* buffer, JASDsp::FxlineConfig_* param_2) {
|
||||
if (buffer != NULL && param_2 != NULL) {
|
||||
u32 bufsize = param_2->field_0xc * 0xa0;
|
||||
puVar3->field_0x4 = buffer;
|
||||
#if TARGET_ANDROID
|
||||
JASCalc::_bzero(buffer, bufsize);
|
||||
#else
|
||||
JASCalc::bzero(buffer, bufsize);
|
||||
#endif
|
||||
JUT_ASSERT(420, ((u32)((uintptr_t)buffer) & 0x1f) == 0);
|
||||
JUT_ASSERT(421, (bufsize & 0x1f) == 0);
|
||||
DCFlushRange(buffer, bufsize);
|
||||
|
||||
@@ -18,7 +18,11 @@ void JASDrumSet::newPercArray(u8 num, JKRHeap* heap) {
|
||||
JUT_ASSERT(39, num <= 128);
|
||||
mPercNumMax = num;
|
||||
mPercArray = JKR_NEW_ARRAY_ARGS(TPerc*, mPercNumMax, heap, 0);
|
||||
#if TARGET_ANDROID
|
||||
JASCalc::_bzero(mPercArray, mPercNumMax * sizeof(TPerc*));
|
||||
#else
|
||||
JASCalc::bzero(mPercArray, mPercNumMax * sizeof(TPerc*));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,7 +53,11 @@ int JASReportCopyBuffer(char *param_1,int lines) {
|
||||
r30 = sLineMax - 1;
|
||||
}
|
||||
src = &sBuffer[r30 * 64];
|
||||
#if TARGET_ANDROID
|
||||
JASCalc::_bcopy(src, dest, 64);
|
||||
#else
|
||||
JASCalc::bcopy(src, dest, 64);
|
||||
#endif
|
||||
r30--;
|
||||
dest += 64;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,11 @@ void* JASTaskThread::allocCallStack(JASThreadCallback callback, const void* msg,
|
||||
}
|
||||
|
||||
callStack->msgType_ = 1;
|
||||
#if TARGET_ANDROID
|
||||
JASCalc::_bcopy(msg, callStack->msg.buffer, msgSize);
|
||||
#else
|
||||
JASCalc::bcopy(msg, callStack->msg.buffer, msgSize);
|
||||
#endif
|
||||
callStack->callback_ = callback;
|
||||
return callStack;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
|
||||
#include "JSystem/JStudio/JStudio/jstudio-object.h"
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/audio.h"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/settings.h"
|
||||
#endif
|
||||
|
||||
namespace JStudio {
|
||||
namespace {
|
||||
@@ -650,10 +654,25 @@ value_or_fun:
|
||||
return;
|
||||
|
||||
value:
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableFrameInterpolation && u <= 5 &&
|
||||
(operation == data::UNK_0x2 || operation == data::UNK_0x3 || operation == data::UNK_0x12))
|
||||
{
|
||||
dusk::frame_interp::request_presentation_sync();
|
||||
}
|
||||
#endif
|
||||
adaptor->adaptor_setVariableValue(control, u, operation, param_2, param_3);
|
||||
return;
|
||||
|
||||
value_n:
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableFrameInterpolation &&
|
||||
(pN == TAdaptor_camera::sauVariableValue_3_POSITION_XYZ || pN == TAdaptor_camera::sauVariableValue_3_TARGET_POSITION_XYZ) &&
|
||||
(operation == data::UNK_0x2 || operation == data::UNK_0x3 || operation == data::UNK_0x12))
|
||||
{
|
||||
dusk::frame_interp::request_presentation_sync();
|
||||
}
|
||||
#endif
|
||||
adaptor->adaptor_setVariableValue_n(control, pN, u, operation, param_2, param_3);
|
||||
return;
|
||||
|
||||
|
||||
@@ -6,26 +6,13 @@
|
||||
#include "JSystem/JUtility/JUTAssert.h"
|
||||
#include "JSystem/JUtility/JUTConsole.h"
|
||||
#include <gx.h>
|
||||
#include "absl/container/node_hash_map.h"
|
||||
|
||||
#if TARGET_PC
|
||||
struct GlyphTextures {
|
||||
absl::node_hash_map<int, TGXTexObj> textures;
|
||||
};
|
||||
#endif
|
||||
|
||||
JUTResFont::JUTResFont() {
|
||||
#if TARGET_PC
|
||||
mGlyphTextures = new GlyphTextures();
|
||||
#endif
|
||||
initialize_state();
|
||||
JUTFont::initialize_state();
|
||||
}
|
||||
|
||||
JUTResFont::JUTResFont(const ResFONT* pFont, JKRHeap* pHeap) {
|
||||
#if TARGET_PC
|
||||
mGlyphTextures = new GlyphTextures();
|
||||
#endif
|
||||
initialize_state();
|
||||
JUTFont::initialize_state();
|
||||
initiate(pFont, pHeap);
|
||||
@@ -33,10 +20,7 @@ JUTResFont::JUTResFont(const ResFONT* pFont, JKRHeap* pHeap) {
|
||||
|
||||
JUTResFont::~JUTResFont() {
|
||||
#if TARGET_PC
|
||||
for (auto& pair : mGlyphTextures->textures) {
|
||||
pair.second.reset();
|
||||
}
|
||||
delete mGlyphTextures;
|
||||
mJoinedTextureObject.reset();
|
||||
#endif
|
||||
|
||||
if (mValid) {
|
||||
@@ -70,6 +54,33 @@ bool JUTResFont::initiate(const ResFONT* pFont, JKRHeap* pHeap) {
|
||||
return true;
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
void JUTResFont::initJoinedTexture() {
|
||||
if (mGly1BlockNum != 1) {
|
||||
CRASH("mGly1BlockNum must be 1!");
|
||||
}
|
||||
|
||||
const auto& block = *mpGlyphBlocks[0];
|
||||
if (block.textureWidth % 8 != 0 || block.textureHeight % 8 != 0) {
|
||||
// Idk how the GameCube's tiling texture format works so this is a safety check.
|
||||
CRASH("Texture size not divisible!");
|
||||
}
|
||||
|
||||
int pageCount = 0;
|
||||
u32 pageNumCells = block.numRows * block.numColumns;
|
||||
for (u32 code = block.startCode; code < block.endCode; code += pageNumCells) {
|
||||
pageCount += 1;
|
||||
}
|
||||
|
||||
mJoinedTextureHeight = block.textureHeight * pageCount;
|
||||
GXInitTexObj(&mJoinedTextureObject, block.data, block.textureWidth,
|
||||
mJoinedTextureHeight, static_cast<GXTexFmt>(block.textureFormat.host()),
|
||||
GX_CLAMP, GX_CLAMP, false);
|
||||
|
||||
GXInitTexObjLOD(&mJoinedTextureObject, GX_LINEAR, GX_LINEAR, 0.0f, 0.0f, 0.0f, 0U, 0U, GX_ANISO_1);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool JUTResFont::protected_initiate(const ResFONT* pFont, JKRHeap* pHeap) {
|
||||
void** p;
|
||||
delete_and_initialize();
|
||||
@@ -100,8 +111,10 @@ bool JUTResFont::protected_initiate(const ResFONT* pFont, JKRHeap* pHeap) {
|
||||
mpMapBlocks = JKR_NEW_ARRAY_ARGS(ResFONT::MAP1*, mMap1BlockNum, p);
|
||||
}
|
||||
setBlock();
|
||||
return true;
|
||||
|
||||
IF_DUSK(initJoinedTexture());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void JUTResFont::countBlock() {
|
||||
@@ -231,14 +244,14 @@ void JUTResFont::setGX(JUtility::TColor col1, JUtility::TColor col2) {
|
||||
}
|
||||
|
||||
f32 JUTResFont::drawChar_scale(f32 pos_x, f32 pos_y, f32 scale_x, f32 scale_y, int str_int,
|
||||
bool flag) {
|
||||
bool flag FONT_DRAW_CTX) {
|
||||
f32 x1;
|
||||
f32 x2;
|
||||
f32 y1;
|
||||
|
||||
JUT_ASSERT(378, mValid);
|
||||
JUTFont::TWidth width;
|
||||
loadFont(str_int, GX_TEXMAP0, &width);
|
||||
loadFont(str_int, GX_TEXMAP0, &width FONT_DRAW_CTX_ARG);
|
||||
|
||||
if ((mFixed) || (!flag)) {
|
||||
x1 = pos_x;
|
||||
@@ -258,15 +271,26 @@ f32 JUTResFont::drawChar_scale(f32 pos_x, f32 pos_y, f32 scale_x, f32 scale_y, i
|
||||
f32 y2 = getDescent() * (scale_y / getHeight()) + pos_y;
|
||||
|
||||
u16 texW = mpGlyphBlocks[field_0x66]->textureWidth;
|
||||
#if TARGET_PC
|
||||
u16 texH = mJoinedTextureHeight;
|
||||
#else
|
||||
u16 texH = mpGlyphBlocks[field_0x66]->textureHeight;
|
||||
#endif
|
||||
u16 cellW = mpGlyphBlocks[field_0x66]->cellWidth;
|
||||
|
||||
u16 cellH = mpGlyphBlocks[field_0x66]->cellHeight;
|
||||
s32 u1 = (mWidth * 0x8000) / texW;
|
||||
s32 v1 = (mHeight * 0x8000) / texH;
|
||||
s32 u2 = ((mWidth + cellW) * 0x8000) / texW;
|
||||
s32 v2 = ((mHeight + cellH) * 0x8000) / texH;
|
||||
|
||||
#if TARGET_PC
|
||||
if (!context) {
|
||||
pushDrawState();
|
||||
}
|
||||
#else
|
||||
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0);
|
||||
#endif
|
||||
GXBegin(GX_QUADS, GX_VTXFMT0, 4);
|
||||
|
||||
// Bottom Left
|
||||
@@ -290,18 +314,33 @@ f32 JUTResFont::drawChar_scale(f32 pos_x, f32 pos_y, f32 scale_x, f32 scale_y, i
|
||||
GXTexCoord2u16(u1, v2);
|
||||
GXEnd();
|
||||
|
||||
#if TARGET_PC
|
||||
if (!context) {
|
||||
popDrawState();
|
||||
}
|
||||
#else
|
||||
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_S16, 0);
|
||||
|
||||
#endif
|
||||
return retval;
|
||||
}
|
||||
|
||||
void JUTResFont::loadFont(int code, GXTexMapID texMapID, JUTFont::TWidth* pDstWidth) {
|
||||
#if TARGET_PC
|
||||
void JUTResFont::pushDrawState() {
|
||||
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_F32, 0);
|
||||
}
|
||||
|
||||
void JUTResFont::popDrawState() {
|
||||
GXSetVtxAttrFmt(GX_VTXFMT0, GX_VA_POS, GX_POS_XYZ, GX_S16, 0);
|
||||
}
|
||||
#endif
|
||||
|
||||
void JUTResFont::loadFont(int code, GXTexMapID texMapID, JUTFont::TWidth* pDstWidth FONT_DRAW_CTX) {
|
||||
if (pDstWidth != 0) {
|
||||
getWidthEntry(code, pDstWidth);
|
||||
}
|
||||
|
||||
int fontCode = getFontCode(code);
|
||||
loadImage(fontCode, texMapID);
|
||||
loadImage(fontCode, texMapID FONT_DRAW_CTX_ARG);
|
||||
}
|
||||
|
||||
void JUTResFont::getWidthEntry(int code, JUTFont::TWidth* i_width) const {
|
||||
@@ -403,7 +442,7 @@ int JUTResFont::getFontCode(int chr) const {
|
||||
return ret;
|
||||
}
|
||||
|
||||
void JUTResFont::loadImage(int code, GXTexMapID id){
|
||||
void JUTResFont::loadImage(int code, GXTexMapID id FONT_DRAW_CTX){
|
||||
int i = 0;
|
||||
for (; i < mGly1BlockNum; i++)
|
||||
{
|
||||
@@ -435,22 +474,15 @@ void JUTResFont::loadImage(int code, GXTexMapID id){
|
||||
mHeight = cellRow * cellH;
|
||||
|
||||
#if TARGET_PC
|
||||
const auto found = mGlyphTextures->textures.find(pageIdx);
|
||||
GXTexObj* texObj;
|
||||
if (found == mGlyphTextures->textures.end()) {
|
||||
texObj = &mGlyphTextures->textures[pageIdx];
|
||||
void* pImg = &mpGlyphBlocks[i]->data[pageIdx * mpGlyphBlocks[i]->textureSize];
|
||||
GXInitTexObj(texObj, pImg, mpGlyphBlocks[i]->textureWidth,
|
||||
mpGlyphBlocks[i]->textureHeight, (GXTexFmt)(u16)mpGlyphBlocks[i]->textureFormat,
|
||||
GX_CLAMP, GX_CLAMP, 0);
|
||||
mHeight += texH * pageIdx;
|
||||
|
||||
GXInitTexObjLOD(texObj, GX_LINEAR, GX_LINEAR, 0.0f, 0.0f, 0.0f, 0U, 0U, GX_ANISO_1);
|
||||
} else {
|
||||
texObj = &found->second;
|
||||
if (!context || !context->isTextureLoaded) {
|
||||
GXLoadTexObj(&mJoinedTextureObject, id);
|
||||
if (context) {
|
||||
context->isTextureLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
GXLoadTexObj(texObj, id);
|
||||
|
||||
// Gets used by some other code.
|
||||
mTexPageIdx = pageIdx;
|
||||
field_0x66 = i;
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
.gradle/
|
||||
build/
|
||||
app/build/
|
||||
local.properties
|
||||
app/src/main/jniLibs/*/*.so
|
||||
@@ -0,0 +1,77 @@
|
||||
# Android Shell
|
||||
|
||||
This directory contains a minimal SDLActivity-based Android app wrapper for Dusk.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Android SDK installed (`ANDROID_HOME`)
|
||||
- Android NDK version used by CMake presets (`ANDROID_NDK_VERSION`)
|
||||
- JDK 17+
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
export ANDROID_HOME="$HOME/Android/Sdk"
|
||||
export ANDROID_NDK_VERSION="29.0.14206865"
|
||||
export JAVA_HOME="/usr/lib/jvm/java-17-openjdk"
|
||||
```
|
||||
|
||||
## Build Native Libraries
|
||||
|
||||
```bash
|
||||
cmake --preset android-arm64
|
||||
cmake --build --preset android-arm64
|
||||
|
||||
cmake --preset android-x86_64
|
||||
cmake --build --preset android-x86_64
|
||||
```
|
||||
|
||||
These builds produce:
|
||||
|
||||
- `build/android-arm64/Binaries/libmain.so`
|
||||
- `build/android-x86_64/Binaries/libmain.so`
|
||||
|
||||
## Stage Libraries Into APK Project
|
||||
|
||||
```bash
|
||||
./android/scripts/stage-jni-libs.sh
|
||||
```
|
||||
|
||||
This copies:
|
||||
|
||||
- `libmain.so` -> `android/app/src/main/jniLibs/arm64-v8a/`
|
||||
- `libmain.so` -> `android/app/src/main/jniLibs/x86_64/`
|
||||
|
||||
## Refresh SDL Java Shim (Optional)
|
||||
|
||||
If you update SDL and want to refresh the embedded Java shim files:
|
||||
|
||||
```bash
|
||||
./android/scripts/sync-sdl-java.sh
|
||||
```
|
||||
|
||||
## Build APK
|
||||
|
||||
```bash
|
||||
cd android
|
||||
./gradlew :app:assembleDebug
|
||||
```
|
||||
|
||||
Output APK:
|
||||
|
||||
- `android/app/build/outputs/apk/debug/app-debug.apk`
|
||||
|
||||
## Launch With Runtime Args (adb)
|
||||
|
||||
You can pass command-line args through the activity intent:
|
||||
|
||||
```bash
|
||||
adb shell am start -n com.twilitrealm.dusk/.DuskActivity \
|
||||
--es dusk_args "'/sdcard/Download/The Legend of Zelda: Twilight Princess (USA).iso'"
|
||||
```
|
||||
|
||||
Supported extras:
|
||||
|
||||
- `dusk_args`: single shell-like argument string
|
||||
- `dusk_argv`: string-array argv
|
||||
- `dusk_disc`: compatibility shortcut (single ISO path)
|
||||
@@ -0,0 +1,50 @@
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.twilitrealm.dusk'
|
||||
compileSdk 36
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'com.twilitrealm.dusk'
|
||||
minSdk 26
|
||||
targetSdk 36
|
||||
versionCode 1
|
||||
versionName '0.1.0'
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
debug {
|
||||
minifyEnabled false
|
||||
}
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
jniLibs.srcDirs = ['src/main/jniLibs']
|
||||
assets.srcDirs = ['../../assets']
|
||||
}
|
||||
}
|
||||
|
||||
splits {
|
||||
abi {
|
||||
enable true
|
||||
reset()
|
||||
include 'arm64-v8a', 'x86_64'
|
||||
universalApk false
|
||||
}
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
# Keep SDL activity and related JNI bridge methods.
|
||||
-keep class org.libsdl.app.** { *; }
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-feature android:glEsVersion="0x00020000" />
|
||||
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.bluetooth" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.gamepad" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.type.pc" android:required="false" />
|
||||
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:hardwareAccelerated="true"
|
||||
android:appCategory="game"
|
||||
android:icon="@android:drawable/sym_def_app_icon"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@android:style/Theme.NoTitleBar"
|
||||
android:enableOnBackInvokedCallback="false">
|
||||
|
||||
<meta-data android:name="android.game_mode_config"
|
||||
android:resource="@xml/game_mode_config" />
|
||||
|
||||
<activity
|
||||
android:name="com.twilitrealm.dusk.DuskActivity"
|
||||
android:alwaysRetainTaskState="true"
|
||||
android:configChanges="layoutDirection|locale|grammaticalGender|fontScale|fontWeightAdjustment|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:launchMode="singleInstance"
|
||||
android:preferMinimalPostProcessing="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.twilitrealm.dusk;
|
||||
|
||||
import android.app.ActionBar;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
|
||||
import org.libsdl.app.SDLActivity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class DuskActivity extends SDLActivity {
|
||||
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) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
getWindow().getDecorView().getWindowInsetsController().hide(WindowInsets.Type.systemBars());
|
||||
}else {
|
||||
View decorView = getWindow().getDecorView();
|
||||
// Hide the status bar.
|
||||
int uiOptions = View.SYSTEM_UI_FLAG_FULLSCREEN;
|
||||
decorView.setSystemUiVisibility(uiOptions);
|
||||
// Remember that you should never show the action bar if the
|
||||
// status bar is hidden, so hide that too if necessary.
|
||||
ActionBar actionBar = getActionBar();
|
||||
actionBar.hide();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getLibraries() {
|
||||
// SDL3 is statically linked into libmain.so in this build.
|
||||
return new String[] {
|
||||
"main"
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String[] getArguments() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
String discPath = intent.getStringExtra("dusk_disc");
|
||||
if (discPath != null && !discPath.isEmpty()) {
|
||||
return new String[] { discPath };
|
||||
}
|
||||
}
|
||||
return new String[0];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,655 @@
|
||||
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.LinkedList;
|
||||
import java.util.UUID;
|
||||
|
||||
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 mFrozen = false;
|
||||
private LinkedList<GattOperation> mOperations;
|
||||
GattOperation mCurrentOperation = null;
|
||||
private Handler mHandler;
|
||||
|
||||
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 inputCharacteristic = UUID.fromString("100F6C33-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 };
|
||||
|
||||
static class GattOperation {
|
||||
private enum Operation {
|
||||
CHR_READ,
|
||||
CHR_WRITE,
|
||||
ENABLE_NOTIFICATION
|
||||
}
|
||||
|
||||
Operation mOp;
|
||||
UUID mUuid;
|
||||
byte[] mValue;
|
||||
BluetoothGatt mGatt;
|
||||
boolean mResult = true;
|
||||
|
||||
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid) {
|
||||
mGatt = gatt;
|
||||
mOp = operation;
|
||||
mUuid = uuid;
|
||||
}
|
||||
|
||||
private GattOperation(BluetoothGatt gatt, GattOperation.Operation operation, UUID uuid, byte[] value) {
|
||||
mGatt = gatt;
|
||||
mOp = operation;
|
||||
mUuid = uuid;
|
||||
mValue = value;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
// 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(inputCharacteristic)) {
|
||||
Log.v(TAG, "Found input characteristic");
|
||||
// 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();
|
||||
}
|
||||
|
||||
// Run in main thread
|
||||
mHandler.post(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
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void queueGattOperation(GattOperation op) {
|
||||
synchronized (mOperations) {
|
||||
mOperations.add(op);
|
||||
}
|
||||
executeNextGattOperation();
|
||||
}
|
||||
|
||||
private void enableNotification(UUID chrUuid) {
|
||||
GattOperation op = HIDDeviceBLESteamController.GattOperation.enableNotification(mGatt, chrUuid);
|
||||
queueGattOperation(op);
|
||||
}
|
||||
|
||||
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 {
|
||||
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);
|
||||
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(inputCharacteristic) && !mFrozen) {
|
||||
mManager.HIDDeviceInputReport(getId(), characteristic.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
|
||||
//Log.v(TAG, "onDescriptorRead status=" + status);
|
||||
}
|
||||
|
||||
@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(inputCharacteristic)) {
|
||||
boolean hasWrittenInputDescriptor = true;
|
||||
BluetoothGattCharacteristic reportChr = chr.getService().getCharacteristic(reportCharacteristic);
|
||||
if (reportChr != null) {
|
||||
Log.v(TAG, "Writing report characteristic to enter valve mode");
|
||||
reportChr.setValue(enterValveMode);
|
||||
gatt.writeCharacteristic(reportChr);
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
// We don't have an easy way to query from the Bluetooth device, but we know what it is
|
||||
final int D0G_BLE2_PID = 0x1106;
|
||||
return D0G_BLE2_PID;
|
||||
}
|
||||
|
||||
@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 {
|
||||
//Log.v(TAG, "writeOutputReport " + HexDump.dumpHexString(report));
|
||||
writeCharacteristic(reportCharacteristic, report);
|
||||
return report.length;
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,690 @@
|
||||
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
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
return bluetoothDevice.getName().equals("SteamController") && ((bluetoothDevice.getType() & BluetoothDevice.DEVICE_TYPE_LE) != 0);
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 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
|
||||
if (mInputEndpoint == null || mOutputEndpoint == null) {
|
||||
Log.w(TAG, "Missing required endpoint on USB device " + getDeviceName());
|
||||
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 (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 {
|
||||
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 (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) {
|
||||
UsbInterface iface = mDevice.getInterface(mInterfaceIndex);
|
||||
mConnection.releaseInterface(iface);
|
||||
mConnection.close();
|
||||
mConnection = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
close();
|
||||
mManager = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setFrozen(boolean frozen) {
|
||||
mFrozen = frozen;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
/**
|
||||
SDL library initialization
|
||||
*/
|
||||
public class SDL {
|
||||
|
||||
// This function should be called first and sets up the native code
|
||||
// so it can call into the Java classes
|
||||
static public void setupJNI() {
|
||||
SDLActivity.nativeSetupJNI();
|
||||
SDLAudioManager.nativeSetupJNI();
|
||||
SDLControllerManager.nativeSetupJNI();
|
||||
}
|
||||
|
||||
// This function should be called each time the activity is started
|
||||
static public void initialize() {
|
||||
setContext(null);
|
||||
|
||||
SDLActivity.initialize();
|
||||
SDLAudioManager.initialize();
|
||||
SDLControllerManager.initialize();
|
||||
}
|
||||
|
||||
// This function stores the current activity (SDL or not)
|
||||
static public void setContext(Activity context) {
|
||||
SDLAudioManager.setContext(context);
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
static public Activity getContext() {
|
||||
return mContext;
|
||||
}
|
||||
|
||||
static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
|
||||
loadLibrary(libraryName, mContext);
|
||||
}
|
||||
|
||||
static void loadLibrary(String libraryName, Context context) throws UnsatisfiedLinkError, SecurityException, NullPointerException {
|
||||
|
||||
if (libraryName == null) {
|
||||
throw new NullPointerException("No library name provided.");
|
||||
}
|
||||
|
||||
try {
|
||||
// Let's see if we have ReLinker available in the project. This is necessary for
|
||||
// some projects that have huge numbers of local libraries bundled, and thus may
|
||||
// trip a bug in Android's native library loader which ReLinker works around. (If
|
||||
// loadLibrary works properly, ReLinker will simply use the normal Android method
|
||||
// internally.)
|
||||
//
|
||||
// To use ReLinker, just add it as a dependency. For more information, see
|
||||
// https://github.com/KeepSafe/ReLinker for ReLinker's repository.
|
||||
//
|
||||
Class<?> relinkClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker");
|
||||
Class<?> relinkListenerClass = context.getClassLoader().loadClass("com.getkeepsafe.relinker.ReLinker$LoadListener");
|
||||
Class<?> contextClass = context.getClassLoader().loadClass("android.content.Context");
|
||||
Class<?> stringClass = context.getClassLoader().loadClass("java.lang.String");
|
||||
|
||||
// Get a 'force' instance of the ReLinker, so we can ensure libraries are reinstalled if
|
||||
// they've changed during updates.
|
||||
Method forceMethod = relinkClass.getDeclaredMethod("force");
|
||||
Object relinkInstance = forceMethod.invoke(null);
|
||||
Class<?> relinkInstanceClass = relinkInstance.getClass();
|
||||
|
||||
// Actually load the library!
|
||||
Method loadMethod = relinkInstanceClass.getDeclaredMethod("loadLibrary", contextClass, stringClass, stringClass, relinkListenerClass);
|
||||
loadMethod.invoke(relinkInstance, context, libraryName, null, null);
|
||||
}
|
||||
catch (final Throwable e) {
|
||||
// Fall back
|
||||
try {
|
||||
System.loadLibrary(libraryName);
|
||||
}
|
||||
catch (final UnsatisfiedLinkError ule) {
|
||||
throw ule;
|
||||
}
|
||||
catch (final SecurityException se) {
|
||||
throw se;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static Activity mContext;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.content.Context;
|
||||
import android.media.AudioDeviceCallback;
|
||||
import android.media.AudioDeviceInfo;
|
||||
import android.media.AudioManager;
|
||||
import android.os.Build;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
|
||||
class SDLAudioManager {
|
||||
protected static final String TAG = "SDLAudio";
|
||||
|
||||
protected static Context mContext;
|
||||
|
||||
private static AudioDeviceCallback mAudioDeviceCallback;
|
||||
|
||||
static void initialize() {
|
||||
mAudioDeviceCallback = null;
|
||||
|
||||
if(Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */)
|
||||
{
|
||||
mAudioDeviceCallback = new AudioDeviceCallback() {
|
||||
@Override
|
||||
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
|
||||
for (AudioDeviceInfo deviceInfo : addedDevices) {
|
||||
nativeAddAudioDevice(deviceInfo.isSink(), deviceInfo.getProductName().toString(), deviceInfo.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
|
||||
for (AudioDeviceInfo deviceInfo : removedDevices) {
|
||||
nativeRemoveAudioDevice(deviceInfo.isSink(), deviceInfo.getId());
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static void setContext(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
static void release(Context context) {
|
||||
// no-op atm
|
||||
}
|
||||
|
||||
// Audio
|
||||
|
||||
private static AudioDeviceInfo getInputAudioDeviceInfo(int deviceId) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) {
|
||||
if (deviceInfo.getId() == deviceId) {
|
||||
return deviceInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static AudioDeviceInfo getPlaybackAudioDeviceInfo(int deviceId) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
for (AudioDeviceInfo deviceInfo : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
|
||||
if (deviceInfo.getId() == deviceId) {
|
||||
return deviceInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static void registerAudioDeviceCallback() {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
// get an initial list now, before hotplug callbacks fire.
|
||||
for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)) {
|
||||
if (dev.getType() == AudioDeviceInfo.TYPE_TELEPHONY) {
|
||||
continue; // Device cannot be opened
|
||||
}
|
||||
nativeAddAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId());
|
||||
}
|
||||
for (AudioDeviceInfo dev : audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)) {
|
||||
nativeAddAudioDevice(dev.isSink(), dev.getProductName().toString(), dev.getId());
|
||||
}
|
||||
audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null);
|
||||
}
|
||||
}
|
||||
|
||||
static void unregisterAudioDeviceCallback() {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
|
||||
audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback);
|
||||
}
|
||||
}
|
||||
|
||||
/** This method is called by SDL using JNI. */
|
||||
static void audioSetThreadPriority(boolean recording, int device_id) {
|
||||
try {
|
||||
|
||||
/* Set thread name */
|
||||
if (recording) {
|
||||
Thread.currentThread().setName("SDLAudioC" + device_id);
|
||||
} else {
|
||||
Thread.currentThread().setName("SDLAudioP" + device_id);
|
||||
}
|
||||
|
||||
/* Set thread priority */
|
||||
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
|
||||
|
||||
} catch (Exception e) {
|
||||
Log.v(TAG, "modify thread properties failed " + e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
static native void nativeSetupJNI();
|
||||
|
||||
static native void nativeRemoveAudioDevice(boolean recording, int deviceId);
|
||||
|
||||
static native void nativeAddAudioDevice(boolean recording, String name, int deviceId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,935 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import android.content.Context;
|
||||
import android.hardware.lights.Light;
|
||||
import android.hardware.lights.LightsRequest;
|
||||
import android.hardware.lights.LightsManager;
|
||||
import android.hardware.lights.LightState;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.VibrationEffect;
|
||||
import android.os.Vibrator;
|
||||
import android.os.VibratorManager;
|
||||
import android.util.Log;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
|
||||
public class SDLControllerManager
|
||||
{
|
||||
|
||||
static native void nativeSetupJNI();
|
||||
|
||||
static native void nativeAddJoystick(int device_id, String name, String desc,
|
||||
int vendor_id, int product_id,
|
||||
int button_mask,
|
||||
int naxes, int axis_mask, int nhats, boolean can_rumble, boolean has_rgb_led);
|
||||
static native void nativeRemoveJoystick(int device_id);
|
||||
static native void nativeAddHaptic(int device_id, String name);
|
||||
static native void nativeRemoveHaptic(int device_id);
|
||||
static public native boolean onNativePadDown(int device_id, int keycode);
|
||||
static public native boolean onNativePadUp(int device_id, int keycode);
|
||||
static native void onNativeJoy(int device_id, int axis,
|
||||
float value);
|
||||
static native void onNativeHat(int device_id, int hat_id,
|
||||
int x, int y);
|
||||
|
||||
protected static SDLJoystickHandler mJoystickHandler;
|
||||
protected static SDLHapticHandler mHapticHandler;
|
||||
|
||||
private static final String TAG = "SDLControllerManager";
|
||||
|
||||
static void initialize() {
|
||||
if (mJoystickHandler == null) {
|
||||
mJoystickHandler = new SDLJoystickHandler();
|
||||
}
|
||||
|
||||
if (mHapticHandler == null) {
|
||||
if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
|
||||
mHapticHandler = new SDLHapticHandler_API31();
|
||||
} else if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) {
|
||||
mHapticHandler = new SDLHapticHandler_API26();
|
||||
} else {
|
||||
mHapticHandler = new SDLHapticHandler();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
|
||||
static public boolean handleJoystickMotionEvent(MotionEvent event) {
|
||||
return mJoystickHandler.handleMotionEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
static void pollInputDevices() {
|
||||
mJoystickHandler.pollInputDevices();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
static void joystickSetLED(int device_id, int red, int green, int blue) {
|
||||
mJoystickHandler.setLED(device_id, red, green, blue);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
static void pollHapticDevices() {
|
||||
mHapticHandler.pollHapticDevices();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
static void hapticRun(int device_id, float intensity, int length) {
|
||||
mHapticHandler.run(device_id, intensity, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
static void hapticRumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) {
|
||||
mHapticHandler.rumble(device_id, low_frequency_intensity, high_frequency_intensity, length);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by SDL using JNI.
|
||||
*/
|
||||
static void hapticStop(int device_id)
|
||||
{
|
||||
mHapticHandler.stop(device_id);
|
||||
}
|
||||
|
||||
// Check if a given device is considered a possible SDL joystick
|
||||
static public boolean isDeviceSDLJoystick(int deviceId) {
|
||||
InputDevice device = InputDevice.getDevice(deviceId);
|
||||
// We cannot use InputDevice.isVirtual before API 16, so let's accept
|
||||
// only nonnegative device ids (VIRTUAL_KEYBOARD equals -1)
|
||||
if ((device == null) || (deviceId < 0)) {
|
||||
return false;
|
||||
}
|
||||
int sources = device.getSources();
|
||||
|
||||
/* This is called for every button press, so let's not spam the logs */
|
||||
/*
|
||||
if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
||||
Log.v(TAG, "Input device " + device.getName() + " has class joystick.");
|
||||
}
|
||||
if ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) {
|
||||
Log.v(TAG, "Input device " + device.getName() + " is a dpad.");
|
||||
}
|
||||
if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
|
||||
Log.v(TAG, "Input device " + device.getName() + " is a gamepad.");
|
||||
}
|
||||
*/
|
||||
|
||||
return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 ||
|
||||
((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) ||
|
||||
((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* Actual joystick functionality available for API >= 19 devices */
|
||||
class SDLJoystickHandler {
|
||||
|
||||
static class SDLJoystick {
|
||||
int device_id;
|
||||
String name;
|
||||
String desc;
|
||||
ArrayList<InputDevice.MotionRange> axes;
|
||||
ArrayList<InputDevice.MotionRange> hats;
|
||||
ArrayList<Light> lights;
|
||||
LightsManager.LightsSession lightsSession;
|
||||
}
|
||||
static class RangeComparator implements Comparator<InputDevice.MotionRange> {
|
||||
@Override
|
||||
public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
|
||||
// Some controllers, like the Moga Pro 2, return AXIS_GAS (22) for right trigger and AXIS_BRAKE (23) for left trigger - swap them so they're sorted in the right order for SDL
|
||||
int arg0Axis = arg0.getAxis();
|
||||
int arg1Axis = arg1.getAxis();
|
||||
if (arg0Axis == MotionEvent.AXIS_GAS) {
|
||||
arg0Axis = MotionEvent.AXIS_BRAKE;
|
||||
} else if (arg0Axis == MotionEvent.AXIS_BRAKE) {
|
||||
arg0Axis = MotionEvent.AXIS_GAS;
|
||||
}
|
||||
if (arg1Axis == MotionEvent.AXIS_GAS) {
|
||||
arg1Axis = MotionEvent.AXIS_BRAKE;
|
||||
} else if (arg1Axis == MotionEvent.AXIS_BRAKE) {
|
||||
arg1Axis = MotionEvent.AXIS_GAS;
|
||||
}
|
||||
|
||||
// Make sure the AXIS_Z is sorted between AXIS_RY and AXIS_RZ.
|
||||
// This is because the usual pairing are:
|
||||
// - AXIS_X + AXIS_Y (left stick).
|
||||
// - AXIS_RX, AXIS_RY (sometimes the right stick, sometimes triggers).
|
||||
// - AXIS_Z, AXIS_RZ (sometimes the right stick, sometimes triggers).
|
||||
// This sorts the axes in the above order, which tends to be correct
|
||||
// for Xbox-ish game pads that have the right stick on RX/RY and the
|
||||
// triggers on Z/RZ.
|
||||
//
|
||||
// Gamepads that don't have AXIS_Z/AXIS_RZ but use
|
||||
// AXIS_LTRIGGER/AXIS_RTRIGGER are unaffected by this.
|
||||
//
|
||||
// References:
|
||||
// - https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input
|
||||
// - https://www.kernel.org/doc/html/latest/input/gamepad.html
|
||||
if (arg0Axis == MotionEvent.AXIS_Z) {
|
||||
arg0Axis = MotionEvent.AXIS_RZ - 1;
|
||||
} else if (arg0Axis > MotionEvent.AXIS_Z && arg0Axis < MotionEvent.AXIS_RZ) {
|
||||
--arg0Axis;
|
||||
}
|
||||
if (arg1Axis == MotionEvent.AXIS_Z) {
|
||||
arg1Axis = MotionEvent.AXIS_RZ - 1;
|
||||
} else if (arg1Axis > MotionEvent.AXIS_Z && arg1Axis < MotionEvent.AXIS_RZ) {
|
||||
--arg1Axis;
|
||||
}
|
||||
|
||||
return arg0Axis - arg1Axis;
|
||||
}
|
||||
}
|
||||
|
||||
private final ArrayList<SDLJoystick> mJoysticks;
|
||||
|
||||
SDLJoystickHandler() {
|
||||
|
||||
mJoysticks = new ArrayList<SDLJoystick>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles adding and removing of input devices.
|
||||
*/
|
||||
synchronized void pollInputDevices() {
|
||||
int[] deviceIds = InputDevice.getDeviceIds();
|
||||
|
||||
for (int device_id : deviceIds) {
|
||||
if (SDLControllerManager.isDeviceSDLJoystick(device_id)) {
|
||||
SDLJoystick joystick = getJoystick(device_id);
|
||||
if (joystick == null) {
|
||||
InputDevice joystickDevice = InputDevice.getDevice(device_id);
|
||||
joystick = new SDLJoystick();
|
||||
joystick.device_id = device_id;
|
||||
joystick.name = joystickDevice.getName();
|
||||
joystick.desc = getJoystickDescriptor(joystickDevice);
|
||||
joystick.axes = new ArrayList<InputDevice.MotionRange>();
|
||||
joystick.hats = new ArrayList<InputDevice.MotionRange>();
|
||||
joystick.lights = new ArrayList<Light>();
|
||||
|
||||
List<InputDevice.MotionRange> ranges = joystickDevice.getMotionRanges();
|
||||
Collections.sort(ranges, new RangeComparator());
|
||||
for (InputDevice.MotionRange range : ranges) {
|
||||
if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
|
||||
if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {
|
||||
joystick.hats.add(range);
|
||||
} else {
|
||||
joystick.axes.add(range);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean can_rumble = false;
|
||||
boolean has_rgb_led = false;
|
||||
if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
|
||||
VibratorManager vibratorManager = joystickDevice.getVibratorManager();
|
||||
int[] vibrators = vibratorManager.getVibratorIds();
|
||||
if (vibrators.length > 0) {
|
||||
can_rumble = true;
|
||||
}
|
||||
LightsManager lightsManager = joystickDevice.getLightsManager();
|
||||
List<Light> lights = lightsManager.getLights();
|
||||
for (Light light : lights) {
|
||||
if (light.hasRgbControl()) {
|
||||
joystick.lights.add(light);
|
||||
}
|
||||
}
|
||||
if (!joystick.lights.isEmpty()) {
|
||||
joystick.lightsSession = lightsManager.openSession();
|
||||
has_rgb_led = true;
|
||||
}
|
||||
}
|
||||
|
||||
mJoysticks.add(joystick);
|
||||
SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc,
|
||||
getVendorId(joystickDevice), getProductId(joystickDevice),
|
||||
getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, can_rumble, has_rgb_led);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Check removed devices */
|
||||
ArrayList<Integer> removedDevices = null;
|
||||
for (SDLJoystick joystick : mJoysticks) {
|
||||
int device_id = joystick.device_id;
|
||||
int i;
|
||||
for (i = 0; i < deviceIds.length; i++) {
|
||||
if (device_id == deviceIds[i]) break;
|
||||
}
|
||||
if (i == deviceIds.length) {
|
||||
if (removedDevices == null) {
|
||||
removedDevices = new ArrayList<Integer>();
|
||||
}
|
||||
removedDevices.add(device_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (removedDevices != null) {
|
||||
for (int device_id : removedDevices) {
|
||||
SDLControllerManager.nativeRemoveJoystick(device_id);
|
||||
for (int i = 0; i < mJoysticks.size(); i++) {
|
||||
if (mJoysticks.get(i).device_id == device_id) {
|
||||
if (Build.VERSION.SDK_INT >= 31 /* Android 12.0 (S) */) {
|
||||
if (mJoysticks.get(i).lightsSession != null) {
|
||||
try {
|
||||
mJoysticks.get(i).lightsSession.close();
|
||||
} catch (Exception e) {
|
||||
// Session may already be unregistered when device disconnects
|
||||
}
|
||||
mJoysticks.get(i).lightsSession = null;
|
||||
}
|
||||
}
|
||||
mJoysticks.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized protected SDLJoystick getJoystick(int device_id) {
|
||||
for (SDLJoystick joystick : mJoysticks) {
|
||||
if (joystick.device_id == device_id) {
|
||||
return joystick;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles given MotionEvent.
|
||||
* @param event the event to be handled.
|
||||
* @return if given event was processed.
|
||||
*/
|
||||
boolean handleMotionEvent(MotionEvent event) {
|
||||
int actionPointerIndex = event.getActionIndex();
|
||||
int action = event.getActionMasked();
|
||||
if (action == MotionEvent.ACTION_MOVE) {
|
||||
SDLJoystick joystick = getJoystick(event.getDeviceId());
|
||||
if (joystick != null) {
|
||||
for (int i = 0; i < joystick.axes.size(); i++) {
|
||||
InputDevice.MotionRange range = joystick.axes.get(i);
|
||||
/* Normalize the value to -1...1 */
|
||||
float value = (event.getAxisValue(range.getAxis(), actionPointerIndex) - range.getMin()) / range.getRange() * 2.0f - 1.0f;
|
||||
SDLControllerManager.onNativeJoy(joystick.device_id, i, value);
|
||||
}
|
||||
for (int i = 0; i < joystick.hats.size() / 2; i++) {
|
||||
int hatX = Math.round(event.getAxisValue(joystick.hats.get(2 * i).getAxis(), actionPointerIndex));
|
||||
int hatY = Math.round(event.getAxisValue(joystick.hats.get(2 * i + 1).getAxis(), actionPointerIndex));
|
||||
SDLControllerManager.onNativeHat(joystick.device_id, i, hatX, hatY);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
String getJoystickDescriptor(InputDevice joystickDevice) {
|
||||
String desc = joystickDevice.getDescriptor();
|
||||
|
||||
if (desc != null && !desc.isEmpty()) {
|
||||
return desc;
|
||||
}
|
||||
|
||||
return joystickDevice.getName();
|
||||
}
|
||||
|
||||
int getProductId(InputDevice joystickDevice) {
|
||||
return joystickDevice.getProductId();
|
||||
}
|
||||
|
||||
int getVendorId(InputDevice joystickDevice) {
|
||||
return joystickDevice.getVendorId();
|
||||
}
|
||||
|
||||
int getAxisMask(List<InputDevice.MotionRange> ranges) {
|
||||
// For compatibility, keep computing the axis mask like before,
|
||||
// only really distinguishing 2, 4 and 6 axes.
|
||||
int axis_mask = 0;
|
||||
if (ranges.size() >= 2) {
|
||||
// ((1 << SDL_GAMEPAD_AXIS_LEFTX) | (1 << SDL_GAMEPAD_AXIS_LEFTY))
|
||||
axis_mask |= 0x0003;
|
||||
}
|
||||
if (ranges.size() >= 4) {
|
||||
// ((1 << SDL_GAMEPAD_AXIS_RIGHTX) | (1 << SDL_GAMEPAD_AXIS_RIGHTY))
|
||||
axis_mask |= 0x000c;
|
||||
}
|
||||
if (ranges.size() >= 6) {
|
||||
// ((1 << SDL_GAMEPAD_AXIS_LEFT_TRIGGER) | (1 << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER))
|
||||
axis_mask |= 0x0030;
|
||||
}
|
||||
// Also add an indicator bit for whether the sorting order has changed.
|
||||
// This serves to disable outdated gamecontrollerdb.txt mappings.
|
||||
boolean have_z = false;
|
||||
boolean have_past_z_before_rz = false;
|
||||
for (InputDevice.MotionRange range : ranges) {
|
||||
int axis = range.getAxis();
|
||||
if (axis == MotionEvent.AXIS_Z) {
|
||||
have_z = true;
|
||||
} else if (axis > MotionEvent.AXIS_Z && axis < MotionEvent.AXIS_RZ) {
|
||||
have_past_z_before_rz = true;
|
||||
}
|
||||
}
|
||||
if (have_z && have_past_z_before_rz) {
|
||||
// If both these exist, the compare() function changed sorting order.
|
||||
// Set a bit to indicate this fact.
|
||||
axis_mask |= 0x8000;
|
||||
}
|
||||
return axis_mask;
|
||||
}
|
||||
|
||||
int getButtonMask(InputDevice joystickDevice) {
|
||||
int button_mask = 0;
|
||||
int[] keys = new int[] {
|
||||
KeyEvent.KEYCODE_BUTTON_A,
|
||||
KeyEvent.KEYCODE_BUTTON_B,
|
||||
KeyEvent.KEYCODE_BUTTON_X,
|
||||
KeyEvent.KEYCODE_BUTTON_Y,
|
||||
KeyEvent.KEYCODE_BACK,
|
||||
KeyEvent.KEYCODE_MENU,
|
||||
KeyEvent.KEYCODE_BUTTON_MODE,
|
||||
KeyEvent.KEYCODE_BUTTON_START,
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBL,
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBR,
|
||||
KeyEvent.KEYCODE_BUTTON_L1,
|
||||
KeyEvent.KEYCODE_BUTTON_R1,
|
||||
KeyEvent.KEYCODE_DPAD_UP,
|
||||
KeyEvent.KEYCODE_DPAD_DOWN,
|
||||
KeyEvent.KEYCODE_DPAD_LEFT,
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT,
|
||||
KeyEvent.KEYCODE_BUTTON_SELECT,
|
||||
KeyEvent.KEYCODE_DPAD_CENTER,
|
||||
|
||||
// These don't map into any SDL controller buttons directly
|
||||
KeyEvent.KEYCODE_BUTTON_L2,
|
||||
KeyEvent.KEYCODE_BUTTON_R2,
|
||||
KeyEvent.KEYCODE_BUTTON_C,
|
||||
KeyEvent.KEYCODE_BUTTON_Z,
|
||||
KeyEvent.KEYCODE_BUTTON_1,
|
||||
KeyEvent.KEYCODE_BUTTON_2,
|
||||
KeyEvent.KEYCODE_BUTTON_3,
|
||||
KeyEvent.KEYCODE_BUTTON_4,
|
||||
KeyEvent.KEYCODE_BUTTON_5,
|
||||
KeyEvent.KEYCODE_BUTTON_6,
|
||||
KeyEvent.KEYCODE_BUTTON_7,
|
||||
KeyEvent.KEYCODE_BUTTON_8,
|
||||
KeyEvent.KEYCODE_BUTTON_9,
|
||||
KeyEvent.KEYCODE_BUTTON_10,
|
||||
KeyEvent.KEYCODE_BUTTON_11,
|
||||
KeyEvent.KEYCODE_BUTTON_12,
|
||||
KeyEvent.KEYCODE_BUTTON_13,
|
||||
KeyEvent.KEYCODE_BUTTON_14,
|
||||
KeyEvent.KEYCODE_BUTTON_15,
|
||||
KeyEvent.KEYCODE_BUTTON_16,
|
||||
};
|
||||
int[] masks = new int[] {
|
||||
(1 << 0), // A -> A
|
||||
(1 << 1), // B -> B
|
||||
(1 << 2), // X -> X
|
||||
(1 << 3), // Y -> Y
|
||||
(1 << 4), // BACK -> BACK
|
||||
(1 << 6), // MENU -> START
|
||||
(1 << 5), // MODE -> GUIDE
|
||||
(1 << 6), // START -> START
|
||||
(1 << 7), // THUMBL -> LEFTSTICK
|
||||
(1 << 8), // THUMBR -> RIGHTSTICK
|
||||
(1 << 9), // L1 -> LEFTSHOULDER
|
||||
(1 << 10), // R1 -> RIGHTSHOULDER
|
||||
(1 << 11), // DPAD_UP -> DPAD_UP
|
||||
(1 << 12), // DPAD_DOWN -> DPAD_DOWN
|
||||
(1 << 13), // DPAD_LEFT -> DPAD_LEFT
|
||||
(1 << 14), // DPAD_RIGHT -> DPAD_RIGHT
|
||||
(1 << 4), // SELECT -> BACK
|
||||
(1 << 0), // DPAD_CENTER -> A
|
||||
(1 << 15), // L2 -> ??
|
||||
(1 << 16), // R2 -> ??
|
||||
(1 << 17), // C -> ??
|
||||
(1 << 18), // Z -> ??
|
||||
(1 << 20), // 1 -> ??
|
||||
(1 << 21), // 2 -> ??
|
||||
(1 << 22), // 3 -> ??
|
||||
(1 << 23), // 4 -> ??
|
||||
(1 << 24), // 5 -> ??
|
||||
(1 << 25), // 6 -> ??
|
||||
(1 << 26), // 7 -> ??
|
||||
(1 << 27), // 8 -> ??
|
||||
(1 << 28), // 9 -> ??
|
||||
(1 << 29), // 10 -> ??
|
||||
(1 << 30), // 11 -> ??
|
||||
(1 << 31), // 12 -> ??
|
||||
// We're out of room...
|
||||
0xFFFFFFFF, // 13 -> ??
|
||||
0xFFFFFFFF, // 14 -> ??
|
||||
0xFFFFFFFF, // 15 -> ??
|
||||
0xFFFFFFFF, // 16 -> ??
|
||||
};
|
||||
boolean[] has_keys = joystickDevice.hasKeys(keys);
|
||||
for (int i = 0; i < keys.length; ++i) {
|
||||
if (has_keys[i]) {
|
||||
button_mask |= masks[i];
|
||||
}
|
||||
}
|
||||
return button_mask;
|
||||
}
|
||||
|
||||
void setLED(int device_id, int red, int green, int blue) {
|
||||
if (Build.VERSION.SDK_INT < 31 /* Android 12.0 (S) */) {
|
||||
return;
|
||||
}
|
||||
SDLJoystick joystick = getJoystick(device_id);
|
||||
if (joystick == null || joystick.lights.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
LightsRequest.Builder lightsRequest = new LightsRequest.Builder();
|
||||
LightState lightState = new LightState.Builder().setColor(Color.rgb(red, green, blue)).build();
|
||||
for (Light light : joystick.lights) {
|
||||
if (light.hasRgbControl()) {
|
||||
lightsRequest.addLight(light, lightState);
|
||||
}
|
||||
}
|
||||
joystick.lightsSession.requestLights(lightsRequest.build());
|
||||
}
|
||||
}
|
||||
|
||||
class SDLHapticHandler_API31 extends SDLHapticHandler {
|
||||
@Override
|
||||
void run(int device_id, float intensity, int length) {
|
||||
SDLHaptic haptic = getHaptic(device_id);
|
||||
if (haptic != null) {
|
||||
vibrate(haptic.vib, intensity, length);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void rumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) {
|
||||
InputDevice device = InputDevice.getDevice(device_id);
|
||||
if (device == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT < 31 /* Android 12.0 (S) */) {
|
||||
/* Silence 'lint' warning */
|
||||
return;
|
||||
}
|
||||
|
||||
VibratorManager manager = device.getVibratorManager();
|
||||
int[] vibrators = manager.getVibratorIds();
|
||||
if (vibrators.length >= 2) {
|
||||
vibrate(manager.getVibrator(vibrators[0]), low_frequency_intensity, length);
|
||||
vibrate(manager.getVibrator(vibrators[1]), high_frequency_intensity, length);
|
||||
} else if (vibrators.length == 1) {
|
||||
float intensity = (low_frequency_intensity * 0.6f) + (high_frequency_intensity * 0.4f);
|
||||
vibrate(manager.getVibrator(vibrators[0]), intensity, length);
|
||||
}
|
||||
}
|
||||
|
||||
private void vibrate(Vibrator vibrator, float intensity, int length) {
|
||||
|
||||
if (Build.VERSION.SDK_INT < 31 /* Android 12.0 (S) */) {
|
||||
/* Silence 'lint' warning */
|
||||
return;
|
||||
}
|
||||
|
||||
if (intensity == 0.0f) {
|
||||
vibrator.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
int value = Math.round(intensity * 255);
|
||||
if (value > 255) {
|
||||
value = 255;
|
||||
}
|
||||
if (value < 1) {
|
||||
vibrator.cancel();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(length, value));
|
||||
}
|
||||
catch (Exception e) {
|
||||
// Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if
|
||||
// something went horribly wrong with the Android 8.0 APIs.
|
||||
vibrator.vibrate(length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SDLHapticHandler_API26 extends SDLHapticHandler {
|
||||
@Override
|
||||
void run(int device_id, float intensity, int length) {
|
||||
|
||||
if (Build.VERSION.SDK_INT < 26 /* Android 8.0 (O) */) {
|
||||
/* Silence 'lint' warning */
|
||||
return;
|
||||
}
|
||||
|
||||
SDLHaptic haptic = getHaptic(device_id);
|
||||
if (haptic != null) {
|
||||
if (intensity == 0.0f) {
|
||||
stop(device_id);
|
||||
return;
|
||||
}
|
||||
|
||||
int vibeValue = Math.round(intensity * 255);
|
||||
|
||||
if (vibeValue > 255) {
|
||||
vibeValue = 255;
|
||||
}
|
||||
if (vibeValue < 1) {
|
||||
stop(device_id);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
haptic.vib.vibrate(VibrationEffect.createOneShot(length, vibeValue));
|
||||
}
|
||||
catch (Exception e) {
|
||||
// Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if
|
||||
// something went horribly wrong with the Android 8.0 APIs.
|
||||
haptic.vib.vibrate(length);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SDLHapticHandler {
|
||||
|
||||
static class SDLHaptic {
|
||||
int device_id;
|
||||
String name;
|
||||
Vibrator vib;
|
||||
}
|
||||
|
||||
private final ArrayList<SDLHaptic> mHaptics;
|
||||
|
||||
SDLHapticHandler() {
|
||||
mHaptics = new ArrayList<SDLHaptic>();
|
||||
}
|
||||
|
||||
void run(int device_id, float intensity, int length) {
|
||||
SDLHaptic haptic = getHaptic(device_id);
|
||||
if (haptic != null) {
|
||||
haptic.vib.vibrate(length);
|
||||
}
|
||||
}
|
||||
|
||||
void rumble(int device_id, float low_frequency_intensity, float high_frequency_intensity, int length) {
|
||||
// Not supported in older APIs
|
||||
}
|
||||
|
||||
void stop(int device_id) {
|
||||
SDLHaptic haptic = getHaptic(device_id);
|
||||
if (haptic != null) {
|
||||
haptic.vib.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
synchronized void pollHapticDevices() {
|
||||
|
||||
final int deviceId_VIBRATOR_SERVICE = 999999;
|
||||
boolean hasVibratorService = false;
|
||||
|
||||
/* Check VIBRATOR_SERVICE */
|
||||
Vibrator vib = (Vibrator) SDL.getContext().getSystemService(Context.VIBRATOR_SERVICE);
|
||||
if (vib != null) {
|
||||
hasVibratorService = vib.hasVibrator();
|
||||
|
||||
if (hasVibratorService) {
|
||||
SDLHaptic haptic = getHaptic(deviceId_VIBRATOR_SERVICE);
|
||||
if (haptic == null) {
|
||||
haptic = new SDLHaptic();
|
||||
haptic.device_id = deviceId_VIBRATOR_SERVICE;
|
||||
haptic.name = "VIBRATOR_SERVICE";
|
||||
haptic.vib = vib;
|
||||
mHaptics.add(haptic);
|
||||
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Check removed devices */
|
||||
ArrayList<Integer> removedDevices = null;
|
||||
for (SDLHaptic haptic : mHaptics) {
|
||||
int device_id = haptic.device_id;
|
||||
if (device_id != deviceId_VIBRATOR_SERVICE || !hasVibratorService) {
|
||||
if (removedDevices == null) {
|
||||
removedDevices = new ArrayList<Integer>();
|
||||
}
|
||||
removedDevices.add(device_id);
|
||||
} // else: don't remove the vibrator if it is still present
|
||||
}
|
||||
|
||||
if (removedDevices != null) {
|
||||
for (int device_id : removedDevices) {
|
||||
SDLControllerManager.nativeRemoveHaptic(device_id);
|
||||
for (int i = 0; i < mHaptics.size(); i++) {
|
||||
if (mHaptics.get(i).device_id == device_id) {
|
||||
mHaptics.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
synchronized protected SDLHaptic getHaptic(int device_id) {
|
||||
for (SDLHaptic haptic : mHaptics) {
|
||||
if (haptic.device_id == device_id) {
|
||||
return haptic;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class SDLGenericMotionListener_API14 implements View.OnGenericMotionListener {
|
||||
protected static final int SDL_PEN_DEVICE_TYPE_UNKNOWN = 0;
|
||||
protected static final int SDL_PEN_DEVICE_TYPE_DIRECT = 1;
|
||||
protected static final int SDL_PEN_DEVICE_TYPE_INDIRECT = 2;
|
||||
|
||||
// Generic Motion (mouse hover, joystick...) events go here
|
||||
@Override
|
||||
public boolean onGenericMotion(View v, MotionEvent event) {
|
||||
if (event.getSource() == InputDevice.SOURCE_JOYSTICK)
|
||||
return SDLControllerManager.handleJoystickMotionEvent(event);
|
||||
|
||||
float x, y;
|
||||
int action = event.getActionMasked();
|
||||
int pointerCount = event.getPointerCount();
|
||||
boolean consumed = false;
|
||||
|
||||
for (int i = 0; i < pointerCount; i++) {
|
||||
int toolType = event.getToolType(i);
|
||||
|
||||
if (toolType == MotionEvent.TOOL_TYPE_MOUSE) {
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_SCROLL:
|
||||
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, i);
|
||||
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, i);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||
consumed = true;
|
||||
break;
|
||||
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
x = getEventX(event, i);
|
||||
y = getEventY(event, i);
|
||||
|
||||
SDLActivity.onNativeMouse(0, action, x, y, checkRelativeEvent(event));
|
||||
consumed = true;
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} else if (toolType == MotionEvent.TOOL_TYPE_STYLUS || toolType == MotionEvent.TOOL_TYPE_ERASER) {
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_HOVER_ENTER:
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
case MotionEvent.ACTION_HOVER_EXIT:
|
||||
x = event.getX(i);
|
||||
y = event.getY(i);
|
||||
float p = event.getPressure(i);
|
||||
if (p > 1.0f) {
|
||||
// may be larger than 1.0f on some devices
|
||||
// see the documentation of getPressure(i)
|
||||
p = 1.0f;
|
||||
}
|
||||
|
||||
// BUTTON_STYLUS_PRIMARY is 2^5, so shift by 4, and apply SDL_PEN_INPUT_DOWN/SDL_PEN_INPUT_ERASER_TIP
|
||||
int buttons = (event.getButtonState() >> 4) | (1 << (toolType == MotionEvent.TOOL_TYPE_STYLUS ? 0 : 30));
|
||||
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
|
||||
buttons |= 0x08;
|
||||
}
|
||||
|
||||
SDLActivity.onNativePen(event.getPointerId(i), getPenDeviceType(event.getDevice()), buttons, action, x, y, p);
|
||||
consumed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return consumed;
|
||||
}
|
||||
|
||||
boolean supportsRelativeMouse() {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean inRelativeMode() {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean setRelativeMouseEnabled(boolean enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void reclaimRelativeMouseModeIfNeeded() {
|
||||
|
||||
}
|
||||
|
||||
boolean checkRelativeEvent(MotionEvent event) {
|
||||
return inRelativeMode();
|
||||
}
|
||||
|
||||
float getEventX(MotionEvent event, int pointerIndex) {
|
||||
return event.getX(pointerIndex);
|
||||
}
|
||||
|
||||
float getEventY(MotionEvent event, int pointerIndex) {
|
||||
return event.getY(pointerIndex);
|
||||
}
|
||||
|
||||
int getPenDeviceType(InputDevice penDevice) {
|
||||
return SDL_PEN_DEVICE_TYPE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
|
||||
class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API14 {
|
||||
// Generic Motion (mouse hover, joystick...) events go here
|
||||
|
||||
private boolean mRelativeModeEnabled;
|
||||
|
||||
@Override
|
||||
boolean supportsRelativeMouse() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean inRelativeMode() {
|
||||
return mRelativeModeEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean setRelativeMouseEnabled(boolean enabled) {
|
||||
mRelativeModeEnabled = enabled;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
float getEventX(MotionEvent event, int pointerIndex) {
|
||||
if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) {
|
||||
/* Silence 'lint' warning */
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (mRelativeModeEnabled && event.getToolType(pointerIndex) == MotionEvent.TOOL_TYPE_MOUSE) {
|
||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X, pointerIndex);
|
||||
} else {
|
||||
return event.getX(pointerIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
float getEventY(MotionEvent event, int pointerIndex) {
|
||||
if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) {
|
||||
/* Silence 'lint' warning */
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (mRelativeModeEnabled && event.getToolType(pointerIndex) == MotionEvent.TOOL_TYPE_MOUSE) {
|
||||
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y, pointerIndex);
|
||||
} else {
|
||||
return event.getY(pointerIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 {
|
||||
// Generic Motion (mouse hover, joystick...) events go here
|
||||
private boolean mRelativeModeEnabled;
|
||||
|
||||
@Override
|
||||
boolean supportsRelativeMouse() {
|
||||
return (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */);
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean inRelativeMode() {
|
||||
return mRelativeModeEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean setRelativeMouseEnabled(boolean enabled) {
|
||||
|
||||
if (Build.VERSION.SDK_INT < 26 /* Android 8.0 (O) */) {
|
||||
/* Silence 'lint' warning */
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */) {
|
||||
if (enabled) {
|
||||
SDLActivity.getContentView().requestPointerCapture();
|
||||
} else {
|
||||
SDLActivity.getContentView().releasePointerCapture();
|
||||
}
|
||||
mRelativeModeEnabled = enabled;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void reclaimRelativeMouseModeIfNeeded() {
|
||||
|
||||
if (Build.VERSION.SDK_INT < 26 /* Android 8.0 (O) */) {
|
||||
/* Silence 'lint' warning */
|
||||
return;
|
||||
}
|
||||
|
||||
if (mRelativeModeEnabled && !SDLActivity.isDeXMode()) {
|
||||
SDLActivity.getContentView().requestPointerCapture();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean checkRelativeEvent(MotionEvent event) {
|
||||
if (Build.VERSION.SDK_INT < 26 /* Android 8.0 (O) */) {
|
||||
/* Silence 'lint' warning */
|
||||
return false;
|
||||
}
|
||||
return event.getSource() == InputDevice.SOURCE_MOUSE_RELATIVE;
|
||||
}
|
||||
|
||||
@Override
|
||||
float getEventX(MotionEvent event, int pointerIndex) {
|
||||
// Relative mouse in capture mode will only have relative for X/Y
|
||||
return event.getX(pointerIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
float getEventY(MotionEvent event, int pointerIndex) {
|
||||
// Relative mouse in capture mode will only have relative for X/Y
|
||||
return event.getY(pointerIndex);
|
||||
}
|
||||
}
|
||||
|
||||
class SDLGenericMotionListener_API29 extends SDLGenericMotionListener_API26 {
|
||||
@Override
|
||||
int getPenDeviceType(InputDevice penDevice)
|
||||
{
|
||||
if (penDevice == null) {
|
||||
return SDL_PEN_DEVICE_TYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
return penDevice.isExternal() ? SDL_PEN_DEVICE_TYPE_INDIRECT : SDL_PEN_DEVICE_TYPE_DIRECT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.content.*;
|
||||
import android.text.InputType;
|
||||
import android.view.*;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
|
||||
/* This is a fake invisible editor view that receives the input and defines the
|
||||
* pan&scan region
|
||||
*/
|
||||
public class SDLDummyEdit extends View implements View.OnKeyListener
|
||||
{
|
||||
InputConnection ic;
|
||||
int input_type;
|
||||
|
||||
SDLDummyEdit(Context context) {
|
||||
super(context);
|
||||
setFocusableInTouchMode(true);
|
||||
setFocusable(true);
|
||||
setOnKeyListener(this);
|
||||
}
|
||||
|
||||
void setInputType(int input_type) {
|
||||
this.input_type = input_type;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCheckIsTextEditor() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
return SDLActivity.handleKeyEvent(v, keyCode, event, ic);
|
||||
}
|
||||
|
||||
//
|
||||
@Override
|
||||
public boolean onKeyPreIme (int keyCode, KeyEvent event) {
|
||||
// As seen on StackOverflow: http://stackoverflow.com/questions/7634346/keyboard-hide-event
|
||||
// FIXME: Discussion at http://bugzilla.libsdl.org/show_bug.cgi?id=1639
|
||||
// FIXME: This is not a 100% effective solution to the problem of detecting if the keyboard is showing or not
|
||||
// FIXME: A more effective solution would be to assume our Layout to be RelativeLayout or LinearLayout
|
||||
// FIXME: And determine the keyboard presence doing this: http://stackoverflow.com/questions/2150078/how-to-check-visibility-of-software-keyboard-in-android
|
||||
// FIXME: An even more effective way would be if Android provided this out of the box, but where would the fun be in that :)
|
||||
if (event.getAction()==KeyEvent.ACTION_UP && keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
if (SDLActivity.mTextEdit != null && SDLActivity.mTextEdit.getVisibility() == View.VISIBLE) {
|
||||
SDLActivity.onNativeKeyboardFocusLost();
|
||||
}
|
||||
}
|
||||
return super.onKeyPreIme(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
|
||||
ic = new SDLInputConnection(this, true);
|
||||
|
||||
outAttrs.inputType = input_type;
|
||||
outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI |
|
||||
EditorInfo.IME_FLAG_NO_FULLSCREEN /* API 11 */;
|
||||
|
||||
return ic;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
import android.content.*;
|
||||
import android.os.Build;
|
||||
import android.text.Editable;
|
||||
import android.view.*;
|
||||
import android.view.inputmethod.BaseInputConnection;
|
||||
import android.widget.EditText;
|
||||
|
||||
class SDLInputConnection extends BaseInputConnection
|
||||
{
|
||||
protected EditText mEditText;
|
||||
protected String mCommittedText = "";
|
||||
|
||||
SDLInputConnection(View targetView, boolean fullEditor) {
|
||||
super(targetView, fullEditor);
|
||||
mEditText = new EditText(SDL.getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editable getEditable() {
|
||||
return mEditText.getEditableText();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean sendKeyEvent(KeyEvent event) {
|
||||
/*
|
||||
* This used to handle the keycodes from soft keyboard (and IME-translated input from hardkeyboard)
|
||||
* However, as of Ice Cream Sandwich and later, almost all soft keyboard doesn't generate key presses
|
||||
* and so we need to generate them ourselves in commitText. To avoid duplicates on the handful of keys
|
||||
* that still do, we empty this out.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Return DOES still generate a key event, however. So rather than using it as the 'click a button' key
|
||||
* as we do with physical keyboards, let's just use it to hide the keyboard.
|
||||
*/
|
||||
|
||||
if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER) {
|
||||
if (SDLActivity.onNativeSoftReturnKey()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return super.sendKeyEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean commitText(CharSequence text, int newCursorPosition) {
|
||||
if (!super.commitText(text, newCursorPosition)) {
|
||||
return false;
|
||||
}
|
||||
updateText();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean setComposingText(CharSequence text, int newCursorPosition) {
|
||||
if (!super.setComposingText(text, newCursorPosition)) {
|
||||
return false;
|
||||
}
|
||||
updateText();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
|
||||
if (Build.VERSION.SDK_INT <= 29 /* Android 10.0 (Q) */) {
|
||||
// Workaround to capture backspace key. Ref: http://stackoverflow.com/questions>/14560344/android-backspace-in-webview-baseinputconnection
|
||||
// and https://bugzilla.libsdl.org/show_bug.cgi?id=2265
|
||||
if (beforeLength > 0 && afterLength == 0) {
|
||||
// backspace(s)
|
||||
while (beforeLength-- > 0) {
|
||||
nativeGenerateScancodeForUnichar('\b');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!super.deleteSurroundingText(beforeLength, afterLength)) {
|
||||
return false;
|
||||
}
|
||||
updateText();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void updateText() {
|
||||
final Editable content = getEditable();
|
||||
if (content == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String text = content.toString();
|
||||
int compareLength = Math.min(text.length(), mCommittedText.length());
|
||||
int matchLength, offset;
|
||||
|
||||
/* Backspace over characters that are no longer in the string */
|
||||
for (matchLength = 0; matchLength < compareLength; ) {
|
||||
int codePoint = mCommittedText.codePointAt(matchLength);
|
||||
if (codePoint != text.codePointAt(matchLength)) {
|
||||
break;
|
||||
}
|
||||
matchLength += Character.charCount(codePoint);
|
||||
}
|
||||
/* FIXME: This doesn't handle graphemes, like '🌬️' */
|
||||
for (offset = matchLength; offset < mCommittedText.length(); ) {
|
||||
int codePoint = mCommittedText.codePointAt(offset);
|
||||
nativeGenerateScancodeForUnichar('\b');
|
||||
offset += Character.charCount(codePoint);
|
||||
}
|
||||
|
||||
if (matchLength < text.length()) {
|
||||
String pendingText = text.subSequence(matchLength, text.length()).toString();
|
||||
if (!SDLActivity.dispatchingKeyEvent()) {
|
||||
for (offset = 0; offset < pendingText.length(); ) {
|
||||
int codePoint = pendingText.codePointAt(offset);
|
||||
if (codePoint == '\n') {
|
||||
if (SDLActivity.onNativeSoftReturnKey()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* Higher code points don't generate simulated scancodes */
|
||||
if (codePoint > 0 && codePoint < 128) {
|
||||
nativeGenerateScancodeForUnichar((char)codePoint);
|
||||
}
|
||||
offset += Character.charCount(codePoint);
|
||||
}
|
||||
}
|
||||
SDLInputConnection.nativeCommitText(pendingText, 0);
|
||||
}
|
||||
mCommittedText = text;
|
||||
}
|
||||
|
||||
public static native void nativeCommitText(String text, int newCursorPosition);
|
||||
|
||||
public static native void nativeGenerateScancodeForUnichar(char c);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
package org.libsdl.app;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.graphics.Insets;
|
||||
import android.hardware.Sensor;
|
||||
import android.hardware.SensorEvent;
|
||||
import android.hardware.SensorEventListener;
|
||||
import android.hardware.SensorManager;
|
||||
import android.os.Build;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.PointerIcon;
|
||||
import android.view.Surface;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.SurfaceView;
|
||||
import android.view.View;
|
||||
import android.view.WindowInsets;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import android.view.ScaleGestureDetector;
|
||||
|
||||
/**
|
||||
SDLSurface. This is what we draw on, so we need to know when it's created
|
||||
in order to do anything useful.
|
||||
|
||||
Because of this, that's where we set up the SDL thread
|
||||
*/
|
||||
public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
|
||||
View.OnApplyWindowInsetsListener, View.OnKeyListener, View.OnTouchListener,
|
||||
SensorEventListener, ScaleGestureDetector.OnScaleGestureListener {
|
||||
|
||||
// Sensors
|
||||
protected SensorManager mSensorManager;
|
||||
protected Display mDisplay;
|
||||
|
||||
// Keep track of the surface size to normalize touch events
|
||||
protected float mWidth, mHeight;
|
||||
|
||||
// Is SurfaceView ready for rendering
|
||||
protected boolean mIsSurfaceReady;
|
||||
|
||||
// Pinch events
|
||||
private final ScaleGestureDetector scaleGestureDetector;
|
||||
|
||||
// Startup
|
||||
protected SDLSurface(Context context) {
|
||||
super(context);
|
||||
getHolder().addCallback(this);
|
||||
|
||||
scaleGestureDetector = new ScaleGestureDetector(context, this);
|
||||
|
||||
setFocusable(true);
|
||||
setFocusableInTouchMode(true);
|
||||
requestFocus();
|
||||
setOnApplyWindowInsetsListener(this);
|
||||
setOnKeyListener(this);
|
||||
setOnTouchListener(this);
|
||||
|
||||
mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
|
||||
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
|
||||
|
||||
setOnGenericMotionListener(SDLActivity.getMotionListener());
|
||||
|
||||
// Some arbitrary defaults to avoid a potential division by zero
|
||||
mWidth = 1.0f;
|
||||
mHeight = 1.0f;
|
||||
|
||||
mIsSurfaceReady = false;
|
||||
}
|
||||
|
||||
protected void handlePause() {
|
||||
enableSensor(Sensor.TYPE_ACCELEROMETER, false);
|
||||
}
|
||||
|
||||
protected void handleResume() {
|
||||
setFocusable(true);
|
||||
setFocusableInTouchMode(true);
|
||||
requestFocus();
|
||||
setOnApplyWindowInsetsListener(this);
|
||||
setOnKeyListener(this);
|
||||
setOnTouchListener(this);
|
||||
enableSensor(Sensor.TYPE_ACCELEROMETER, true);
|
||||
}
|
||||
|
||||
protected Surface getNativeSurface() {
|
||||
return getHolder().getSurface();
|
||||
}
|
||||
|
||||
// Called when we have a valid drawing surface
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
Log.v("SDL", "surfaceCreated()");
|
||||
SDLActivity.onNativeSurfaceCreated();
|
||||
}
|
||||
|
||||
// Called when we lose the surface
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
Log.v("SDL", "surfaceDestroyed()");
|
||||
|
||||
// Transition to pause, if needed
|
||||
SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
|
||||
SDLActivity.handleNativeState();
|
||||
|
||||
mIsSurfaceReady = false;
|
||||
SDLActivity.onNativeSurfaceDestroyed();
|
||||
}
|
||||
|
||||
// Called when the surface is resized
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder,
|
||||
int format, int width, int height) {
|
||||
Log.v("SDL", "surfaceChanged()");
|
||||
|
||||
if (SDLActivity.mSingleton == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mWidth = width;
|
||||
mHeight = height;
|
||||
int nDeviceWidth = width;
|
||||
int nDeviceHeight = height;
|
||||
float density = 1.0f;
|
||||
try
|
||||
{
|
||||
DisplayMetrics realMetrics = new DisplayMetrics();
|
||||
mDisplay.getRealMetrics( realMetrics );
|
||||
nDeviceWidth = realMetrics.widthPixels;
|
||||
nDeviceHeight = realMetrics.heightPixels;
|
||||
// Use densityDpi instead of density to more closely match what the UI scale is
|
||||
density = (float)realMetrics.densityDpi / 160.0f;
|
||||
} catch(Exception ignored) {
|
||||
}
|
||||
|
||||
synchronized(SDLActivity.getContext()) {
|
||||
// In case we're waiting on a size change after going fullscreen, send a notification.
|
||||
SDLActivity.getContext().notifyAll();
|
||||
}
|
||||
|
||||
Log.v("SDL", "Window size: " + width + "x" + height);
|
||||
Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
|
||||
SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, density, mDisplay.getRefreshRate());
|
||||
SDLActivity.onNativeResize();
|
||||
|
||||
// Prevent a screen distortion glitch,
|
||||
// for instance when the device is in Landscape and a Portrait App is resumed.
|
||||
boolean skip = false;
|
||||
int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
|
||||
|
||||
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
|
||||
if (mWidth > mHeight) {
|
||||
skip = true;
|
||||
}
|
||||
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
|
||||
if (mWidth < mHeight) {
|
||||
skip = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Special Patch for Square Resolution: Black Berry Passport
|
||||
if (skip) {
|
||||
double min = Math.min(mWidth, mHeight);
|
||||
double max = Math.max(mWidth, mHeight);
|
||||
|
||||
if (max / min < 1.20) {
|
||||
Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
|
||||
skip = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't skip if we might be multi-window or have popup dialogs
|
||||
if (skip) {
|
||||
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
|
||||
skip = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (skip) {
|
||||
Log.v("SDL", "Skip .. Surface is not ready.");
|
||||
mIsSurfaceReady = false;
|
||||
return;
|
||||
}
|
||||
|
||||
/* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
|
||||
SDLActivity.onNativeSurfaceChanged();
|
||||
|
||||
/* Surface is ready */
|
||||
mIsSurfaceReady = true;
|
||||
|
||||
SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
|
||||
SDLActivity.handleNativeState();
|
||||
}
|
||||
|
||||
// Window inset
|
||||
@Override
|
||||
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
|
||||
if (Build.VERSION.SDK_INT >= 30 /* Android 11 (R) */) {
|
||||
Insets combined = insets.getInsets(WindowInsets.Type.systemBars() |
|
||||
WindowInsets.Type.systemGestures() |
|
||||
WindowInsets.Type.mandatorySystemGestures() |
|
||||
WindowInsets.Type.tappableElement() |
|
||||
WindowInsets.Type.displayCutout());
|
||||
|
||||
SDLActivity.onNativeInsetsChanged(combined.left, combined.right, combined.top, combined.bottom);
|
||||
}
|
||||
|
||||
// Pass these to any child views in case they need them
|
||||
return insets;
|
||||
}
|
||||
|
||||
// Key events
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
return SDLActivity.handleKeyEvent(v, keyCode, event, null);
|
||||
}
|
||||
|
||||
private float getNormalizedX(float x)
|
||||
{
|
||||
if (mWidth <= 1) {
|
||||
return 0.5f;
|
||||
} else {
|
||||
return (x / (mWidth - 1));
|
||||
}
|
||||
}
|
||||
|
||||
private float getNormalizedY(float y)
|
||||
{
|
||||
if (mHeight <= 1) {
|
||||
return 0.5f;
|
||||
} else {
|
||||
return (y / (mHeight - 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Touch events
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
/* Ref: http://developer.android.com/training/gestures/multi.html */
|
||||
int touchDevId = event.getDeviceId();
|
||||
final int pointerCount = event.getPointerCount();
|
||||
int action = event.getActionMasked();
|
||||
int pointerId;
|
||||
int i = 0;
|
||||
float x,y,p;
|
||||
|
||||
if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN)
|
||||
i = event.getActionIndex();
|
||||
|
||||
do {
|
||||
int toolType = event.getToolType(i);
|
||||
|
||||
if (toolType == MotionEvent.TOOL_TYPE_MOUSE) {
|
||||
int buttonState = event.getButtonState();
|
||||
boolean relative = false;
|
||||
|
||||
// We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
|
||||
// if we are. We'll leverage our existing mouse motion listener
|
||||
SDLGenericMotionListener_API14 motionListener = SDLActivity.getMotionListener();
|
||||
x = motionListener.getEventX(event, i);
|
||||
y = motionListener.getEventY(event, i);
|
||||
relative = motionListener.inRelativeMode();
|
||||
|
||||
SDLActivity.onNativeMouse(buttonState, action, x, y, relative);
|
||||
} else if (toolType == MotionEvent.TOOL_TYPE_STYLUS || toolType == MotionEvent.TOOL_TYPE_ERASER) {
|
||||
pointerId = event.getPointerId(i);
|
||||
x = event.getX(i);
|
||||
y = event.getY(i);
|
||||
p = event.getPressure(i);
|
||||
if (p > 1.0f) {
|
||||
// may be larger than 1.0f on some devices
|
||||
// see the documentation of getPressure(i)
|
||||
p = 1.0f;
|
||||
}
|
||||
|
||||
// BUTTON_STYLUS_PRIMARY is 2^5, so shift by 4, and apply SDL_PEN_INPUT_DOWN/SDL_PEN_INPUT_ERASER_TIP
|
||||
int buttonState = (event.getButtonState() >> 4) | (1 << (toolType == MotionEvent.TOOL_TYPE_STYLUS ? 0 : 30));
|
||||
if ((event.getButtonState() & MotionEvent.BUTTON_TERTIARY) != 0) {
|
||||
buttonState |= 0x08;
|
||||
}
|
||||
|
||||
SDLActivity.onNativePen(pointerId, SDLActivity.getMotionListener().getPenDeviceType(event.getDevice()), buttonState, action, x, y, p);
|
||||
} else { // MotionEvent.TOOL_TYPE_FINGER or MotionEvent.TOOL_TYPE_UNKNOWN
|
||||
pointerId = event.getPointerId(i);
|
||||
x = getNormalizedX(event.getX(i));
|
||||
y = getNormalizedY(event.getY(i));
|
||||
p = event.getPressure(i);
|
||||
if (p > 1.0f) {
|
||||
// may be larger than 1.0f on some devices
|
||||
// see the documentation of getPressure(i)
|
||||
p = 1.0f;
|
||||
}
|
||||
|
||||
SDLActivity.onNativeTouch(touchDevId, pointerId, action, x, y, p);
|
||||
}
|
||||
|
||||
// Non-primary up/down
|
||||
if (action == MotionEvent.ACTION_POINTER_UP || action == MotionEvent.ACTION_POINTER_DOWN)
|
||||
break;
|
||||
} while (++i < pointerCount);
|
||||
|
||||
scaleGestureDetector.onTouchEvent(event);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Sensor events
|
||||
protected void enableSensor(int sensortype, boolean enabled) {
|
||||
// TODO: This uses getDefaultSensor - what if we have >1 accels?
|
||||
if (enabled) {
|
||||
mSensorManager.registerListener(this,
|
||||
mSensorManager.getDefaultSensor(sensortype),
|
||||
SensorManager.SENSOR_DELAY_GAME, null);
|
||||
} else {
|
||||
mSensorManager.unregisterListener(this,
|
||||
mSensorManager.getDefaultSensor(sensortype));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAccuracyChanged(Sensor sensor, int accuracy) {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSensorChanged(SensorEvent event) {
|
||||
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
|
||||
|
||||
// Since we may have an orientation set, we won't receive onConfigurationChanged events.
|
||||
// We thus should check here.
|
||||
int newRotation;
|
||||
|
||||
float x, y;
|
||||
switch (mDisplay.getRotation()) {
|
||||
case Surface.ROTATION_0:
|
||||
default:
|
||||
x = event.values[0];
|
||||
y = event.values[1];
|
||||
newRotation = 0;
|
||||
break;
|
||||
case Surface.ROTATION_90:
|
||||
x = -event.values[1];
|
||||
y = event.values[0];
|
||||
newRotation = 90;
|
||||
break;
|
||||
case Surface.ROTATION_180:
|
||||
x = -event.values[0];
|
||||
y = -event.values[1];
|
||||
newRotation = 180;
|
||||
break;
|
||||
case Surface.ROTATION_270:
|
||||
x = event.values[1];
|
||||
y = -event.values[0];
|
||||
newRotation = 270;
|
||||
break;
|
||||
}
|
||||
|
||||
if (newRotation != SDLActivity.mCurrentRotation) {
|
||||
SDLActivity.mCurrentRotation = newRotation;
|
||||
SDLActivity.onNativeRotationChanged(newRotation);
|
||||
}
|
||||
|
||||
SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
|
||||
y / SensorManager.GRAVITY_EARTH,
|
||||
event.values[2] / SensorManager.GRAVITY_EARTH);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent android internal NullPointerException (https://github.com/libsdl-org/SDL/issues/13306)
|
||||
@Override
|
||||
public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
|
||||
try {
|
||||
return super.onResolvePointerIcon(event, pointerIndex);
|
||||
} catch (NullPointerException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Captured pointer events for API 26.
|
||||
@Override
|
||||
public boolean onCapturedPointerEvent(MotionEvent event)
|
||||
{
|
||||
int action = event.getActionMasked();
|
||||
int pointerCount = event.getPointerCount();
|
||||
|
||||
for (int i = 0; i < pointerCount; i++) {
|
||||
float x, y;
|
||||
switch (action) {
|
||||
case MotionEvent.ACTION_SCROLL:
|
||||
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, i);
|
||||
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, i);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, false);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_HOVER_MOVE:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
x = event.getX(i);
|
||||
y = event.getY(i);
|
||||
SDLActivity.onNativeMouse(0, action, x, y, true);
|
||||
return true;
|
||||
|
||||
case MotionEvent.ACTION_BUTTON_PRESS:
|
||||
case MotionEvent.ACTION_BUTTON_RELEASE:
|
||||
|
||||
// Change our action value to what SDL's code expects.
|
||||
if (action == MotionEvent.ACTION_BUTTON_PRESS) {
|
||||
action = MotionEvent.ACTION_DOWN;
|
||||
} else { /* MotionEvent.ACTION_BUTTON_RELEASE */
|
||||
action = MotionEvent.ACTION_UP;
|
||||
}
|
||||
|
||||
x = event.getX(i);
|
||||
y = event.getY(i);
|
||||
int button = event.getButtonState();
|
||||
|
||||
SDLActivity.onNativeMouse(button, action, x, y, true);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScale(ScaleGestureDetector detector) {
|
||||
float scale = detector.getScaleFactor();
|
||||
SDLActivity.onNativePinchUpdate(scale);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScaleBegin(ScaleGestureDetector detector) {
|
||||
SDLActivity.onNativePinchStart();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScaleEnd(ScaleGestureDetector detector) {
|
||||
SDLActivity.onNativePinchEnd();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Dusk</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<game-mode-config
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:supportsBatteryGameMode="true"
|
||||
android:supportsPerformanceGameMode="true"
|
||||
/>
|
||||
@@ -0,0 +1,3 @@
|
||||
plugins {
|
||||
id 'com.android.application' version '8.13.2' apply false
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8
|
||||
android.useAndroidX=true
|
||||
Binary file not shown.
@@ -0,0 +1,6 @@
|
||||
#Thu Nov 11 18:20:34 PST 2021
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
Vendored
+160
@@ -0,0 +1,160 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn ( ) {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die ( ) {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
esac
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|grep -E -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|grep -E -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
|
||||
function splitJvmOpts() {
|
||||
JVM_OPTS=("$@")
|
||||
}
|
||||
eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
|
||||
JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
|
||||
|
||||
exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
|
||||
Vendored
+90
@@ -0,0 +1,90 @@
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windowz variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
if "%@eval[2+2]" == "4" goto 4NT_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
goto execute
|
||||
|
||||
:4NT_args
|
||||
@rem Get arguments from the 4NT Shell from JP Software
|
||||
set CMD_LINE_ARGS=%$
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
APP_DIR="$ROOT_DIR/platforms/android/app/src/main/jniLibs"
|
||||
ANDROID_HOME_DIR="${ANDROID_HOME:-$HOME/Android/Sdk}"
|
||||
ANDROID_NDK_VER="${ANDROID_NDK_VERSION:-}"
|
||||
ANDROID_STAGE_ABIS="${ANDROID_STAGE_ABIS:-arm64-v8a x86_64}"
|
||||
ANDROID_STAGE_STRIP="${ANDROID_STAGE_STRIP:-1}"
|
||||
STRIP_TOOL=""
|
||||
|
||||
if [[ -z "$ANDROID_NDK_VER" ]] && [[ -d "$ANDROID_HOME_DIR/ndk" ]]; then
|
||||
ANDROID_NDK_VER="$(ls -1 "$ANDROID_HOME_DIR/ndk" | sort -V | tail -n 1)"
|
||||
fi
|
||||
|
||||
if [[ -n "$ANDROID_NDK_VER" ]]; then
|
||||
TOOLCHAIN_BIN="$ANDROID_HOME_DIR/ndk/$ANDROID_NDK_VER/toolchains/llvm/prebuilt/linux-x86_64/bin"
|
||||
if [[ -x "$TOOLCHAIN_BIN/llvm-strip" ]]; then
|
||||
STRIP_TOOL="$TOOLCHAIN_BIN/llvm-strip"
|
||||
fi
|
||||
fi
|
||||
|
||||
copy_lib() {
|
||||
local abi="$1"
|
||||
local src="$2"
|
||||
local dst_dir="$APP_DIR/$abi"
|
||||
local dst="$dst_dir/libmain.so"
|
||||
mkdir -p "$dst_dir"
|
||||
cp -f "$src" "$dst"
|
||||
if [[ "$ANDROID_STAGE_STRIP" != "0" ]] && [[ -n "$STRIP_TOOL" ]]; then
|
||||
"$STRIP_TOOL" --strip-debug "$dst"
|
||||
echo "Staged and stripped $src -> $dst"
|
||||
else
|
||||
echo "Staged $src -> $dst (strip disabled or strip tool unavailable)"
|
||||
fi
|
||||
}
|
||||
|
||||
declare -A ABI_TO_LIB=(
|
||||
["arm64-v8a"]="$ROOT_DIR/build/android-arm64/libmain.so"
|
||||
["x86_64"]="$ROOT_DIR/build/android-x86_64/libmain.so"
|
||||
)
|
||||
|
||||
# Drop any previously staged ABI directories to avoid stale APK contents.
|
||||
rm -rf "$APP_DIR/x86" "$APP_DIR/arm64-v8a" "$APP_DIR/x86_64"
|
||||
|
||||
for abi in $ANDROID_STAGE_ABIS; do
|
||||
src="${ABI_TO_LIB[$abi]:-}"
|
||||
if [[ -z "$src" ]]; then
|
||||
echo "Unsupported ABI '$abi'. Supported ABIs: arm64-v8a x86_64" >&2
|
||||
exit 1
|
||||
fi
|
||||
copy_lib "$abi" "$src"
|
||||
done
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "$0")/../../.." && pwd)"
|
||||
SRC_DEFAULT="$ROOT_DIR/build/android-arm64/_deps/sdl-src/android-project/app/src/main/java/org/libsdl/app"
|
||||
SRC_DIR="${1:-$SRC_DEFAULT}"
|
||||
DST_DIR="$ROOT_DIR/platforms/android/app/src/main/java/org/libsdl/app"
|
||||
|
||||
if [[ ! -d "$SRC_DIR" ]]; then
|
||||
echo "SDL Java source directory not found: $SRC_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$DST_DIR"
|
||||
cp -f "$SRC_DIR"/*.java "$DST_DIR"/
|
||||
echo "Synced SDL Java sources from $SRC_DIR to $DST_DIR"
|
||||
@@ -0,0 +1,18 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.name = "dusk-android"
|
||||
include ':app'
|
||||
@@ -0,0 +1,91 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$InputPng,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$OutputIco
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Add-Type -AssemblyName System.Drawing
|
||||
|
||||
$iconSizes = @(16, 24, 32, 48, 64, 128, 256)
|
||||
|
||||
$inputPath = (Resolve-Path $InputPng).Path
|
||||
$outputPath = [System.IO.Path]::GetFullPath($OutputIco)
|
||||
$outputDir = Split-Path -Parent $outputPath
|
||||
if (-not [string]::IsNullOrEmpty($outputDir)) {
|
||||
[System.IO.Directory]::CreateDirectory($outputDir) | Out-Null
|
||||
}
|
||||
|
||||
$sourceImage = [System.Drawing.Image]::FromFile($inputPath)
|
||||
|
||||
try {
|
||||
$entries = New-Object System.Collections.Generic.List[object]
|
||||
|
||||
foreach ($size in $iconSizes) {
|
||||
$bitmap = New-Object System.Drawing.Bitmap $size, $size
|
||||
try {
|
||||
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
||||
try {
|
||||
$graphics.Clear([System.Drawing.Color]::Transparent)
|
||||
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
|
||||
$graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
|
||||
$graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
|
||||
$graphics.CompositingQuality = [System.Drawing.Drawing2D.CompositingQuality]::HighQuality
|
||||
$graphics.DrawImage($sourceImage, 0, 0, $size, $size)
|
||||
} finally {
|
||||
$graphics.Dispose()
|
||||
}
|
||||
|
||||
$memory = New-Object System.IO.MemoryStream
|
||||
try {
|
||||
$bitmap.Save($memory, [System.Drawing.Imaging.ImageFormat]::Png)
|
||||
$entries.Add([pscustomobject]@{
|
||||
Size = $size
|
||||
Bytes = $memory.ToArray()
|
||||
}) | Out-Null
|
||||
} finally {
|
||||
$memory.Dispose()
|
||||
}
|
||||
} finally {
|
||||
$bitmap.Dispose()
|
||||
}
|
||||
}
|
||||
|
||||
$fileStream = [System.IO.File]::Open($outputPath, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write)
|
||||
try {
|
||||
$writer = New-Object System.IO.BinaryWriter $fileStream
|
||||
try {
|
||||
$writer.Write([UInt16]0)
|
||||
$writer.Write([UInt16]1)
|
||||
$writer.Write([UInt16]$entries.Count)
|
||||
|
||||
$dataOffset = 6 + (16 * $entries.Count)
|
||||
foreach ($entry in $entries) {
|
||||
$dimension = if ($entry.Size -ge 256) { 0 } else { [byte]$entry.Size }
|
||||
$writer.Write([byte]$dimension)
|
||||
$writer.Write([byte]$dimension)
|
||||
$writer.Write([byte]0)
|
||||
$writer.Write([byte]0)
|
||||
$writer.Write([UInt16]1)
|
||||
$writer.Write([UInt16]32)
|
||||
$writer.Write([UInt32]$entry.Bytes.Length)
|
||||
$writer.Write([UInt32]$dataOffset)
|
||||
$dataOffset += $entry.Bytes.Length
|
||||
}
|
||||
|
||||
foreach ($entry in $entries) {
|
||||
$writer.Write($entry.Bytes)
|
||||
}
|
||||
} finally {
|
||||
$writer.Dispose()
|
||||
}
|
||||
} finally {
|
||||
$fileStream.Dispose()
|
||||
}
|
||||
} finally {
|
||||
$sourceImage.Dispose()
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<assemblyIdentity
|
||||
version="@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.@PROJECT_VERSION_TWEAK@"
|
||||
processorArchitecture="*"
|
||||
name="@DUSK_BUNDLE_IDENTIFIER@"
|
||||
type="win32"/>
|
||||
<description>@DUSK_FILE_DESCRIPTION@</description>
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity
|
||||
type="win32"
|
||||
name="Microsoft.Windows.Common-Controls"
|
||||
version="6.0.0.0"
|
||||
processorArchitecture="*"
|
||||
publicKeyToken="6595b64144ccf1df"
|
||||
language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
|
||||
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
|
||||
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
</assembly>
|
||||
@@ -0,0 +1,38 @@
|
||||
#include <windows.h>
|
||||
#include <winver.h>
|
||||
|
||||
IDI_MAIN_ICON ICON "@DUSK_WINDOWS_ICON_ICO@"
|
||||
1 RT_MANIFEST "@DUSK_WINDOWS_MANIFEST@"
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,@PROJECT_VERSION_TWEAK@
|
||||
PRODUCTVERSION @PROJECT_VERSION_MAJOR@,@PROJECT_VERSION_MINOR@,@PROJECT_VERSION_PATCH@,@PROJECT_VERSION_TWEAK@
|
||||
FILEFLAGSMASK 0x3fL
|
||||
#ifdef _DEBUG
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
FILESUBTYPE 0x0L
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904B0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "@DUSK_COMPANY_NAME@\0"
|
||||
VALUE "FileDescription", "@DUSK_FILE_DESCRIPTION@\0"
|
||||
VALUE "FileVersion", "@DUSK_VERSION_STRING@\0"
|
||||
VALUE "InternalName", "dusk\0"
|
||||
VALUE "LegalCopyright", "@DUSK_COPYRIGHT@\0"
|
||||
VALUE "OriginalFilename", "dusk.exe\0"
|
||||
VALUE "ProductName", "@DUSK_PRODUCT_NAME@\0"
|
||||
VALUE "ProductVersion", "@DUSK_VERSION_STRING@\0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x0409, 1200
|
||||
END
|
||||
END
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 340 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 95 KiB |
@@ -115,7 +115,14 @@ static Z2WolfHowlLine sNewSong3[9] = {
|
||||
{HOWL_LINE_MID, 45},
|
||||
};
|
||||
|
||||
static Z2WolfHowlData sGuideData[9] = {
|
||||
#if TARGET_PC
|
||||
static Z2WolfHowlLine sHowlTimeSong[6] = {
|
||||
{HOWL_LINE_MID, 20}, {HOWL_LINE_LOW, 20}, {HOWL_LINE_HIGH, 40},
|
||||
{HOWL_LINE_MID, 20}, {HOWL_LINE_LOW, 20}, {HOWL_LINE_HIGH, 40},
|
||||
};
|
||||
#endif
|
||||
|
||||
static Z2WolfHowlData sGuideData[9 IF_DUSK(+1)] = {
|
||||
{ARRAY_SIZE(sHowlTobikusa), sHowlTobikusa},
|
||||
{ARRAY_SIZE(sHowlUmakusa), sHowlUmakusa},
|
||||
{ARRAY_SIZE(sHowlZeldaSong), sHowlZeldaSong},
|
||||
@@ -125,6 +132,9 @@ static Z2WolfHowlData sGuideData[9] = {
|
||||
{ARRAY_SIZE(sNewSong1), sNewSong1},
|
||||
{ARRAY_SIZE(sNewSong2), sNewSong2},
|
||||
{ARRAY_SIZE(sNewSong3), sNewSong3},
|
||||
#if TARGET_PC
|
||||
{ARRAY_SIZE(sHowlTimeSong), sHowlTimeSong},
|
||||
#endif
|
||||
};
|
||||
|
||||
Z2WolfHowlMgr::Z2WolfHowlMgr() : JASGlobalInstance(true) {
|
||||
@@ -356,6 +366,13 @@ void Z2WolfHowlMgr::setCorrectData(s8 curveID, Z2WolfHowlData* data) {
|
||||
cPitchCenter = 0.94387f;
|
||||
cPitchDown = 0.74915f;
|
||||
break;
|
||||
#if TARGET_PC
|
||||
case Z2WOLFHOWL_TIMESONG:
|
||||
cPitchUp = 1.259906f;
|
||||
cPitchCenter = 0.94387f;
|
||||
cPitchDown = 0.840885f;
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
cPitchUp = 1.1892f;
|
||||
cPitchCenter = 1.0f;
|
||||
@@ -400,7 +417,7 @@ u8 Z2WolfHowlMgr::getCorrectLineNum() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static JAISoundID sCorrectPhrase[9] = {
|
||||
static JAISoundID sCorrectPhrase[9 IF_DUSK(+1)] = {
|
||||
Z2BGM_HOWL_TOBIKUSA,
|
||||
Z2BGM_HOWL_UMAKUSA,
|
||||
Z2BGM_HOWL_ZELDASONG,
|
||||
@@ -410,9 +427,12 @@ static JAISoundID sCorrectPhrase[9] = {
|
||||
Z2BGM_NEW_01_HOWL,
|
||||
Z2BGM_NEW_02_HOWL,
|
||||
Z2BGM_NEW_03_HOWL,
|
||||
#if TARGET_PC
|
||||
0xFFFFFFFF,
|
||||
#endif
|
||||
};
|
||||
|
||||
static JAISoundID sWindStoneSound[9] = {
|
||||
static JAISoundID sWindStoneSound[9 IF_DUSK(+1)] = {
|
||||
0xFFFFFFFF,
|
||||
0xFFFFFFFF,
|
||||
Z2BGM_STONE_ZELDASONG,
|
||||
@@ -422,9 +442,12 @@ static JAISoundID sWindStoneSound[9] = {
|
||||
Z2BGM_NEW_01_STONE,
|
||||
Z2BGM_NEW_02_STONE,
|
||||
Z2BGM_NEW_03_STONE,
|
||||
#if TARGET_PC
|
||||
0xFFFFFFFF,
|
||||
#endif
|
||||
};
|
||||
|
||||
static JAISoundID sCorrectDuo[9] = {
|
||||
static JAISoundID sCorrectDuo[9 IF_DUSK(+1)] = {
|
||||
0xFFFFFFFF,
|
||||
0xFFFFFFFF,
|
||||
0xFFFFFFFF,
|
||||
@@ -434,6 +457,9 @@ static JAISoundID sCorrectDuo[9] = {
|
||||
Z2BGM_NEW_01_DUO,
|
||||
Z2BGM_NEW_02_DUO,
|
||||
Z2BGM_NEW_03_DUO,
|
||||
#if TARGET_PC
|
||||
0xFFFFFFFF,
|
||||
#endif
|
||||
};
|
||||
|
||||
s8 Z2WolfHowlMgr::checkLine() {
|
||||
|
||||
@@ -4394,13 +4394,34 @@ void daAlink_c::setSelectEquipItem(BOOL param_0) {
|
||||
if (mClothesChangeWaitTimer == 0) {
|
||||
if (checkZoraWearAbility()) {
|
||||
if (checkZoraWearMaskDraw()) {
|
||||
field_0x06f0->show();
|
||||
#if TARGET_PC
|
||||
if (field_0x06f0 != NULL)
|
||||
#endif
|
||||
{
|
||||
field_0x06f0->show();
|
||||
}
|
||||
|
||||
if (!checkEquipHeavyBoots()) {
|
||||
field_0x06e4->show();
|
||||
#if TARGET_PC
|
||||
if (field_0x06e4 != NULL)
|
||||
#endif
|
||||
{
|
||||
field_0x06e4->show();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
field_0x06f0->hide();
|
||||
field_0x06e4->hide();
|
||||
#if TARGET_PC
|
||||
if (field_0x06f0 != NULL)
|
||||
#endif
|
||||
{
|
||||
field_0x06f0->hide();
|
||||
}
|
||||
#if TARGET_PC
|
||||
if (field_0x06e4 != NULL)
|
||||
#endif
|
||||
{
|
||||
field_0x06e4->hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19295,11 +19316,20 @@ void daAlink_c::setWaterDropColor(const J3DGXColorS10* i_color) {
|
||||
|
||||
if (!checkNoResetFlg2(FLG2_UNK_80000)) {
|
||||
if (checkZoraWearAbility()) {
|
||||
#if TARGET_PC
|
||||
if (field_0x064C->getMaterialNum() >= 14)
|
||||
#endif
|
||||
{
|
||||
field_0x064C->getMaterialNodePointer(13)->setTevColor(1, i_color);
|
||||
field_0x064C->getMaterialNodePointer(0)->setTevColor(1, i_color);
|
||||
field_0x064C->getMaterialNodePointer(1)->setTevColor(1, i_color);
|
||||
mpLinkHatModel->getModelData()->getMaterialNodePointer(1)->setTevColor(1, i_color);
|
||||
}
|
||||
} else if (checkMagicArmorWearAbility()) {
|
||||
#if TARGET_PC
|
||||
if (field_0x064C->getMaterialNum() >= 12)
|
||||
#endif
|
||||
{
|
||||
field_0x064C->getMaterialNodePointer(11)->setTevColor(1, i_color);
|
||||
field_0x064C->getMaterialNodePointer(10)->setTevColor(1, i_color);
|
||||
field_0x064C->getMaterialNodePointer(9)->setTevColor(1, i_color);
|
||||
@@ -19307,11 +19337,21 @@ void daAlink_c::setWaterDropColor(const J3DGXColorS10* i_color) {
|
||||
field_0x064C->getMaterialNodePointer(6)->setTevColor(1, i_color);
|
||||
mpLinkHatModel->getModelData()->getMaterialNodePointer(2)->setTevColor(1, i_color);
|
||||
mpLinkHatModel->getModelData()->getMaterialNodePointer(1)->setTevColor(1, i_color);
|
||||
}
|
||||
} else if (checkCasualWearFlg()) {
|
||||
#if TARGET_PC
|
||||
if (field_0x064C->getMaterialNum() >= 8)
|
||||
#endif
|
||||
{
|
||||
field_0x064C->getMaterialNodePointer(7)->setTevColor(1, i_color);
|
||||
mpLinkHatModel->getModelData()->getMaterialNodePointer(0)->setTevColor(1, i_color);
|
||||
field_0x064C->getMaterialNodePointer(5)->setTevColor(1, var_r31);
|
||||
}
|
||||
} else {
|
||||
#if TARGET_PC
|
||||
if (field_0x064C->getMaterialNum() >= 18)
|
||||
#endif
|
||||
{
|
||||
field_0x064C->getMaterialNodePointer(17)->setTevColor(1, i_color);
|
||||
field_0x064C->getMaterialNodePointer(9)->setTevColor(1, i_color);
|
||||
field_0x064C->getMaterialNodePointer(0)->setTevColor(1, i_color);
|
||||
@@ -19321,6 +19361,7 @@ void daAlink_c::setWaterDropColor(const J3DGXColorS10* i_color) {
|
||||
field_0x064C->getMaterialNodePointer(16)->setTevColor(1, var_r31);
|
||||
field_0x064C->getMaterialNodePointer(15)->setTevColor(1, var_r31);
|
||||
field_0x064C->getMaterialNodePointer(14)->setTevColor(1, var_r31);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19506,7 +19547,12 @@ int daAlink_c::draw() {
|
||||
field_0x06e8->hide();
|
||||
}
|
||||
|
||||
field_0x06f0->hide();
|
||||
#if TARGET_PC
|
||||
if (field_0x06f0 != NULL)
|
||||
#endif
|
||||
{
|
||||
field_0x06f0->hide();
|
||||
}
|
||||
|
||||
#if PLATFORM_SHIELD
|
||||
if (mProcID == PROC_HOOKSHOT_WALL_SHOOT || mProcID == PROC_HOOKSHOT_SUBJECT) {
|
||||
@@ -19536,7 +19582,12 @@ int daAlink_c::draw() {
|
||||
}
|
||||
|
||||
if (!checkZoraWearMaskDraw() && checkZoraWearAbility()) {
|
||||
field_0x06f0->hide();
|
||||
#if TARGET_PC
|
||||
if (field_0x06f0 != NULL)
|
||||
#endif
|
||||
{
|
||||
field_0x06f0->hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19545,7 +19596,12 @@ int daAlink_c::draw() {
|
||||
}
|
||||
|
||||
if (checkZoraWearMaskDraw() || !checkZoraWearAbility()) {
|
||||
field_0x06f0->show();
|
||||
#if TARGET_PC
|
||||
if (field_0x06f0 != NULL)
|
||||
#endif
|
||||
{
|
||||
field_0x06f0->show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,65 @@
|
||||
#include "d/d_meter2_draw.h"
|
||||
#include "d/d_meter2_info.h"
|
||||
|
||||
void daAlink_c::handleWolfHowl() {
|
||||
if (checkWolf()) {
|
||||
if (!dusk::getSettings().game.sunsSong) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to see if Link has the ability to transform.
|
||||
if (!dComIfGs_isEventBit(dSv_event_flag_c::M_077)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure there is a proper pointer to the mMeterClass and mpMeterDraw structs in
|
||||
// g_meter2_info.
|
||||
const auto meterClassPtr = g_meter2_info.getMeterClass();
|
||||
if (!meterClassPtr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto meterDrawPtr = meterClassPtr->getMeterDrawPtr();
|
||||
if (!meterDrawPtr) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure that link is not in a cutscene.
|
||||
if (checkEventRun()) {
|
||||
Z2GetAudioMgr()->seStart(Z2SE_SYS_ERROR, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
mDoCPd_c::getCpadInfo(PAD_1).mPressedButtonFlags = 0;
|
||||
|
||||
// Ensure that the Z Button is not dimmed
|
||||
if (meterDrawPtr->getButtonZAlpha() != 1.f) {
|
||||
Z2GetAudioMgr()->seStart(Z2SE_SYS_ERROR, NULL, 0, 0, 1.0f, 1.0f, -1.0f, -1.0f, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
bool canTransform = false;
|
||||
|
||||
if (mLinkAcch.ChkGroundHit() && !checkModeFlg(MODE_PLAYER_FLY) && !checkMagneBootsOn()) {
|
||||
if (!checkForestOldCentury()) {
|
||||
if (checkMidnaRide()) {
|
||||
if ((checkWolf() &&
|
||||
(checkModeFlg(MODE_UNK_1000) || dComIfGp_checkPlayerStatus0(0, 0x10))) ||
|
||||
(!checkWolf() &&
|
||||
(checkEventRun() || getMidnaActor()->checkMetamorphoseEnable()) &&
|
||||
(checkModeFlg(4) || dComIfGp_checkPlayerStatus0(0, 0x10))))
|
||||
{
|
||||
canTransform = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getWolfHowlMgrP()->setCorrectCurve(9);
|
||||
procWolfHowlDemoInit();
|
||||
}
|
||||
}
|
||||
|
||||
void daAlink_c::handleQuickTransform() {
|
||||
if (!dusk::getSettings().game.enableQuickTransform) {
|
||||
return;
|
||||
|
||||
@@ -3954,7 +3954,13 @@ int daAlink_c::procWolfHowlDemoInit() {
|
||||
mZ2WolfHowlMgr.setCorrectCurve(-1);
|
||||
}
|
||||
} else {
|
||||
#if TARGET_PC
|
||||
if (mZ2WolfHowlMgr.getCorrectCurveID() != 9) {
|
||||
mZ2WolfHowlMgr.setCorrectCurve(-1);
|
||||
}
|
||||
#else
|
||||
mZ2WolfHowlMgr.setCorrectCurve(-1);
|
||||
#endif
|
||||
}
|
||||
|
||||
mNormalSpeed = 0.0f;
|
||||
@@ -4095,6 +4101,19 @@ int daAlink_c::procWolfHowlDemo() {
|
||||
dStage_changeScene(mProcVar0.mHowlExitID, 0.0f, 0, fopAcM_GetRoomNo(this),
|
||||
shape_angle.y, -1);
|
||||
} else {
|
||||
#if TARGET_PC
|
||||
if (daAlink_getAlinkActorClass()->getCorrectCurveID() == 9) {
|
||||
if (dComIfGp_roomControl_getTimePass()) {
|
||||
g_env_light.time_change_rate = 1.0f;
|
||||
dComIfGp_event_reset();
|
||||
dCam_getBody()->EndEventCamera(fopAcM_GetID(this));
|
||||
} else {
|
||||
setWolfHowlNotHappen(isSkipEdge);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
|
||||
fopAc_ac_c* actor_p = NULL;
|
||||
if (gwolf_p == NULL) {
|
||||
fopAcIt_Executor((fopAcIt_ExecutorFunc)daAlink_searchWolfHowl,
|
||||
|
||||
@@ -277,6 +277,11 @@ void daOnsTaru_c::mode_init_drop() {
|
||||
cLib_offBit<u32>(attention_info.flags, fopAc_AttnFlag_CARRY_e);
|
||||
gravity = -7.0f;
|
||||
mMode = MODE_DROP_e;
|
||||
|
||||
#if TARGET_PC
|
||||
// TODO: figure out why this is needed and where exactly the UB happens
|
||||
mCcStts.ClrCcMove();
|
||||
#endif
|
||||
}
|
||||
|
||||
void daOnsTaru_c::mode_proc_drop() {
|
||||
|
||||
@@ -18,6 +18,11 @@
|
||||
#include "JSystem/J2DGraph/J2DTextBox.h"
|
||||
#include "m_Do/m_Do_graphic.h"
|
||||
|
||||
#ifdef TARGET_PC
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/settings.h"
|
||||
#endif
|
||||
|
||||
class daTit_HIO_c : public JORReflexible {
|
||||
public:
|
||||
daTit_HIO_c();
|
||||
@@ -343,6 +348,12 @@ void daTitle_c::fastLogoDispInit() {
|
||||
field_0x604 = 0;
|
||||
mWaitTimer = 30;
|
||||
mProcID = 5;
|
||||
|
||||
#ifdef TARGET_PC
|
||||
if (dusk::getSettings().game.enableFrameInterpolation) {
|
||||
dusk::frame_interp::request_presentation_sync();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void daTitle_c::fastLogoDisp() {
|
||||
|
||||
+37
-1
@@ -20,7 +20,6 @@
|
||||
#include "m_Do/m_Do_controller_pad.h"
|
||||
#include "m_Do/m_Do_graphic.h"
|
||||
#include "m_Do/m_Do_lib.h"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
@@ -29,6 +28,11 @@
|
||||
#include "d/d_debug_camera.h"
|
||||
#endif
|
||||
|
||||
#if TARGET_PC
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/logging.h"
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
static f32 limitf(f32 value, f32 min, f32 max) {
|
||||
@@ -2048,6 +2052,18 @@ s32 dCamera_c::nextType(s32 i_curType) {
|
||||
bool dCamera_c::onTypeChange(s32 i_curType, s32 i_nextType) {
|
||||
daAlink_c* unusedPlayer = daAlink_getAlinkActorClass();
|
||||
|
||||
#if TARGET_PC
|
||||
const s32 event_type_id = specialType[CAM_TYPE_EVENT];
|
||||
DuskLog.debug(
|
||||
"frameInterp: onTypeChange {} -> {} (event_type_id={}, leaving_event={}, entering_event={})",
|
||||
static_cast<int>(i_curType),
|
||||
static_cast<int>(i_nextType),
|
||||
static_cast<int>(event_type_id),
|
||||
i_curType == event_type_id,
|
||||
i_nextType == event_type_id
|
||||
);
|
||||
#endif
|
||||
|
||||
if (i_curType == specialType[CAM_TYPE_EVENT]) {
|
||||
if (mCamSetup.CheckFlag(0x4000)) {
|
||||
mGear = 0;
|
||||
@@ -10161,6 +10177,26 @@ bool dCamera_c::eventCamera(s32 param_0) {
|
||||
ActionNames[var_r29]);
|
||||
#endif
|
||||
|
||||
#if TARGET_PC
|
||||
if (dusk::getSettings().game.enableFrameInterpolation) {
|
||||
switch (var_r29) {
|
||||
case 3:
|
||||
case 4:
|
||||
case 5:
|
||||
case 12:
|
||||
dusk::frame_interp::request_presentation_sync();
|
||||
break;
|
||||
default:
|
||||
DuskLog.debug(
|
||||
"frameInterp: presentation sync not requested for ZEV event [{}] (staff idx {})",
|
||||
static_cast<const char*>(ActionNames[var_r29]),
|
||||
static_cast<int>(mEventData.mStaffIdx)
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (getEvFloatData(&sp28, "KeepDist") != 0 && mViewCache.mDirection.R() < sp28)
|
||||
{
|
||||
mViewCache.mDirection.R(sp28);
|
||||
|
||||
+24
-32
@@ -1440,16 +1440,17 @@ void dDlst_shadowSimple_c::set(cXyz* param_0, f32 param_1, f32 param_2, cXyz* pa
|
||||
|
||||
void dDlst_shadowControl_c::init() {
|
||||
#if TARGET_PC
|
||||
u16 resMult = dusk::getSettings().game.shadowResolutionMultiplier;
|
||||
mTexResScale = dusk::getSettings().game.shadowResolutionMultiplier;
|
||||
// Increase shadow map resolution
|
||||
u16 l_realImageSize[2] =
|
||||
{
|
||||
static_cast<u16>(192 * resMult),
|
||||
static_cast<u16>(64 * resMult)
|
||||
static_cast<u16>(192 * mTexResScale),
|
||||
static_cast<u16>(64 * mTexResScale)
|
||||
};
|
||||
#else
|
||||
static u16 l_realImageSize[2] = {192, 64};
|
||||
#endif
|
||||
|
||||
for (int i = 0; i < 2; i++) {
|
||||
u16 size = l_realImageSize[i];
|
||||
|
||||
@@ -1458,10 +1459,13 @@ void dDlst_shadowControl_c::init() {
|
||||
#else
|
||||
u32 buffer_size = GXGetTexBufferSize(size, size, 5, GX_DISABLE, 0);
|
||||
#endif
|
||||
field_0x15ef0[i] = JKR_NEW_ARRAY_ARGS(u8, buffer_size, 0x20);
|
||||
GXInitTexObj(&field_0x15eb0[i], field_0x15ef0[i], size, size, GX_TF_RGB5A3, GX_CLAMP,
|
||||
JKR_DELETE_ARRAY(mShadowTexData[i]);
|
||||
mShadowTexData[i] = JKR_NEW_ARRAY_ARGS(u8, buffer_size, 0x20);
|
||||
|
||||
mShadowTexObj[i].reset();
|
||||
GXInitTexObj(&mShadowTexObj[i], mShadowTexData[i], size, size, GX_TF_RGB5A3, GX_CLAMP,
|
||||
GX_CLAMP, GX_DISABLE);
|
||||
GXInitTexObjLOD(&field_0x15eb0[i], GX_LINEAR, GX_LINEAR, 0.0f, 0.0f, 0.0f, GX_FALSE,
|
||||
GXInitTexObjLOD(&mShadowTexObj[i], GX_LINEAR, GX_LINEAR, 0.0f, 0.0f, 0.0f, GX_FALSE,
|
||||
GX_FALSE, GX_ANISO_1);
|
||||
}
|
||||
}
|
||||
@@ -1479,25 +1483,13 @@ void dDlst_shadowControl_c::reset() {
|
||||
mRealNum = 0;
|
||||
field_0x4 = NULL;
|
||||
|
||||
#ifdef TARGET_PC
|
||||
field_0x15eb0[0].reset();
|
||||
field_0x15eb0[1].reset();
|
||||
#if TARGET_PC
|
||||
if (mTexResScale != dusk::getSettings().game.shadowResolutionMultiplier)
|
||||
init();
|
||||
#endif
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
int lastShadowValue = 0;
|
||||
#endif
|
||||
|
||||
void dDlst_shadowControl_c::imageDraw(Mtx param_0) {
|
||||
#if TARGET_PC
|
||||
if (lastShadowValue != dusk::getSettings().game.shadowResolutionMultiplier) {
|
||||
reset();
|
||||
init();
|
||||
lastShadowValue = dusk::getSettings().game.shadowResolutionMultiplier;
|
||||
}
|
||||
#endif
|
||||
|
||||
static u8 l_matDL[] ATTRIBUTE_ALIGN(32) = {
|
||||
0x10, 0x00, 0x00, 0x10, 0x0E, 0x00, 0x00, 0x04, 0x00, 0x10, 0x00, 0x00, 0x10, 0x10,
|
||||
0x00, 0x00, 0x04, 0x00, 0x61, 0x28, 0x38, 0x00, 0x00, 0x61, 0xC0, 0x08, 0xFF, 0xF2,
|
||||
@@ -1530,7 +1522,7 @@ void dDlst_shadowControl_c::imageDraw(Mtx param_0) {
|
||||
j3dSys.setDrawModeOpaTexEdge();
|
||||
J3DShape::resetVcdVatCache();
|
||||
dDlst_shadowReal_c* shadowReal = field_0x4;
|
||||
int r29 = 0;
|
||||
int chan = 0;
|
||||
int tex = 0;
|
||||
u16 r27;
|
||||
u16 r26;
|
||||
@@ -1539,8 +1531,8 @@ void dDlst_shadowControl_c::imageDraw(Mtx param_0) {
|
||||
#endif
|
||||
for (; shadowReal; shadowReal = shadowReal->getZsortNext()) {
|
||||
if (shadowReal->isUse()) {
|
||||
if (r29 == 0) {
|
||||
r27 = GXGetTexObjWidth(field_0x15eb0 + tex);
|
||||
if (chan == 0) {
|
||||
r27 = GXGetTexObjWidth(&mShadowTexObj[tex]);
|
||||
r26 = r27 * 2;
|
||||
#ifdef TARGET_PC
|
||||
GXCreateFrameBuffer(r26, r26);
|
||||
@@ -1549,27 +1541,27 @@ void dDlst_shadowControl_c::imageDraw(Mtx param_0) {
|
||||
GXSetViewport(0.0f, 0.0f, r26, r26, 0.0f, 1.0f);
|
||||
GXSetScissor(0, 0, r26, r26);
|
||||
}
|
||||
GXSetTevColor(GX_TEVREG0, l_imageDrawColor[r29]);
|
||||
if (r29 == 3) {
|
||||
GXSetTevColor(GX_TEVREG0, l_imageDrawColor[chan]);
|
||||
if (chan == 3) {
|
||||
GXSetColorUpdate(GX_DISABLE);
|
||||
GXSetAlphaUpdate(GX_ENABLE);
|
||||
}
|
||||
shadowReal->imageDraw(param_0);
|
||||
r29 = (r29 + 1) % 4;
|
||||
if (r29 == 0) {
|
||||
chan = (chan + 1) % 4;
|
||||
if (chan == 0) {
|
||||
GXSetTexCopySrc(0, 0, r26, r26);
|
||||
GXSetTexCopyDst(r27, r27, GX_TF_RGB5A3, GX_TRUE);
|
||||
GXSetColorUpdate(GX_ENABLE);
|
||||
GXCopyTex(field_0x15ef0[tex++], GX_TRUE);
|
||||
GXCopyTex(mShadowTexData[tex++], GX_TRUE);
|
||||
GXPixModeSync();
|
||||
GXSetAlphaUpdate(GX_DISABLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (r29) {
|
||||
if (chan) {
|
||||
GXSetTexCopySrc(0, 0, r26, r26);
|
||||
GXSetTexCopyDst(r27, r27, GX_TF_RGB5A3, GX_TRUE);
|
||||
GXCopyTex(field_0x15ef0[tex], GX_TRUE);
|
||||
GXCopyTex(mShadowTexData[tex], GX_TRUE);
|
||||
GXPixModeSync();
|
||||
GXSetAlphaUpdate(GX_DISABLE);
|
||||
}
|
||||
@@ -1621,7 +1613,7 @@ void dDlst_shadowControl_c::draw(Mtx param_0) {
|
||||
for (int i2 = 0, i3 = 0; real != NULL; real = real->getZsortNext()) {
|
||||
if (real->isUse()) {
|
||||
if (i2 == 0) {
|
||||
TGXTexObj* obj = &field_0x15eb0[i3];
|
||||
TGXTexObj* obj = &mShadowTexObj[i3];
|
||||
i3++;
|
||||
|
||||
GXLoadTexObj(obj, GX_TEXMAP0);
|
||||
|
||||
@@ -1540,8 +1540,21 @@ void dScnKy_env_light_c::setDaytime() {
|
||||
}
|
||||
|
||||
if (dComIfGp_roomControl_getTimePass() && !field_0x130a && temp_r29) {
|
||||
#if TARGET_PC
|
||||
f32 prev = daytime;
|
||||
#endif
|
||||
|
||||
daytime += time_change_rate;
|
||||
|
||||
#if TARGET_PC
|
||||
if (time_change_rate == 1.0f &&
|
||||
(std::fmod(daytime - 90.0f + 360.0f, 360.0f) < std::fmod(prev - 90.0f + 360.0f, 360.0f) ||
|
||||
std::fmod(daytime - 285.0f + 360.0f, 360.0f) < std::fmod(prev - 285.0f + 360.0f, 360.0f)))
|
||||
{
|
||||
g_env_light.time_change_rate = 0.012f;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Stage is Fishing Pond or Hena's Hut
|
||||
if (!strcmp(dComIfGp_getStartStageName(), "F_SP127") ||
|
||||
!strcmp(dComIfGp_getStartStageName(), "R_SP127"))
|
||||
|
||||
+15
-2
@@ -48,13 +48,20 @@ public:
|
||||
GXPixModeSync();
|
||||
#if TARGET_PC
|
||||
// init mTexObj at capture time so the gpu ref survives window resizes
|
||||
GXInitTexObj(&mTexObj, mDoGph_gInf_c::getFrameBufferTex(), mDoGph_gInf_c::getWidth(), mDoGph_gInf_c::getHeight(),
|
||||
mCaptureWidth = mDoGph_gInf_c::getWidth();
|
||||
mCaptureHeight = mDoGph_gInf_c::getHeight();
|
||||
GXInitTexObj(&mTexObj, mDoGph_gInf_c::getFrameBufferTex(), mCaptureWidth, mCaptureHeight,
|
||||
(GXTexFmt)mDoGph_gInf_c::getFrameBufferTimg()->format, GX_CLAMP, GX_CLAMP, GX_FALSE);
|
||||
GXInitTexObjLOD(&mTexObj, GX_LINEAR, GX_LINEAR, 0.0f, 0.0f, 0.0f, GX_FALSE, GX_FALSE, GX_ANISO_1);
|
||||
#endif
|
||||
} else {
|
||||
#if TARGET_PC
|
||||
// reuse the persistent mTexObj
|
||||
// If the window was resized since capture, force a re-capture at the new size
|
||||
if (mCaptureWidth != (u16)mDoGph_gInf_c::getWidth() ||
|
||||
mCaptureHeight != (u16)mDoGph_gInf_c::getHeight()) {
|
||||
mFlag = 1;
|
||||
return;
|
||||
}
|
||||
GXLoadTexObj(&mTexObj, GX_TEXMAP0);
|
||||
#else
|
||||
TGXTexObj tex;
|
||||
@@ -112,6 +119,10 @@ public:
|
||||
mFlag = 0;
|
||||
mAlpha = 255;
|
||||
mTopFlag = 0;
|
||||
#if TARGET_PC
|
||||
mCaptureWidth = 0;
|
||||
mCaptureHeight = 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
void setCaptureFlag() { mFlag = 1; }
|
||||
@@ -126,6 +137,8 @@ private:
|
||||
/* 0x5 */ u8 mAlpha;
|
||||
/* 0x6 */ u8 mTopFlag;
|
||||
#if TARGET_PC
|
||||
u16 mCaptureWidth;
|
||||
u16 mCaptureHeight;
|
||||
TGXTexObj mTexObj;
|
||||
#endif
|
||||
};
|
||||
|
||||
+40
-1
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "aurora/lib/logging.hpp"
|
||||
#include "dusk/io.hpp"
|
||||
#include "dusk/settings.h"
|
||||
|
||||
#include <limits>
|
||||
#include <string>
|
||||
@@ -37,9 +38,24 @@ const ConfigImplBase* ConfigVarBase::getImpl() const noexcept {
|
||||
return impl;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
static T sanitizeEnumValue(const ConfigVar<T>& cVar, T value) {
|
||||
if constexpr (std::is_enum_v<T>) {
|
||||
using Underlying = std::underlying_type_t<T>;
|
||||
const Underlying raw = static_cast<Underlying>(value);
|
||||
const Underlying min = static_cast<Underlying>(ConfigEnumRange<T>::min);
|
||||
const Underlying max = static_cast<Underlying>(ConfigEnumRange<T>::max);
|
||||
if (raw < min || raw > max) {
|
||||
return cVar.getDefaultValue();
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
template<ConfigValue T>
|
||||
void ConfigImpl<T>::loadFromJson(ConfigVar<T>& cVar, const json& jsonValue) {
|
||||
cVar.setValue(jsonValue.get<T>(), false);
|
||||
cVar.setValue(sanitizeEnumValue(cVar, jsonValue.get<T>()), false);
|
||||
}
|
||||
|
||||
template<ConfigValue T>
|
||||
@@ -85,6 +101,28 @@ static void loadFromArgImpl(ConfigVar<std::string>& cVar, const std::string_view
|
||||
cVar.setOverrideValue(std::string(stringValue));
|
||||
}
|
||||
|
||||
template<ConfigValue T> requires std::is_enum_v<T>
|
||||
static void loadFromArgImpl(ConfigVar<T>& cVar, const std::string_view stringValue) {
|
||||
using Underlying = std::underlying_type_t<T>;
|
||||
const std::string str(stringValue);
|
||||
|
||||
if constexpr (std::is_signed_v<Underlying>) {
|
||||
const auto result = std::stoll(str);
|
||||
if (result >= std::numeric_limits<Underlying>::min() && result <= std::numeric_limits<Underlying>::max()) {
|
||||
cVar.setOverrideValue(sanitizeEnumValue(cVar, static_cast<T>(result)));
|
||||
} else {
|
||||
throw std::out_of_range("Value is too large");
|
||||
}
|
||||
} else {
|
||||
const auto result = std::stoull(str);
|
||||
if (result <= std::numeric_limits<Underlying>::max()) {
|
||||
cVar.setOverrideValue(sanitizeEnumValue(cVar, static_cast<T>(result)));
|
||||
} else {
|
||||
throw std::out_of_range("Value is too large");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template<ConfigValue T>
|
||||
void ConfigImpl<T>::loadFromArg(ConfigVar<T>& cVar, const std::string_view stringValue) {
|
||||
loadFromArgImpl(cVar, stringValue);
|
||||
@@ -115,6 +153,7 @@ namespace dusk::config {
|
||||
template class ConfigImpl<f32>;
|
||||
template class ConfigImpl<f64>;
|
||||
template class ConfigImpl<std::string>;
|
||||
template class ConfigImpl<dusk::BloomMode>;
|
||||
}
|
||||
|
||||
void dusk::config::Register(ConfigVarBase& configVar) {
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
#include "dusk/crash_reporting.h"
|
||||
|
||||
#include "dusk/app_info.hpp"
|
||||
#include "dusk/dusk.h"
|
||||
#include "dusk/logging.h"
|
||||
#include "dusk/settings.h"
|
||||
#include "version.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
|
||||
#include "SDL3/SDL_filesystem.h"
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
#include <sentry.h>
|
||||
#endif
|
||||
|
||||
namespace dusk {
|
||||
|
||||
namespace {
|
||||
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
bool g_sentryInitialized = false;
|
||||
|
||||
bool IsTruthy(std::string_view value) {
|
||||
return value == "1" || value == "true" || value == "TRUE" || value == "yes"
|
||||
|| value == "YES" || value == "on" || value == "ON";
|
||||
}
|
||||
|
||||
std::string GetEnvOrEmpty(const char* name) {
|
||||
if (const char* value = std::getenv(name)) {
|
||||
return value;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool GetEffectiveEnabled() {
|
||||
const std::string env = GetEnvOrEmpty("DUSK_SENTRY_ENABLED");
|
||||
if (!env.empty()) {
|
||||
return IsTruthy(env);
|
||||
}
|
||||
return getSettings().backend.enableCrashReporting;
|
||||
}
|
||||
|
||||
std::string GetEffectiveDsn() {
|
||||
const std::string env = GetEnvOrEmpty("DUSK_SENTRY_DSN");
|
||||
if (!env.empty()) {
|
||||
return env;
|
||||
}
|
||||
return DUSK_SENTRY_DSN;
|
||||
}
|
||||
|
||||
bool GetEffectiveDebug() {
|
||||
const std::string env = GetEnvOrEmpty("DUSK_SENTRY_DEBUG");
|
||||
if (!env.empty()) {
|
||||
return IsTruthy(env);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string GetReleaseName() {
|
||||
return std::string(AppName) + "@" DUSK_WC_DESCRIBE;
|
||||
}
|
||||
|
||||
std::filesystem::path GetSentryDatabasePath() {
|
||||
return std::filesystem::path(configPath) / "sentry";
|
||||
}
|
||||
|
||||
std::filesystem::path GetLogAttachmentPath() {
|
||||
if (const char* logPath = GetLogFilePath()) {
|
||||
return logPath;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::filesystem::path GetCrashpadHandlerPath() {
|
||||
const char* basePath = SDL_GetBasePath();
|
||||
if (!basePath) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::filesystem::path handlerDir(basePath);
|
||||
#if _WIN32
|
||||
return handlerDir / "crashpad_handler.exe";
|
||||
#else
|
||||
return handlerDir / "crashpad_handler";
|
||||
#endif
|
||||
}
|
||||
|
||||
void ConfigurePathOptions(sentry_options_t* options) {
|
||||
const auto databasePath = GetSentryDatabasePath();
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(databasePath, ec);
|
||||
if (ec) {
|
||||
DuskLog.warn("Unable to create Sentry database path '{}': {}",
|
||||
databasePath.string(), ec.message());
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
const std::wstring databasePathWide = databasePath.wstring();
|
||||
sentry_options_set_database_pathw(options, databasePathWide.c_str());
|
||||
|
||||
const auto handlerPath = GetCrashpadHandlerPath();
|
||||
if (!handlerPath.empty()) {
|
||||
const std::wstring handlerPathWide = handlerPath.wstring();
|
||||
sentry_options_set_handler_pathw(options, handlerPathWide.c_str());
|
||||
}
|
||||
#else
|
||||
const std::string databasePathUtf8 = databasePath.string();
|
||||
sentry_options_set_database_path(options, databasePathUtf8.c_str());
|
||||
|
||||
const auto handlerPath = GetCrashpadHandlerPath();
|
||||
if (!handlerPath.empty()) {
|
||||
const std::string handlerPathUtf8 = handlerPath.string();
|
||||
sentry_options_set_handler_path(options, handlerPathUtf8.c_str());
|
||||
}
|
||||
#endif
|
||||
|
||||
const auto logPath = GetLogAttachmentPath();
|
||||
if (!logPath.empty()) {
|
||||
#if _WIN32
|
||||
sentry_options_add_attachmentw(options, logPath.wstring().c_str());
|
||||
#else
|
||||
sentry_options_add_attachment(options, logPath.string().c_str());
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void InitializeCrashReporting() {
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
if (g_sentryInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!GetEffectiveEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string dsn = GetEffectiveDsn();
|
||||
if (dsn.empty()) {
|
||||
DuskLog.warn("Crash reporting is enabled but no Sentry DSN is configured");
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string release = GetReleaseName();
|
||||
|
||||
sentry_options_t* options = sentry_options_new();
|
||||
sentry_options_set_dsn(options, dsn.c_str());
|
||||
sentry_options_set_release(options, release.c_str());
|
||||
sentry_options_set_environment(options, DUSK_SENTRY_ENVIRONMENT);
|
||||
sentry_options_set_debug(options, GetEffectiveDebug() ? 1 : 0);
|
||||
sentry_options_set_cache_keep(options, 1);
|
||||
sentry_options_set_max_breadcrumbs(options, 100);
|
||||
ConfigurePathOptions(options);
|
||||
|
||||
if (sentry_init(options) != 0) {
|
||||
DuskLog.warn("Failed to initialize Sentry crash reporting");
|
||||
return;
|
||||
}
|
||||
|
||||
sentry_set_tag("git_branch", DUSK_WC_BRANCH);
|
||||
sentry_set_tag("build_type", DUSK_BUILD_TYPE);
|
||||
sentry_set_tag("tp_version", DUSK_TP_VERSION);
|
||||
g_sentryInitialized = true;
|
||||
|
||||
DuskLog.info("Initialized Sentry crash reporting");
|
||||
#endif
|
||||
}
|
||||
|
||||
void ShutdownCrashReporting() {
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
if (!g_sentryInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
sentry_close();
|
||||
g_sentryInitialized = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace dusk
|
||||
+1
-1
@@ -54,7 +54,7 @@ void *_memcpy(void* dest, void const* src, int n) {
|
||||
}
|
||||
|
||||
void DCZeroRange(void* addr, uint32_t nBytes) {
|
||||
#ifdef _MSC_VER
|
||||
#if defined(_MSC_VER) || TARGET_ANDROID
|
||||
memset(addr, 0, nBytes);
|
||||
#else
|
||||
bzero(addr, nBytes);
|
||||
|
||||
@@ -63,6 +63,9 @@ bool s_initialized = false;
|
||||
bool g_enabled = false;
|
||||
bool g_recording = false;
|
||||
bool g_interpolating = false;
|
||||
bool g_sync_presentation = false;
|
||||
uint32_t g_presentation_counter = 0;
|
||||
|
||||
float g_step = 0.0f;
|
||||
uint32_t g_pending_presentation_ui_ticks = 0;
|
||||
uint32_t g_current_presentation_ui_ticks = 0;
|
||||
@@ -235,7 +238,7 @@ void interpolate_branch(const Path& old_path, const Path& new_path, float step)
|
||||
}
|
||||
|
||||
const Mtx* resolve_replacement(const Mtx* source, Mtx* scratch) {
|
||||
if (!g_interpolating || source == nullptr) {
|
||||
if (!g_interpolating || source == nullptr || dusk::frame_interp::presentation_sync_active()) {
|
||||
return source;
|
||||
}
|
||||
|
||||
@@ -268,6 +271,7 @@ void begin_record() {
|
||||
ensure_initialized();
|
||||
if (!g_enabled) {
|
||||
g_interpolating = false;
|
||||
g_sync_presentation = false;
|
||||
g_previous_recording = {};
|
||||
g_current_recording = {};
|
||||
g_current_path.clear();
|
||||
@@ -275,6 +279,7 @@ void begin_record() {
|
||||
return;
|
||||
}
|
||||
|
||||
g_sync_presentation = false;
|
||||
g_previous_recording = std::move(g_current_recording);
|
||||
g_current_recording = {};
|
||||
g_current_path.clear();
|
||||
@@ -292,21 +297,37 @@ void interpolate(float step) {
|
||||
ensure_initialized();
|
||||
clear_replacements();
|
||||
g_step = std::clamp(step, 0.0f, 1.0f);
|
||||
g_interpolating = g_enabled && !g_recording && has_recording_data(g_current_recording);
|
||||
g_interpolating = g_enabled && !g_recording && !g_sync_presentation && has_recording_data(g_current_recording);
|
||||
if (!g_interpolating) {
|
||||
return;
|
||||
}
|
||||
const Path& old_root = has_recording_data(g_previous_recording) ? g_previous_recording.root : g_current_recording.root;
|
||||
interpolate_branch(old_root, g_current_recording.root, g_step);
|
||||
}
|
||||
|
||||
if (!has_recording_data(g_previous_recording)) {
|
||||
interpolate_branch(g_current_recording.root, g_current_recording.root, g_step);
|
||||
void notify_presentation_frame() {
|
||||
ensure_initialized();
|
||||
++g_presentation_counter;
|
||||
}
|
||||
|
||||
void request_presentation_sync() {
|
||||
ensure_initialized();
|
||||
if (!g_enabled) {
|
||||
return;
|
||||
}
|
||||
g_sync_presentation = true;
|
||||
}
|
||||
|
||||
interpolate_branch(g_previous_recording.root, g_current_recording.root, g_step);
|
||||
bool presentation_sync_active() {
|
||||
if (!s_initialized || !g_enabled) {
|
||||
return false;
|
||||
}
|
||||
return g_sync_presentation;
|
||||
}
|
||||
|
||||
float get_interpolation_step() {
|
||||
return g_step;
|
||||
ensure_initialized();
|
||||
return presentation_sync_active() ? 1.0f : g_step;
|
||||
}
|
||||
|
||||
void notify_sim_tick_complete() {
|
||||
@@ -371,7 +392,7 @@ void record_final_mtx_raw(const Mtx* dest, const Mtx src) {
|
||||
}
|
||||
|
||||
bool lookup_replacement(const void* source, Mtx out) {
|
||||
if (!s_initialized || !g_interpolating || source == nullptr) {
|
||||
if (presentation_sync_active() || !g_interpolating || source == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -385,7 +406,7 @@ bool lookup_replacement(const void* source, Mtx out) {
|
||||
}
|
||||
|
||||
bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out) {
|
||||
if (!s_initialized || !g_interpolating || lhs == nullptr || rhs == nullptr) {
|
||||
if (presentation_sync_active() || !g_interpolating || lhs == nullptr || rhs == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -393,9 +414,7 @@ bool lookup_concat_replacement(const void* lhs, const void* rhs, Mtx out) {
|
||||
Mtx rhs_scratch;
|
||||
const Mtx* resolved_lhs = resolve_replacement(reinterpret_cast<const Mtx*>(lhs), &lhs_scratch);
|
||||
const Mtx* resolved_rhs = resolve_replacement(reinterpret_cast<const Mtx*>(rhs), &rhs_scratch);
|
||||
if (resolved_lhs == reinterpret_cast<const Mtx*>(lhs) &&
|
||||
resolved_rhs == reinterpret_cast<const Mtx*>(rhs))
|
||||
{
|
||||
if (resolved_lhs == reinterpret_cast<const Mtx*>(lhs) && resolved_rhs == reinterpret_cast<const Mtx*>(rhs)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ static void ShowAllJAISeqs() {
|
||||
}
|
||||
|
||||
void dusk::ImGuiMenuTools::ShowAudioDebug() {
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F7, m_showAudioDebug)) {
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F10, m_showAudioDebug)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -255,7 +255,8 @@ void dusk::ImGuiMenuTools::ShowAudioDebug() {
|
||||
}
|
||||
|
||||
void dusk::ImGuiMenuTools::ShowSaveEditor() {
|
||||
if (m_showSaveEditor) {
|
||||
m_saveEditor.draw(m_showSaveEditor);
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F6, m_showSaveEditor)) {
|
||||
return;
|
||||
}
|
||||
m_saveEditor.draw(m_showSaveEditor);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
namespace dusk {
|
||||
void ImGuiMenuTools::ShowCameraOverlay() {
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F6, m_showCameraOverlay)) {
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F9, m_showCameraOverlay)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -269,6 +269,7 @@ namespace dusk {
|
||||
m_menuTools.ShowAudioDebug();
|
||||
m_menuTools.ShowSaveEditor();
|
||||
}
|
||||
m_menuTools.ShowStateShare();
|
||||
DuskDebugPad(); // temporary, remove later
|
||||
|
||||
// Only show cursor when menu or any windows are open
|
||||
|
||||
@@ -47,4 +47,15 @@ void DuskDebugPad() {
|
||||
pad.mMainStickValue = 1.0f;
|
||||
pad.mMainStickAngle = -0x4000;
|
||||
}
|
||||
|
||||
if (ImGui::IsKeyDown(ImGuiKey_Q)) {
|
||||
pad.mTriggerLeft = 1.0;
|
||||
pad.mTrigLockL = 1;
|
||||
pad.mHoldLockL = 1;
|
||||
}
|
||||
if (ImGui::IsKeyDown(ImGuiKey_E)) {
|
||||
pad.mTriggerRight = 1.0;
|
||||
pad.mTrigLockR = 1;
|
||||
pad.mHoldLockR = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,22 @@ bool AssetExists(const std::string& path) {
|
||||
SDL_PathInfo pathInfo{};
|
||||
return SDL_GetPathInfo(path.c_str(), &pathInfo) && pathInfo.type == SDL_PATHTYPE_FILE;
|
||||
}
|
||||
|
||||
ImTextureID AddTexture(const char* assetName) {
|
||||
auto image = GetImage(GetAssetPath(assetName));
|
||||
if (image.data == nullptr || image.width == 0 || image.height == 0) {
|
||||
return 0;
|
||||
}
|
||||
return aurora_imgui_add_texture(image.width, image.height, image.data.get());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
ImFont* ImGuiEngine::fontNormal;
|
||||
ImFont* ImGuiEngine::fontLarge;
|
||||
ImFont* ImGuiEngine::fontExtraLarge;
|
||||
ImFont* ImGuiEngine::fontMono;
|
||||
ImTextureID ImGuiEngine::duskIcon = 0;
|
||||
ImTextureID ImGuiEngine::orgIcon = 0;
|
||||
ImTextureID ImGuiEngine::duskLogo = 0;
|
||||
|
||||
inline ImFont* CreateFont(float size, const std::string& fontPath, std::string_view fontName) {
|
||||
bool fontFileExists = !fontPath.empty() && AssetExists(fontPath);
|
||||
@@ -149,6 +158,7 @@ void ImGuiEngine_Initialize(float scale) {
|
||||
|
||||
Image GetImage(const std::string& path) {
|
||||
if (!AssetExists(path)) {
|
||||
DuskLog.warn("Image '{}' does not exist", path);
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -187,13 +197,11 @@ Image GetImage(const std::string& path) {
|
||||
}
|
||||
|
||||
void ImGuiEngine_AddTextures() {
|
||||
if (ImGuiEngine::duskIcon == 0) {
|
||||
auto icon = GetImage(GetAssetPath("icon.png"));
|
||||
if (icon.data == nullptr || icon.width == 0 || icon.height == 0) {
|
||||
ImGuiEngine::duskIcon = 0;
|
||||
return;
|
||||
}
|
||||
ImGuiEngine::duskIcon = aurora_imgui_add_texture(icon.width, icon.height, icon.data.get());
|
||||
if (ImGuiEngine::orgIcon == 0) {
|
||||
ImGuiEngine::orgIcon = AddTexture("org-icon.png");
|
||||
}
|
||||
if (ImGuiEngine::duskLogo == 0) {
|
||||
ImGuiEngine::duskLogo = AddTexture("logo.png");
|
||||
}
|
||||
}
|
||||
} // namespace dusk
|
||||
|
||||
@@ -11,7 +11,8 @@ public:
|
||||
static ImFont* fontLarge;
|
||||
static ImFont* fontExtraLarge;
|
||||
static ImFont* fontMono;
|
||||
static ImTextureID duskIcon;
|
||||
static ImTextureID orgIcon;
|
||||
static ImTextureID duskLogo;
|
||||
};
|
||||
|
||||
void ImGuiEngine_Initialize(float scale);
|
||||
@@ -23,5 +24,5 @@ struct Image {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
};
|
||||
Image GetImage(std::string_view path);
|
||||
Image GetImage(const std::string& path);
|
||||
} // namespace dusk
|
||||
|
||||
@@ -12,11 +12,13 @@ namespace dusk {
|
||||
static void ApplyPresetClassic() {
|
||||
auto& s = getSettings();
|
||||
s.video.lockAspectRatio.setValue(true);
|
||||
s.game.bloomMode.setValue(BloomMode::Classic);
|
||||
VILockAspectRatio(defaultAspectRatioW, defaultAspectRatioH);
|
||||
}
|
||||
|
||||
static void ApplyPresetHD() {
|
||||
auto& s = getSettings();
|
||||
s.game.bloomMode.setValue(BloomMode::Classic);
|
||||
s.game.hideTvSettingsScreen.setValue(true);
|
||||
s.game.skipWarningScreen.setValue(true);
|
||||
s.game.noReturnRupees.setValue(true);
|
||||
@@ -37,6 +39,8 @@ static void ApplyPresetDusk() {
|
||||
s.game.instantSaves.setValue(true);
|
||||
s.game.midnasLamentNonStop.setValue(true);
|
||||
s.game.enableFrameInterpolation.setValue(true);
|
||||
s.game.sunsSong.setValue(true);
|
||||
s.game.bloomMode.setValue(BloomMode::Dusk);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace dusk {
|
||||
void ImGuiMenuTools::ShowMapLoader() {
|
||||
if (!m_showMapLoader) {
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F7, m_showMapLoader)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,15 @@ namespace dusk {
|
||||
if (ImGui::BeginMenu("Enhancements")) {
|
||||
if (ImGui::BeginMenu("Quality of Life")) {
|
||||
config::ImGuiCheckbox("Quick Transform (R+Y)", getSettings().game.enableQuickTransform);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Allows you to quickly transform between forms\n"
|
||||
"without having to talk to Midna.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Sun's Song (R+X)", getSettings().game.sunsSong);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Allows Wolf Link to howl and change the time of day.");
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Bigger Wallets", getSettings().game.biggerWallets);
|
||||
if (ImGui::IsItemHovered()) {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "m_Do/m_Do_graphic.h"
|
||||
|
||||
#include <aurora/gfx.h>
|
||||
#include <SDL3/SDL_gamepad.h>
|
||||
|
||||
#include "dusk/main.h"
|
||||
|
||||
@@ -62,7 +63,30 @@ namespace dusk {
|
||||
config::Save();
|
||||
}
|
||||
|
||||
config::ImGuiCheckbox("Native Bloom", getSettings().game.enableBloom);
|
||||
constexpr const char* bloomModeNames[] = {"Off", "Classic", "Dusk"};
|
||||
int bloomMode = static_cast<int>(getSettings().game.bloomMode.getValue());
|
||||
if (ImGui::BeginCombo("Bloom", bloomModeNames[bloomMode])) {
|
||||
for (int i = 0; i < IM_ARRAYSIZE(bloomModeNames); i++) {
|
||||
const bool selected = bloomMode == i;
|
||||
if (ImGui::Selectable(bloomModeNames[i], selected)) {
|
||||
getSettings().game.bloomMode.setValue(static_cast<BloomMode>(i));
|
||||
config::Save();
|
||||
}
|
||||
if (selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
|
||||
bool bloomOff = bloomMode == static_cast<int>(BloomMode::Off);
|
||||
if (bloomOff) ImGui::BeginDisabled();
|
||||
float mult = getSettings().game.bloomMultiplier.getValue();
|
||||
if (ImGui::SliderFloat("Bloom Brightness", &mult, 0.0f, 1.0f, "%.2f")) {
|
||||
getSettings().game.bloomMultiplier.setValue(mult);
|
||||
config::Save();
|
||||
}
|
||||
if (bloomOff) ImGui::EndDisabled();
|
||||
|
||||
config::ImGuiCheckbox("Enable Water Refraction", getSettings().game.enableWaterRefraction);
|
||||
|
||||
@@ -110,6 +134,9 @@ namespace dusk {
|
||||
if (ImGui::BeginMenu("Interface")) {
|
||||
config::ImGuiCheckbox("Skip Pre-Launch UI", getSettings().backend.skipPreLaunchUI);
|
||||
config::ImGuiCheckbox("Show Pipeline Compilation", getSettings().backend.showPipelineCompilation);
|
||||
#if DUSK_ENABLE_SENTRY_NATIVE
|
||||
config::ImGuiCheckbox("Enable Crash Reporting", getSettings().backend.enableCrashReporting);
|
||||
#endif
|
||||
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
@@ -148,6 +175,122 @@ namespace dusk {
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
struct SpecificButtonName {
|
||||
SDL_GamepadType Type;
|
||||
const char* Name;
|
||||
};
|
||||
|
||||
struct ButtonNames {
|
||||
SDL_GamepadButton Button;
|
||||
std::vector<SpecificButtonName> Names;
|
||||
};
|
||||
|
||||
// clang-format off
|
||||
static const std::vector<ButtonNames> GamepadButtonNames = {
|
||||
{ SDL_GAMEPAD_BUTTON_LEFT_STICK, {
|
||||
{SDL_GAMEPAD_TYPE_PS3, "L3"},
|
||||
{SDL_GAMEPAD_TYPE_PS4, "L3"},
|
||||
{SDL_GAMEPAD_TYPE_PS5, "L3"},
|
||||
{SDL_GAMEPAD_TYPE_XBOX360, "Left Stick"},
|
||||
{SDL_GAMEPAD_TYPE_XBOXONE, "Left Stick"},
|
||||
{SDL_GAMEPAD_TYPE_GAMECUBE, "Control Stick"},
|
||||
}},
|
||||
{ SDL_GAMEPAD_BUTTON_RIGHT_STICK, {
|
||||
{SDL_GAMEPAD_TYPE_PS3, "R3"},
|
||||
{SDL_GAMEPAD_TYPE_PS4, "R3"},
|
||||
{SDL_GAMEPAD_TYPE_PS5, "R3"},
|
||||
{SDL_GAMEPAD_TYPE_XBOX360, "Right Stick"},
|
||||
{SDL_GAMEPAD_TYPE_XBOXONE, "Right Stick"},
|
||||
{SDL_GAMEPAD_TYPE_GAMECUBE, "C Stick"},
|
||||
}},
|
||||
{ SDL_GAMEPAD_BUTTON_LEFT_SHOULDER, {
|
||||
{SDL_GAMEPAD_TYPE_PS3, "L1"},
|
||||
{SDL_GAMEPAD_TYPE_PS4, "L1"},
|
||||
{SDL_GAMEPAD_TYPE_PS5, "L1"},
|
||||
{SDL_GAMEPAD_TYPE_XBOX360, "LB"},
|
||||
{SDL_GAMEPAD_TYPE_XBOXONE, "LB"},
|
||||
}},
|
||||
{ SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER, {
|
||||
{SDL_GAMEPAD_TYPE_PS3, "R1"},
|
||||
{SDL_GAMEPAD_TYPE_PS4, "R1"},
|
||||
{SDL_GAMEPAD_TYPE_PS5, "R1"},
|
||||
{SDL_GAMEPAD_TYPE_XBOX360, "RB"},
|
||||
{SDL_GAMEPAD_TYPE_XBOXONE, "RB"},
|
||||
{SDL_GAMEPAD_TYPE_GAMECUBE, "Z"},
|
||||
}},
|
||||
{ SDL_GAMEPAD_BUTTON_BACK, {
|
||||
{SDL_GAMEPAD_TYPE_PS3, "Select"},
|
||||
{SDL_GAMEPAD_TYPE_PS4, "Share"},
|
||||
{SDL_GAMEPAD_TYPE_PS5, "Create"},
|
||||
{SDL_GAMEPAD_TYPE_XBOX360, "Back"},
|
||||
{SDL_GAMEPAD_TYPE_XBOXONE, "View"},
|
||||
}},
|
||||
{ SDL_GAMEPAD_BUTTON_START, {
|
||||
{SDL_GAMEPAD_TYPE_PS3, "Start"},
|
||||
{SDL_GAMEPAD_TYPE_PS4, "Options"},
|
||||
{SDL_GAMEPAD_TYPE_PS5, "Options"},
|
||||
{SDL_GAMEPAD_TYPE_XBOX360, "Start"},
|
||||
{SDL_GAMEPAD_TYPE_XBOXONE, "Menu"},
|
||||
{SDL_GAMEPAD_TYPE_GAMECUBE, "Start/Pause"},
|
||||
}},
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
static const char* GetNameForGamepadButton(SDL_Gamepad* gamepad, u32 buttonUntyped) {
|
||||
if (buttonUntyped == PAD_NATIVE_BUTTON_INVALID) {
|
||||
return "Not bound";
|
||||
}
|
||||
|
||||
auto button = static_cast<SDL_GamepadButton>(buttonUntyped);
|
||||
auto label = SDL_GetGamepadButtonLabel(gamepad, button);
|
||||
|
||||
switch (label) {
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_A:
|
||||
return "A";
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_B:
|
||||
return "B";
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_X:
|
||||
return "X";
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_Y:
|
||||
return "Y";
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_CROSS:
|
||||
return "Cross";
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_CIRCLE:
|
||||
return "Circle";
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_TRIANGLE:
|
||||
return "Triangle";
|
||||
case SDL_GAMEPAD_BUTTON_LABEL_SQUARE:
|
||||
return "Square";
|
||||
default:; // Fall through
|
||||
}
|
||||
|
||||
auto padType = SDL_GetGamepadType(gamepad);
|
||||
for (const auto& buttonNames : GamepadButtonNames) {
|
||||
if (buttonNames.Button != button) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const auto& name : buttonNames.Names) {
|
||||
if (name.Type == padType) {
|
||||
return name.Name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch (button) {
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_LEFT:
|
||||
return "D-pad left";
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT:
|
||||
return "D-pad right";
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_UP:
|
||||
return "D-pad up";
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_DOWN:
|
||||
return "D-pad down";
|
||||
default:
|
||||
return PADGetNativeButtonName(buttonUntyped);
|
||||
}
|
||||
}
|
||||
|
||||
void ImGuiMenuGame::windowControllerConfig() {
|
||||
if (!m_showControllerConfig) {
|
||||
return;
|
||||
@@ -277,6 +420,7 @@ namespace dusk {
|
||||
|
||||
ImGuiBeginGroupPanel("Buttons", ImVec2(150 * scale, 20 * scale));
|
||||
|
||||
SDL_Gamepad* gamepad = PADGetSDLGamepadForIndex(PADGetIndexForPort(m_controllerConfig.m_selectedPort));
|
||||
u32 buttonCount;
|
||||
PADButtonMapping* btnMappingList = PADGetButtonMappings(m_controllerConfig.m_selectedPort, &buttonCount);
|
||||
if (btnMappingList != nullptr) {
|
||||
@@ -296,7 +440,7 @@ namespace dusk {
|
||||
if (m_controllerConfig.m_isReading && m_controllerConfig.m_pendingButtonMapping == &btnMappingList[i]) {
|
||||
dispName = fmt::format("Press a Key...##{}", btnName);
|
||||
} else {
|
||||
const char* nativeName = PADGetNativeButtonName(btnMappingList[i].nativeButton);
|
||||
const char* nativeName = GetNameForGamepadButton(gamepad, btnMappingList[i].nativeButton);
|
||||
if (nativeName == nullptr) {
|
||||
nativeName = "[unbound]";
|
||||
}
|
||||
|
||||
@@ -48,12 +48,14 @@ namespace dusk {
|
||||
ImGui::MenuItem("Process Management", hotkeys::SHOW_PROCESS_MANAGEMENT, &m_showProcessManagement);
|
||||
ImGui::MenuItem("Debug Overlay", hotkeys::SHOW_DEBUG_OVERLAY, &m_showDebugOverlay);
|
||||
ImGui::MenuItem("Heap Viewer", hotkeys::SHOW_HEAP_VIEWER, &m_showHeapOverlay);
|
||||
ImGui::MenuItem("Stub Log", hotkeys::SHOW_STUB_LOG, &m_showStubLog);
|
||||
ImGui::MenuItem("Debug Camera", hotkeys::SHOW_CAMERA_DEBUG, &m_showCameraOverlay);
|
||||
ImGui::MenuItem("Map Loader", nullptr, &m_showMapLoader);
|
||||
ImGui::MenuItem("Player Info", nullptr, &m_showPlayerInfo);
|
||||
ImGui::MenuItem("Save Editor", nullptr, &m_showSaveEditor);
|
||||
ImGui::MenuItem("Player Info", hotkeys::SHOW_PLAYER_INFO, &m_showPlayerInfo);
|
||||
ImGui::MenuItem("Save Editor", hotkeys::SHOW_SAVE_EDITOR, &m_showSaveEditor);
|
||||
ImGui::MenuItem("Map Loader", hotkeys::SHOW_MAP_LOADER, &m_showMapLoader);
|
||||
ImGui::MenuItem("State Share", hotkeys::SHOW_STATE_SHARE, &m_showStateShare);
|
||||
ImGui::MenuItem("Debug Camera", hotkeys::SHOW_DEBUG_CAMERA, &m_showCameraOverlay);
|
||||
ImGui::MenuItem("Audio Debug", hotkeys::SHOW_AUDIO_DEBUG, &m_showAudioDebug);
|
||||
|
||||
ImGui::MenuItem("Stub Log", nullptr, &m_showStubLog);
|
||||
|
||||
if (!dusk::IsGameLaunched) {
|
||||
ImGui::EndDisabled();
|
||||
@@ -127,7 +129,7 @@ namespace dusk {
|
||||
}
|
||||
|
||||
void ImGuiMenuTools::ShowPlayerInfo() {
|
||||
if (!m_showPlayerInfo) {
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F5, m_showPlayerInfo)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
#include "imgui.h"
|
||||
#include "ImGuiSaveEditor.hpp"
|
||||
#include "ImGuiStateShare.hpp"
|
||||
|
||||
namespace dusk {
|
||||
class ImGuiMenuTools {
|
||||
@@ -23,6 +24,7 @@ namespace dusk {
|
||||
void ShowPlayerInfo();
|
||||
void ShowAudioDebug();
|
||||
void ShowSaveEditor();
|
||||
void ShowStateShare();
|
||||
|
||||
private:
|
||||
bool m_showDebugOverlay = false;
|
||||
@@ -57,6 +59,9 @@ namespace dusk {
|
||||
|
||||
bool m_showSaveEditor = false;
|
||||
ImGuiSaveEditor m_saveEditor;
|
||||
|
||||
bool m_showStateShare = false;
|
||||
ImGuiStateShare m_stateShare;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,11 @@ void fileDialogCallback(void* userdata, const char* const* filelist, [[maybe_unu
|
||||
ImGuiPreLaunchWindow::ImGuiPreLaunchWindow() = default;
|
||||
|
||||
bool ImGuiPreLaunchWindow::isSelectedPathValid() const {
|
||||
#if TARGET_ANDROID
|
||||
return !m_selectedIsoPath.empty(); // unsure why SDL_GetPathInfo doesnt work here
|
||||
#else
|
||||
return !m_selectedIsoPath.empty() && SDL_GetPathInfo(m_selectedIsoPath.c_str(), nullptr);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ImGuiPreLaunchWindow::draw() {
|
||||
@@ -80,12 +84,20 @@ void ImGuiPreLaunchWindow::draw() {
|
||||
|
||||
float iconSize = 150.f;
|
||||
ImGui::SameLine(windowSize.x / 2 - iconSize + (iconSize / 2));
|
||||
if (ImGuiEngine::duskIcon != 0)
|
||||
ImGui::Image(ImGuiEngine::duskIcon, ImVec2{iconSize, iconSize});
|
||||
if (ImGuiEngine::orgIcon != 0) {
|
||||
ImGui::Image(ImGuiEngine::orgIcon, ImVec2{iconSize, iconSize});
|
||||
}
|
||||
ImGuiTextCenter("Twilit Realm presents");
|
||||
ImGui::PushFont(ImGuiEngine::fontExtraLarge);
|
||||
ImGuiTextCenter("Dusk");
|
||||
ImGui::PopFont();
|
||||
if (ImGuiEngine::duskLogo) {
|
||||
ImGui::NewLine();
|
||||
float width = iconSize * 2.5f;
|
||||
ImGui::SameLine(windowSize.x / 2 - width + (width / 2));
|
||||
ImGui::Image(ImGuiEngine::duskLogo, ImVec2{width, iconSize});
|
||||
} else {
|
||||
ImGui::PushFont(ImGuiEngine::fontExtraLarge);
|
||||
ImGuiTextCenter("Dusk");
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
(this->*drawTable[m_CurMenu])();
|
||||
|
||||
@@ -98,7 +110,7 @@ void ImGuiPreLaunchWindow::drawMainMenu() {
|
||||
|
||||
ImGui::PushFont(ImGuiEngine::fontLarge);
|
||||
|
||||
if (m_selectedIsoPath.empty() || !SDL_GetPathInfo(m_selectedIsoPath.c_str(), nullptr)) {
|
||||
if (!isSelectedPathValid()) {
|
||||
if (ImGuiButtonCenter("Select disc image...")) {
|
||||
SDL_ShowOpenFileDialog(&fileDialogCallback, this, aurora::window::get_sdl_window(),
|
||||
skGameDiscFileFilters.data(), int(skGameDiscFileFilters.size()),
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
#include "ImGuiStateShare.hpp"
|
||||
#include "ImGuiMenuTools.hpp"
|
||||
#include "ImGuiConsole.hpp"
|
||||
|
||||
#include "imgui.h"
|
||||
#include "fmt/format.h"
|
||||
#include "absl/strings/escaping.h"
|
||||
|
||||
#include "d/d_com_inf_game.h"
|
||||
#include "dusk/main.h"
|
||||
|
||||
#include <zstd.h>
|
||||
|
||||
namespace dusk {
|
||||
|
||||
#pragma pack(push, 1)
|
||||
struct StateSharePacket {
|
||||
char stageName[8];
|
||||
int8_t roomNo;
|
||||
int8_t layer;
|
||||
int16_t startPoint;
|
||||
// followed by raw dSv_info_c bytes
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
static constexpr size_t PACKET_TOTAL = sizeof(StateSharePacket) + sizeof(dSv_info_c);
|
||||
|
||||
void ImGuiStateShare::copyState() {
|
||||
StateSharePacket pkt = {};
|
||||
strncpy(pkt.stageName, dComIfGp_getStartStageName(), 7);
|
||||
pkt.roomNo = dComIfGp_getStartStageRoomNo();
|
||||
pkt.layer = dComIfGp_getStartStageLayer();
|
||||
pkt.startPoint = dComIfGp_getStartStagePoint();
|
||||
|
||||
std::string raw(PACKET_TOTAL, '\0');
|
||||
memcpy(raw.data(), &pkt, sizeof(pkt));
|
||||
memcpy(raw.data() + sizeof(pkt), &g_dComIfG_gameInfo.info, sizeof(dSv_info_c));
|
||||
|
||||
size_t bound = ZSTD_compressBound(raw.size());
|
||||
std::string compressed(bound, '\0');
|
||||
compressed.resize(ZSTD_compress(compressed.data(), bound, raw.data(), raw.size(), 1));
|
||||
|
||||
std::string encoded = absl::Base64Escape(compressed);
|
||||
ImGui::SetClipboardText(encoded.c_str());
|
||||
m_statusMsg = "Copied to clipboard.";
|
||||
}
|
||||
|
||||
bool ImGuiStateShare::pasteState() {
|
||||
const char* clip = ImGui::GetClipboardText();
|
||||
if (!clip || clip[0] == '\0') {
|
||||
m_statusMsg = "Clipboard is empty.";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string decoded;
|
||||
if (!absl::Base64Unescape(clip, &decoded)) {
|
||||
m_statusMsg = "Invalid base64.";
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned long long dSize = ZSTD_getFrameContentSize(decoded.data(), decoded.size());
|
||||
if (dSize == ZSTD_CONTENTSIZE_ERROR || dSize == ZSTD_CONTENTSIZE_UNKNOWN || dSize < PACKET_TOTAL) {
|
||||
m_statusMsg = "Not a valid state string.";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string raw(static_cast<size_t>(dSize), '\0');
|
||||
size_t result = ZSTD_decompress(raw.data(), raw.size(), decoded.data(), decoded.size());
|
||||
if (ZSTD_isError(result)) {
|
||||
m_statusMsg = fmt::format("Decompression failed: {}", ZSTD_getErrorName(result));
|
||||
return false;
|
||||
}
|
||||
|
||||
StateSharePacket pkt;
|
||||
memcpy(&pkt, raw.data(), sizeof(pkt));
|
||||
pkt.stageName[7] = '\0';
|
||||
|
||||
memcpy(&g_dComIfG_gameInfo.info, raw.data() + sizeof(pkt), sizeof(dSv_info_c));
|
||||
|
||||
s16 spawnPoint = pkt.startPoint == -4 ? -1 : pkt.startPoint;
|
||||
|
||||
if (spawnPoint == -1) {
|
||||
dComIfGs_setRestartRoomParam(pkt.roomNo & 0x3F);
|
||||
}
|
||||
|
||||
dComIfGp_setNextStage(pkt.stageName, spawnPoint, pkt.roomNo, pkt.layer);
|
||||
m_pendingInfo = g_dComIfG_gameInfo.info;
|
||||
|
||||
m_statusMsg = fmt::format("Warping to {} room {} layer {}.", pkt.stageName, (int)pkt.roomNo, (int)pkt.layer);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ImGuiStateShare::tickPendingApply() {
|
||||
if (!m_pendingInfo.has_value() || dComIfGp_isEnableNextStage())
|
||||
return;
|
||||
g_dComIfG_gameInfo.info = *m_pendingInfo;
|
||||
m_pendingInfo.reset();
|
||||
}
|
||||
|
||||
void ImGuiStateShare::draw(bool& open) {
|
||||
if (dusk::IsGameLaunched)
|
||||
tickPendingApply();
|
||||
|
||||
if (!open)
|
||||
return;
|
||||
|
||||
if (!ImGui::Begin("State Share", &open, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dusk::IsGameLaunched) ImGui::BeginDisabled();
|
||||
if (ImGui::Button("Copy State")) copyState();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Import State")) pasteState();
|
||||
if (!dusk::IsGameLaunched) ImGui::EndDisabled();
|
||||
|
||||
if (!m_statusMsg.empty()) {
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextWrapped("%s", m_statusMsg.c_str());
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
void ImGuiMenuTools::ShowStateShare() {
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F8, m_showStateShare))
|
||||
return;
|
||||
m_stateShare.draw(m_showStateShare);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef DUSK_IMGUI_STATESHARE_HPP
|
||||
#define DUSK_IMGUI_STATESHARE_HPP
|
||||
|
||||
#include "d/d_save.h"
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace dusk {
|
||||
class ImGuiStateShare {
|
||||
public:
|
||||
void draw(bool& open);
|
||||
|
||||
private:
|
||||
void copyState();
|
||||
bool pasteState();
|
||||
void tickPendingApply();
|
||||
|
||||
std::string m_statusMsg;
|
||||
std::optional<dSv_info_c> m_pendingInfo;
|
||||
};
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#include "dusk/logging.h"
|
||||
#include "imgui.h"
|
||||
#include "ImGuiConsole.hpp"
|
||||
#include "ImGuiMenuTools.hpp"
|
||||
|
||||
namespace dusk {
|
||||
@@ -49,7 +48,7 @@ namespace dusk {
|
||||
void ImGuiMenuTools::ShowStubLog() {
|
||||
std::lock_guard lock(StubLogMutex);
|
||||
|
||||
if (!ImGuiConsole::CheckMenuViewToggle(ImGuiKey_F5, m_showStubLog)) {
|
||||
if (!m_showStubLog) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
+154
-11
@@ -1,9 +1,20 @@
|
||||
#include "dusk/logging.h"
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "tracy/Tracy.hpp"
|
||||
|
||||
#if TARGET_ANDROID
|
||||
#include "android/log.h"
|
||||
#include <vector>
|
||||
#include <sstream>
|
||||
#endif
|
||||
|
||||
bool StubLogEnabled = true;
|
||||
|
||||
using namespace std::literals::string_view_literals;
|
||||
@@ -20,6 +31,60 @@ static constexpr std::string_view StubFragments[] = {
|
||||
"but selective updates are not implemented"sv,
|
||||
};
|
||||
|
||||
namespace {
|
||||
std::mutex g_logMutex;
|
||||
FILE* g_logFile = nullptr;
|
||||
std::string g_logFilePath;
|
||||
|
||||
const char* LogLevelString(AuroraLogLevel level) {
|
||||
switch (level) {
|
||||
case LOG_DEBUG:
|
||||
return "DEBUG";
|
||||
case LOG_INFO:
|
||||
return "INFO";
|
||||
case LOG_WARNING:
|
||||
return "WARNING";
|
||||
case LOG_ERROR:
|
||||
return "ERROR";
|
||||
case LOG_FATAL:
|
||||
return "FATAL";
|
||||
}
|
||||
|
||||
return "??";
|
||||
}
|
||||
|
||||
FILE* LogStreamForLevel(AuroraLogLevel level) {
|
||||
return level >= LOG_ERROR ? stderr : stdout;
|
||||
}
|
||||
|
||||
std::string MakeTimestampedLogName() {
|
||||
const auto now = std::chrono::system_clock::now();
|
||||
const std::time_t nowTime = std::chrono::system_clock::to_time_t(now);
|
||||
|
||||
std::tm localTime{};
|
||||
#if _WIN32
|
||||
localtime_s(&localTime, &nowTime);
|
||||
#else
|
||||
localtime_r(&nowTime, &localTime);
|
||||
#endif
|
||||
|
||||
std::array<char, 32> buffer{};
|
||||
std::strftime(buffer.data(), buffer.size(), "dusk-%Y%m%d-%H%M%S.log", &localTime);
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
void WriteLogLine(FILE* out, const char* levelStr, const char* module, const char* message, unsigned int len) {
|
||||
if (out == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::fprintf(out, "[%s | %s] ", levelStr, module);
|
||||
std::fwrite(message, 1, len, out);
|
||||
std::fputc('\n', out);
|
||||
std::fflush(out);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
static bool IsForStubLog(const char* message) {
|
||||
std::string_view msg_view(message);
|
||||
|
||||
@@ -32,6 +97,7 @@ static bool IsForStubLog(const char* message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
#if TARGET_ANDROID
|
||||
void aurora_log_callback(AuroraLogLevel level, const char* module, const char* message,
|
||||
unsigned int len) {
|
||||
ZoneScoped;
|
||||
@@ -40,32 +106,109 @@ void aurora_log_callback(AuroraLogLevel level, const char* module, const char* m
|
||||
return;
|
||||
}
|
||||
|
||||
const char* levelStr = "??";
|
||||
FILE* out = stdout;
|
||||
int android_log_level = 0;
|
||||
switch (level) {
|
||||
case LOG_DEBUG:
|
||||
levelStr = "DEBUG";
|
||||
android_log_level = ANDROID_LOG_DEBUG;
|
||||
break;
|
||||
case LOG_INFO:
|
||||
levelStr = "INFO";
|
||||
android_log_level = ANDROID_LOG_INFO;
|
||||
break;
|
||||
case LOG_WARNING:
|
||||
levelStr = "WARNING";
|
||||
android_log_level = ANDROID_LOG_WARN;
|
||||
break;
|
||||
case LOG_ERROR:
|
||||
levelStr = "ERROR";
|
||||
out = stderr;
|
||||
android_log_level = ANDROID_LOG_ERROR;
|
||||
break;
|
||||
case LOG_FATAL:
|
||||
levelStr = "FATAL";
|
||||
out = stderr;
|
||||
android_log_level = ANDROID_LOG_FATAL;
|
||||
break;
|
||||
}
|
||||
fprintf(out, "[%s | %s] %s\n", levelStr, module, message);
|
||||
|
||||
std::stringstream msgStream(message);
|
||||
std::string segment;
|
||||
while(std::getline(msgStream, segment)) {
|
||||
__android_log_print(android_log_level, module, "%s\n", segment.c_str());
|
||||
}
|
||||
|
||||
if (level == LOG_FATAL) {
|
||||
fflush(out);
|
||||
abort();
|
||||
}
|
||||
}
|
||||
#else
|
||||
void aurora_log_callback(AuroraLogLevel level, const char* module, const char* message,
|
||||
unsigned int len) {
|
||||
ZoneScoped;
|
||||
if (StubLogEnabled && level != LOG_FATAL && IsForStubLog(message)) {
|
||||
dusk::SendToStubLog(level, module, message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (module == nullptr) {
|
||||
module = "";
|
||||
}
|
||||
|
||||
const char* levelStr = LogLevelString(level);
|
||||
FILE* out = LogStreamForLevel(level);
|
||||
WriteLogLine(out, levelStr, module, message, len);
|
||||
|
||||
{
|
||||
std::lock_guard lock(g_logMutex);
|
||||
if (g_logFile != nullptr) {
|
||||
WriteLogLine(g_logFile, levelStr, module, message, len);
|
||||
}
|
||||
}
|
||||
|
||||
if (level == LOG_FATAL) {
|
||||
abort();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
aurora::Module DuskLog("dusk");
|
||||
|
||||
void dusk::InitializeFileLogging(const char* configDir, AuroraLogLevel logLevel) {
|
||||
std::lock_guard lock(g_logMutex);
|
||||
if (g_logFile != nullptr || configDir == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
const std::filesystem::path logsDir = std::filesystem::path(configDir) / "logs";
|
||||
std::filesystem::create_directories(logsDir, ec);
|
||||
if (ec) {
|
||||
std::fprintf(stderr, "[WARNING | dusk] Failed to create log directory '%s': %s\n",
|
||||
logsDir.string().c_str(), ec.message().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
const std::filesystem::path logPath = logsDir / MakeTimestampedLogName();
|
||||
g_logFile = std::fopen(logPath.string().c_str(), "wb");
|
||||
if (g_logFile == nullptr) {
|
||||
std::fprintf(stderr, "[WARNING | dusk] Failed to open log file '%s'\n",
|
||||
logPath.string().c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
g_logFilePath = logPath.string();
|
||||
aurora::g_config.logCallback = &aurora_log_callback;
|
||||
aurora::g_config.logLevel = logLevel;
|
||||
WriteLogLine(g_logFile, "INFO", "dusk", "File logging initialized", 24);
|
||||
}
|
||||
|
||||
void dusk::ShutdownFileLogging() {
|
||||
std::lock_guard lock(g_logMutex);
|
||||
if (g_logFile == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::fflush(g_logFile);
|
||||
std::fclose(g_logFile);
|
||||
g_logFile = nullptr;
|
||||
}
|
||||
|
||||
const char* dusk::GetLogFilePath() {
|
||||
std::lock_guard lock(g_logMutex);
|
||||
return g_logFilePath.empty() ? nullptr : g_logFilePath.c_str();
|
||||
}
|
||||
|
||||
+111
-11
@@ -1,26 +1,126 @@
|
||||
#if _WIN32
|
||||
#define WINDOWS_LEAN_AND_MEAN
|
||||
#include <Windows.h>
|
||||
#include <shellapi.h>
|
||||
#endif
|
||||
|
||||
#include <aurora/main.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
int game_main(int argc, char* argv[]);
|
||||
|
||||
void WindowsSetupConsole();
|
||||
namespace {
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
WindowsSetupConsole();
|
||||
#if _WIN32
|
||||
bool ShouldShowWindowsConsole(int argc, char* argv[]) {
|
||||
if (const auto* env = std::getenv("DUSK_CONSOLE")) {
|
||||
if (env[0] != '\0' && env[0] != '0') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
if (std::string_view(argv[i]) == "--console") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void SetupWindowsConsoleStreams() {
|
||||
FILE* stream = nullptr;
|
||||
freopen_s(&stream, "CONIN$", "r", stdin);
|
||||
freopen_s(&stream, "CONOUT$", "w", stdout);
|
||||
freopen_s(&stream, "CONOUT$", "w", stderr);
|
||||
}
|
||||
|
||||
void WindowsSetupConsole(bool showConsole) {
|
||||
if (!showConsole) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AttachConsole(ATTACH_PARENT_PROCESS)) {
|
||||
AllocConsole();
|
||||
}
|
||||
|
||||
SetupWindowsConsoleStreams();
|
||||
SetConsoleOutputCP(CP_UTF8);
|
||||
|
||||
if (const HANDLE stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
stdoutHandle != INVALID_HANDLE_VALUE && stdoutHandle != nullptr) {
|
||||
DWORD consoleMode = 0;
|
||||
if (GetConsoleMode(stdoutHandle, &consoleMode)) {
|
||||
SetConsoleMode(stdoutHandle,
|
||||
consoleMode | ENABLE_PROCESSED_OUTPUT
|
||||
| ENABLE_VIRTUAL_TERMINAL_PROCESSING);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int DuskMain(int argc, char* argv[]) {
|
||||
WindowsSetupConsole(ShouldShowWindowsConsole(argc, argv));
|
||||
return game_main(argc, argv);
|
||||
}
|
||||
|
||||
void WindowsSetupConsole() {
|
||||
#if _WIN32
|
||||
SetConsoleOutputCP(CP_UTF8);
|
||||
std::vector<std::string> WideArgsToUtf8(int argc, wchar_t** argv) {
|
||||
std::vector<std::string> utf8Args;
|
||||
utf8Args.reserve(argc);
|
||||
|
||||
auto stdout = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
DWORD consoleMode;
|
||||
GetConsoleMode(stdout, &consoleMode);
|
||||
SetConsoleMode(stdout, consoleMode | ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
|
||||
for (int i = 0; i < argc; ++i) {
|
||||
const int requiredSize =
|
||||
WideCharToMultiByte(CP_UTF8, 0, argv[i], -1, nullptr, 0, nullptr, nullptr);
|
||||
if (requiredSize <= 0) {
|
||||
utf8Args.emplace_back();
|
||||
continue;
|
||||
}
|
||||
|
||||
std::vector<char> utf8Buffer(static_cast<size_t>(requiredSize));
|
||||
WideCharToMultiByte(CP_UTF8, 0, argv[i], -1, utf8Buffer.data(), requiredSize, nullptr,
|
||||
nullptr);
|
||||
utf8Args.emplace_back(utf8Buffer.data());
|
||||
}
|
||||
|
||||
return utf8Args;
|
||||
}
|
||||
|
||||
int RunWindowsGuiEntryPoint() {
|
||||
int argc = 0;
|
||||
wchar_t** wideArgv = CommandLineToArgvW(GetCommandLineW(), &argc);
|
||||
if (wideArgv == nullptr) {
|
||||
return DuskMain(__argc, __argv);
|
||||
}
|
||||
|
||||
std::vector<std::string> utf8Args = WideArgsToUtf8(argc, wideArgv);
|
||||
LocalFree(wideArgv);
|
||||
|
||||
std::vector<char*> argv;
|
||||
argv.reserve(utf8Args.size());
|
||||
for (auto& arg : utf8Args) {
|
||||
argv.push_back(arg.data());
|
||||
}
|
||||
|
||||
return DuskMain(argc, argv.data());
|
||||
}
|
||||
#else
|
||||
int DuskMain(int argc, char* argv[]) {
|
||||
return game_main(argc, argv);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
return DuskMain(argc, argv);
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
int WINAPI wWinMain(HINSTANCE, HINSTANCE, PWSTR, int) {
|
||||
return RunWindowsGuiEntryPoint();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -35,13 +35,15 @@ UserSettings g_userSettings = {
|
||||
.noMissClimbing {"game.noMissClimbing", false},
|
||||
.fastTears {"game.fastTears", false},
|
||||
.instantSaves {"game.instantSaves", false},
|
||||
.sunsSong {"game.sunsSong", false},
|
||||
|
||||
// Preferences
|
||||
.enableMirrorMode {"game.enableMirrorMode", false},
|
||||
.invertCameraXAxis {"game.invertCameraXAxis", false},
|
||||
|
||||
// Graphics
|
||||
.enableBloom {"game.enableBloom", true},
|
||||
.bloomMode {"game.bloomMode", BloomMode::Classic},
|
||||
.bloomMultiplier {"game.bloomMultiplier", 1.0f},
|
||||
.enableWaterRefraction {"game.enableWaterRefraction", true},
|
||||
.enableFrameInterpolation = {"game.enableFrameInterpolation", false},
|
||||
.shadowResolutionMultiplier {"game.shadowResolutionMultiplier", 1},
|
||||
@@ -75,7 +77,8 @@ UserSettings g_userSettings = {
|
||||
.graphicsBackend {"backend.graphicsBackend", "auto"},
|
||||
.skipPreLaunchUI {"backend.skipPreLaunchUI", false},
|
||||
.showPipelineCompilation {"backend.showPipelineCompilation", false},
|
||||
.wasPresetChosen {"backend.wasPresetChosen", false}
|
||||
.wasPresetChosen {"backend.wasPresetChosen", false},
|
||||
.enableCrashReporting {"backend.enableCrashReporting", true}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -111,9 +114,11 @@ void registerSettings() {
|
||||
Register(g_userSettings.game.fastClimbing);
|
||||
Register(g_userSettings.game.fastTears);
|
||||
Register(g_userSettings.game.instantSaves);
|
||||
Register(g_userSettings.game.sunsSong);
|
||||
Register(g_userSettings.game.enableMirrorMode);
|
||||
Register(g_userSettings.game.invertCameraXAxis);
|
||||
Register(g_userSettings.game.enableBloom);
|
||||
Register(g_userSettings.game.bloomMode);
|
||||
Register(g_userSettings.game.bloomMultiplier);
|
||||
Register(g_userSettings.game.enableWaterRefraction);
|
||||
Register(g_userSettings.game.shadowResolutionMultiplier);
|
||||
Register(g_userSettings.game.enableFastIronBoots);
|
||||
@@ -137,6 +142,7 @@ void registerSettings() {
|
||||
Register(g_userSettings.backend.skipPreLaunchUI);
|
||||
Register(g_userSettings.backend.showPipelineCompilation);
|
||||
Register(g_userSettings.backend.wasPresetChosen);
|
||||
Register(g_userSettings.backend.enableCrashReporting);
|
||||
}
|
||||
|
||||
// Transient settings
|
||||
|
||||
@@ -748,6 +748,12 @@ void fapGm_Execute() {
|
||||
#endif
|
||||
|
||||
#if TARGET_PC
|
||||
if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigX(PAD_1)) {
|
||||
if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) {
|
||||
dynamic_cast<daAlink_c*>(link)->handleWolfHowl();
|
||||
}
|
||||
}
|
||||
|
||||
if (mDoCPd_c::getHoldR(PAD_1) && mDoCPd_c::getTrigY(PAD_1)) {
|
||||
if (const auto link = g_dComIfG_gameInfo.play.getPlayer(0)) {
|
||||
dynamic_cast<daAlink_c*>(link)->handleQuickTransform();
|
||||
|
||||
+80
-52
@@ -28,10 +28,6 @@
|
||||
#include "d/d_menu_collect.h"
|
||||
#include "d/d_meter2_info.h"
|
||||
#include "d/d_s_play.h"
|
||||
#include "dusk/endian.h"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/gx_helper.h"
|
||||
#include "dusk/logging.h"
|
||||
#include "f_ap/f_ap_game.h"
|
||||
#include "f_op/f_op_actor_mng.h"
|
||||
#include "f_op/f_op_camera_mng.h"
|
||||
@@ -52,8 +48,12 @@
|
||||
|
||||
#if TARGET_PC
|
||||
#include "d/actor/d_a_horse.h"
|
||||
#include "dusk/imgui/ImGuiConsole.hpp"
|
||||
#include "dusk/dusk.h"
|
||||
#include "dusk/endian.h"
|
||||
#include "dusk/frame_interpolation.h"
|
||||
#include "dusk/gx_helper.h"
|
||||
#include "dusk/imgui/ImGuiConsole.hpp"
|
||||
#include "dusk/logging.h"
|
||||
#endif
|
||||
|
||||
class mDoGph_HIO_c : public JORReflexible {
|
||||
@@ -1259,7 +1259,7 @@ void mDoGph_gInf_c::bloom_c::remove() {
|
||||
}
|
||||
|
||||
#if TARGET_PC
|
||||
static void CopyToTexObj(GXTexObj* pDst, uintptr_t texID, int dstWidth, int dstHeight, GXTexFmt dstFmt = GX_TF_RGBA8) {
|
||||
static void CopyToTexObj(GXTexObj* pDst, uintptr_t texID, u16 dstWidth, u16 dstHeight, GXTexFmt dstFmt = GX_TF_RGBA8) {
|
||||
GXSetTexCopyDst(dstWidth, dstHeight, dstFmt, FALSE);
|
||||
GXCopyTex((void*)texID, false);
|
||||
GXInitTexObj(pDst, (void*)texID, dstWidth, dstHeight, dstFmt, GX_CLAMP, GX_CLAMP, GX_FALSE);
|
||||
@@ -1311,35 +1311,58 @@ void mDoGph_gInf_c::bloom_c::draw2() {
|
||||
BlurPass0, BlurPassN = BlurPass0 + MaxDivNum,
|
||||
MaxTexNum,
|
||||
};
|
||||
scissor_class divPorts[MaxDivNum];
|
||||
divPorts[0] = {0.0f, 0.0f, 1.0f, 1.0f}; // full-size texture
|
||||
divPorts[1] = {0.0f, 0.0f, 0.5f, 0.5f}; // bloom texture (wide enough, half-tall)
|
||||
divPorts[2] = {0.5f, 0.0f, 0.25f, 0.25f};
|
||||
for (int i = 3; i < ARRAY_SIZE(divPorts); i++) {
|
||||
auto& port = divPorts[i];
|
||||
auto const& prev = divPorts[i - 1];
|
||||
port.x_orig = prev.x_orig;
|
||||
port.y_orig = prev.y_orig + prev.height;
|
||||
port.width = prev.width * 0.5f;
|
||||
port.height = prev.height * 0.5f;
|
||||
struct {
|
||||
u16 x;
|
||||
u16 y;
|
||||
u16 w;
|
||||
u16 h;
|
||||
} divRects[MaxDivNum];
|
||||
divRects[0] = {
|
||||
0,
|
||||
0,
|
||||
static_cast<u16>(width),
|
||||
static_cast<u16>(height),
|
||||
};
|
||||
divRects[1] = {
|
||||
0,
|
||||
0,
|
||||
static_cast<u16>(divRects[0].w / 2),
|
||||
static_cast<u16>(divRects[0].h / 2),
|
||||
};
|
||||
divRects[2] = {
|
||||
divRects[1].w,
|
||||
0,
|
||||
static_cast<u16>(divRects[1].w / 2),
|
||||
static_cast<u16>(divRects[1].h / 2),
|
||||
};
|
||||
for (int i = 3; i < ARRAY_SIZE(divRects); i++) {
|
||||
const auto& prev = divRects[i - 1];
|
||||
divRects[i] = {
|
||||
prev.x,
|
||||
static_cast<u16>(prev.y + prev.h),
|
||||
static_cast<u16>(prev.w / 2),
|
||||
static_cast<u16>(prev.h / 2),
|
||||
};
|
||||
}
|
||||
|
||||
auto divCopySrc = [&](int divNo) {
|
||||
auto const& port = divPorts[divNo];
|
||||
GXSetTexCopySrc(port.x_orig * width, port.y_orig * height, port.width * width, port.height * height);
|
||||
auto const& rect = divRects[divNo];
|
||||
GXSetTexCopySrc(rect.x, rect.y, rect.w, rect.h);
|
||||
};
|
||||
|
||||
TGXTexObj tmpTex[MaxTexNum];
|
||||
auto divCopyTex = [&](uintptr_t texNo, int divNo) -> GXTexObj * {
|
||||
auto const& port = divPorts[divNo];
|
||||
CopyToTexObj(&tmpTex[texNo], texNo, port.width * width, port.height * height);
|
||||
auto const& rect = divRects[divNo];
|
||||
CopyToTexObj(&tmpTex[texNo], texNo, rect.w, rect.h);
|
||||
return &tmpTex[texNo];
|
||||
};
|
||||
|
||||
auto divQuad = [&](int divNo) {
|
||||
auto const& port = divPorts[divNo];
|
||||
f32 x0 = port.x_orig, y0 = port.y_orig;
|
||||
f32 x1 = x0 + port.width, y1 = y0 + port.height;
|
||||
auto const& rect = divRects[divNo];
|
||||
f32 x0 = rect.x / width;
|
||||
f32 y0 = rect.y / height;
|
||||
f32 x1 = (rect.x + rect.w) / width;
|
||||
f32 y1 = (rect.y + rect.h) / height;
|
||||
GXBegin(GX_QUADS, GX_VTXFMT0, 4);
|
||||
GXPosition3f32(x0, y0, -5);
|
||||
GXTexCoord2s8(0, 0);
|
||||
@@ -1409,18 +1432,16 @@ void mDoGph_gInf_c::bloom_c::draw2() {
|
||||
// Setup blur filter TEV.
|
||||
GXSetNumTexGens(8);
|
||||
|
||||
auto SetupBlurMtx = [](float scale) {
|
||||
u32 texMtxID = GX_TEXMTX0;
|
||||
int angle = 0;
|
||||
for (int texCoord = (int)GX_TEXCOORD0; texCoord < (int)GX_MAX_TEXCOORD; texCoord++) {
|
||||
GXSetTexCoordGen((GXTexCoordID)texCoord, GX_TG_MTX2x4, GX_TG_TEX0, texMtxID);
|
||||
mDoMtx_stack_c::transS((scale * cM_scos(angle)) * getInvScale(),
|
||||
scale * cM_ssin(angle), 0.0f);
|
||||
mDoMtx_stack_c::transS((blurScale * cM_scos(angle)) * getInvScale(),
|
||||
blurScale * cM_ssin(angle), 0.0f);
|
||||
GXLoadTexMtxImm(mDoMtx_stack_c::get(), texMtxID, GX_MTX2x4);
|
||||
texMtxID += 3;
|
||||
angle += 0x2000;
|
||||
}
|
||||
};
|
||||
|
||||
GXSetNumTevStages(8);
|
||||
for (int stage = 0; stage < 8; stage++) {
|
||||
@@ -1433,19 +1454,20 @@ void mDoGph_gInf_c::bloom_c::draw2() {
|
||||
}
|
||||
|
||||
// Successively downsample and apply blurs.
|
||||
int divStart = 2;
|
||||
int divNum = 6;
|
||||
static int divStart = 2;
|
||||
static int divNum = 6; // inclusive
|
||||
|
||||
// Distribute the brightness through each pass.
|
||||
int totalNumPasses = (divNum - divStart) * 2; // down, up
|
||||
float brightnessF32 = (mBlureRatio / 255.0f);
|
||||
// The original mBlureRatio is multiplied into each sample, of which there are 8 samples originally.
|
||||
// This is applied over two passes, the second one with an alpha of 25%; however, the clipping that this introduces is a bit integral to the look,
|
||||
// so we do the same thing, letting it clip.
|
||||
float brightnessF32 = (mBlureRatio * 16 / 255.0f);
|
||||
|
||||
// Distribute the brightness through the total number of passes.
|
||||
f32 totalNumPasses = (divNum - divStart + 1);
|
||||
float brightnessPerPass = 255.0f * powf(brightnessF32, 1.0f / totalNumPasses);
|
||||
GXSetTevColorS10(GX_TEVREG1, {0, 0, 0, s16(brightnessPerPass / 8)});
|
||||
|
||||
for (int i = divStart; i < divNum; i++) {
|
||||
float blurStrength = 1.0f + (i - divStart) * 5.0f;
|
||||
SetupBlurMtx(blurScale * blurStrength);
|
||||
|
||||
// Apply blur filter.
|
||||
divQuad(i);
|
||||
|
||||
@@ -1457,16 +1479,18 @@ void mDoGph_gInf_c::bloom_c::draw2() {
|
||||
GXLoadTexObj(blurTex, GX_TEXMAP0);
|
||||
}
|
||||
|
||||
// All the way down at the bottom. Instead of blurring the bottom layer by itself, we blur when going up to the next layer.
|
||||
// The remaining upscales are all just normal alpha blending.
|
||||
// All the way down at the bottom.
|
||||
divQuad(divNum);
|
||||
|
||||
// Now successively alpha blend back up, don't blur anymore.
|
||||
GXSetNumTevStages(1);
|
||||
GXSetTevColorS10(GX_TEVREG1, {0, 0, 0, s16(brightnessPerPass)});
|
||||
GXSetTevColorS10(GX_TEVREG1, {0, 0, 0, s16(255)});
|
||||
GXSetTexCoordGen(GX_TEXCOORD0, GX_TG_MTX2x4, GX_TG_TEX0, GX_IDENTITY);
|
||||
GXSetBlendMode(GX_BM_BLEND, GX_BL_ONE, GX_BL_ONE, GX_LO_OR);
|
||||
GXSetBlendMode(GX_BM_BLEND, GX_BL_SRCALPHA, GX_BL_ONE, GX_LO_OR);
|
||||
for (int i = divNum; i > divStart; i--) {
|
||||
float alpha = 255.0f * powf(0.25f * dusk::getSettings().game.bloomMultiplier.getValue(), 1.0f / (divNum - i + 1));
|
||||
GXSetTevColorS10(GX_TEVREG0, {0, 0, 0, s16(alpha)});
|
||||
|
||||
divCopySrc(i);
|
||||
GXTexObj* upTex = divCopyTex(BlurPass0 + i, i);
|
||||
GXLoadTexObj(upTex, GX_TEXMAP0);
|
||||
@@ -1500,13 +1524,11 @@ void mDoGph_gInf_c::bloom_c::draw2() {
|
||||
|
||||
void mDoGph_gInf_c::bloom_c::draw() {
|
||||
ZoneScoped;
|
||||
if (!dusk::getSettings().game.enableBloom) {
|
||||
if (dusk::getSettings().game.bloomMode.getValue() == dusk::BloomMode::Dusk) {
|
||||
draw2();
|
||||
return;
|
||||
}
|
||||
|
||||
static bool s_bloom2 = false;
|
||||
if (s_bloom2) {
|
||||
draw2();
|
||||
if (dusk::getSettings().game.bloomMode.getValue() != dusk::BloomMode::Classic) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1598,7 +1620,12 @@ void mDoGph_gInf_c::bloom_c::draw() {
|
||||
GXSetTevAlphaOp(GX_TEVSTAGE2, GX_TEV_ADD, GX_TB_ZERO, GX_CS_SCALE_1, GX_TRUE,
|
||||
GX_TEVPREV);
|
||||
GXSetBlendMode(GX_BM_NONE, GX_BL_ZERO, GX_BL_ZERO, GX_LO_OR);
|
||||
GXColorS10 tevColor0 = {(s16)-mPoint, (s16)-mPoint, (s16)-mPoint, 0x40};
|
||||
#if TARGET_PC
|
||||
s16 bloomAlpha = s16(0x40 * dusk::getSettings().game.bloomMultiplier.getValue());
|
||||
#else
|
||||
s16 bloomAlpha = 0x40;
|
||||
#endif
|
||||
GXColorS10 tevColor0 = {(s16)-mPoint, (s16)-mPoint, (s16)-mPoint, bloomAlpha};
|
||||
GXSetTevColorS10(GX_TEVREG0, tevColor0);
|
||||
GXColor tevColor1 = {mBlureRatio, mBlureRatio, mBlureRatio, mBlureRatio};
|
||||
GXSetTevColor(GX_TEVREG1, tevColor1);
|
||||
@@ -1962,12 +1989,13 @@ static void captureScreenPerspDrawInfo(JPADrawInfo& info) {
|
||||
static void drawItem3D() {
|
||||
ZoneScoped;
|
||||
#ifdef TARGET_PC
|
||||
// Frame interpolation: Title screen needs 0.0f while everything else that runs through this is -100.0f.
|
||||
// Running presentation faster than logic revealed the problem. Thanks, Nintendo.
|
||||
if (fopAcM_SearchByName(fpcNm_TITLE_e) != nullptr) {
|
||||
dMenu_Collect3D_c::setViewPortOffsetY(0.0f);
|
||||
} else {
|
||||
dMenu_Collect3D_c::setViewPortOffsetY(-100.0f);
|
||||
if (dusk::getSettings().game.enableFrameInterpolation) {
|
||||
// FRAME INTERP NOTE: Title screen needs 0.0f while everything else that runs through this is -100.0f.
|
||||
if (fopAcM_SearchByName(fpcNm_TITLE_e) != nullptr) {
|
||||
dMenu_Collect3D_c::setViewPortOffsetY(0.0f);
|
||||
} else {
|
||||
dMenu_Collect3D_c::setViewPortOffsetY(-100.0f);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Mtx item_mtx;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user