pax_global_header00006660000000000000000000000064147726321240014522gustar00rootroot0000000000000052 comment=39712ee46e03d022f8055fe5fa69eef5be85e5c3 xsystem35-sdl2-2.14.3/000077500000000000000000000000001477263212400143375ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/.editorconfig000066400000000000000000000002411477263212400170110ustar00rootroot00000000000000# https://EditorConfig.org root = true [*] charset = utf-8 [*.{h,c}] indent_style = tab indent_size = 4 [CMakeLists.txt] indent_style = space indent_size = 2 xsystem35-sdl2-2.14.3/.github/000077500000000000000000000000001477263212400156775ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/.github/ISSUE_TEMPLATE/000077500000000000000000000000001477263212400200625ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000006431477263212400225570ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve xsystem35 title: '' labels: '' assignees: '' --- ### Environment - xsystem35 version: - OS: - Game title: ### Steps to reproduce the behavior 1. 2. 3. ### Expected behavior ### Actual behavior ### Additional context xsystem35-sdl2-2.14.3/.github/workflows/000077500000000000000000000000001477263212400177345ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/.github/workflows/android.yml000066400000000000000000000014041477263212400220760ustar00rootroot00000000000000name: Android Build on: [push, pull_request] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Install Deps run: sudo apt install ninja-build - name: Build run: | cd android echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore.jks JAVA_HOME=$JAVA_HOME_17_X64 ANDROID_NDK_HOME=$ANDROID_SDK_ROOT/ndk-bundle ./gradlew assembleRelease env: KEY_ALIAS: ${{ secrets.KEY_ALIAS }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: xsystem35-apk path: android/app/build/outputs/apk/release/app-release.apk xsystem35-sdl2-2.14.3/.github/workflows/emscripten.yml000066400000000000000000000017051477263212400226330ustar00rootroot00000000000000name: Emscripten Build on: [push, pull_request] env: EM_VERSION: latest EM_CACHE_FOLDER: 'emsdk-cache' jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup cache id: cache-system-libraries uses: actions/cache@v4 with: path: ${{env.EM_CACHE_FOLDER}} key: ${{env.EM_VERSION}}-${{ runner.os }} - name: Setup Emscripten toolchain uses: mymindstorm/setup-emsdk@v14 with: version: ${{ env.EM_VERSION }} actions-cache-folder: ${{env.EM_CACHE_FOLDER}} - name: Build run: | mkdir -p out/wasm cd out/wasm emcmake cmake -DCMAKE_BUILD_TYPE=MinSizeRel -DCMAKE_COMPILE_WARNING_AS_ERROR=YES ../../ make -j4 mkdir xsystem35 mv src/xsystem35.* xsystem35/ - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: xsystem35-wasm path: out/wasm/xsystem35 xsystem35-sdl2-2.14.3/.github/workflows/linux.yml000066400000000000000000000013551477263212400216220ustar00rootroot00000000000000name: Linux Build on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: build-type: ["Debug", "Release"] name: Linux ${{ matrix.build-type }} steps: - uses: actions/checkout@v4 - name: Install Deps run: | sudo apt update sudo apt install libgtk-3-dev libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev libwebp-dev libportmidi-dev libcjson-dev asciidoctor - name: Build run: | mkdir -p out/${{ matrix.build-type }} cd out/${{ matrix.build-type }} cmake -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} ../../ make -j4 - name: Test run: ctest --output-on-failure working-directory: out/${{ matrix.build-type }} xsystem35-sdl2-2.14.3/.github/workflows/windows.yml000066400000000000000000000033361477263212400221560ustar00rootroot00000000000000name: Windows Build on: [push, pull_request] jobs: build: runs-on: windows-latest strategy: fail-fast: false matrix: include: - sys: mingw32 package: "xsystem35-32bit" cmake_args: "-DENABLE_DEBUGGER=no" deps: "" - sys: ucrt64 package: "xsystem35-64bit" cmake_args: "" deps: "cjson:p" defaults: run: shell: msys2 {0} name: MSYS2 ${{ matrix.sys }} steps: - name: Set up MSYS2 uses: msys2/setup-msys2@v2 with: msystem: ${{ matrix.sys }} pacboy: >- SDL2:p SDL2_ttf:p SDL2_mixer:p libwebp:p portmidi:p ${{ matrix.deps }} - name: Checkout uses: actions/checkout@v4 - name: Copy licenses run: | ls /${{ matrix.sys }}/share/licenses mkdir dist cp -r /${{ matrix.sys }}/share/licenses dist/ cp fonts/*.license dist/licenses/ - name: Install dev dependencies run: pacboy --noconfirm -S --needed gcc:p cmake:p ninja:p asciidoctor:p - name: Build run: | mkdir -p out/release cd out/release cmake -GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_COMPILE_WARNING_AS_ERROR=YES ${{ matrix.cmake_args }} ../../ ninja - name: Test run: ctest --output-on-failure working-directory: out/release - name: Package run: | cp out/release/src/xsystem35.exe out/release/doc/*.html *.md COPYING xsys35rc.sample dist/ - name: Upload uses: actions/upload-artifact@v4 with: name: ${{ matrix.package }} path: dist compression-level: 9 # because we use it for releases xsystem35-sdl2-2.14.3/.gitignore000066400000000000000000000003651477263212400163330ustar00rootroot00000000000000out android/.gradle android/.idea android/app/.cxx android/app/.externalNativeBuild android/app/build android/app/src/main/assets android/build android/keystore.jks android/local.properties .ccls-cache .dir-locals.el compile_commands.json build xsystem35-sdl2-2.14.3/CHANGELOG.md000066400000000000000000000145721477263212400161610ustar00rootroot00000000000000# Changelog ## 2.14.3 - 2024-04-01 - Fixed build for big endian architectures. ## 2.14.2 - 2024-03-09 - Android: Back button to exit the game (#50) - Improved touch input usability in ママトト (#64) ## 2.14.1 - 2024-02-07 - Fixed font selection and text decoration not working in selection menus. - Fixed a graphic issue in DALK intro. ## 2.14.0 - 2024-01-09 - New supported games: 俺の下であがけ and 楽園行. - Windows: On Windows 10 version 1903 or above, now xsystem35 can handle Japanese filenames correctly even if the system locale is not Japanese. ## 2.13.0 - 2024-12-07 - Windows: `Option` -> `Auto Copy Text to Clipboard` menu command has been added. When enabled, in-game text will be copied to the clipboard automatically. This can be used with text hooker tools that support clipboard monitoring. ## 2.12.1 - 2024-12-02 - Windows: Fixed missing DLL error (#60) ## 2.12.0 - 2024-11-26 - Implemented playback of the opening movie for 妻みぐい2. ## 2.11.7 - 2024-11-12 - Fixed crash when canceling menu with two-finger touch (#58) - Fixed screen artifact in Rance 3 immediately after selecting "Game Start" ## 2.11.6 - 2024-07-22 - Fixed strange behavior when canceling the battle selection menu in Kichikuou Rance. (#56) - Use system mouse cursors whenever possible. They look better on high dpi displays. - Added manual page for the xsystem35 command. ## 2.11.5 - 2024-06-10 - Android: Fixed a black screen issue in the MangaGamer version of Rance 5D. - Fixed `fileCheckExist` command. (#55) ## 2.11.4 - 2024-05-22 - Windows: In full-screen mode, the menu bar now appears only when the mouse pointer is at the top of the screen. (#53) - Fixed text rendering issue in Rance 3 intro. (#54) ## 2.11.3 - 2024-04-11 - Android: Fixed a bug where save files from older versions could not be loaded in version 2.11.2. ## 2.11.2 - 2024-04-10 - Added `-savedir` option to specify save directory. - Android: Fixed a bug where .xsys35rc in a subdirectory of ZIP was not loaded. (#51) ## 2.11.1 - 2024-02-18 - Windows: Fixed a black screen issue in the MangaGamer version of Rance 5D (#45). - Fixed a crash bug in command-line debugger. ## 2.11.0 - 2024-01-20 - Windows: Now `xsystem35.exe` is a standalone executable. You can just copy it to the game folder and run it. - Windows: The installer is no longer provided. If you have an earlier version installed, please uninstall it. - Windows: The debugger is no longer available in the 32-bit version. Please use the 64-bit version if you need it. - Added experimental `enable_zb` option (#44). See `xsys35rc.sample` for details. ## 2.10.1 - 2024-01-01 - Windows: Added "Integer Scaling" menu option - Fixed initial palette colors (to match System3.9) - Fixed a bug where palette 246 to 249 were unintentionally changed after loading 256-color CGs ## 2.10.0 - 2023-12-09 - Breaking change: A [bug](https://github.com/kichikuou/xsystem35-sdl2/issues/41) has been fixed in which save format for 大悪司 and かえるにょ国にょアリス was not compatible with System3.9. As a result, save files for these games created with older versions of xsystem35 are no longer usable. (Loading will not fail, but wrong values will be loaded.) - Now the 64-bit executable supports Windows 10 or later. For older Windows, please use the 32-bit version. - Fixed a problem with the download edition of Daiakuji requiring insertion of Disk 2. (#43) - Debugger: Added debug commands for monitoring the color palette. - Minor bug fixes. ## 2.9.1 - 2023-06-22 - Fixed a bug where the `CX 1` command (copy with transparent color) did not work in some games (e.g. Daiakuji) - Debugger: Now breakpoints do not make the window completely unresponsive - Debugger: Fixed a conditional breakpoint crash bug ## 2.9.0 - 2023-05-04 - Now xsystem35 uses the System3.9 save file format (unless `-saveformat` command line flag is specified). Old save files can still be loaded. - Changed the default naming convention for save files from `[a-z]sleep.asd` to `s[a-z].asd`. - Fixed a bug where SACT games (e.g. Rance 5D) were not responding to touch. - Fixed memory leaks in debugger. ## 2.8.0 - 2023-02-12 - Added support for Rance 4.1/4.2 ver1.05 - Fixed movement animation in rooms in Rance 4 v2.05 (#35) - Added `-game` option which can be used to enable game-specific hacks in translated games ## 2.7.0 - 2023-01-28 - Added support for Rance4 ver.2.05 - Now supports BGM playback for the download edition of Daiakuji - Bug fixes ## 2.6.0 - 2023-01-01 - Android: Introduced a new way to simulate right-click; tapping on the black bars at the left/right or top/bottom of the screen is treated as a right click. (Two-finger touch, the old way to simulate a right click, still works.) - Windows: Added a menu command to enable/disable automatic mouse movements. ## 2.5.1 - 2022-07-31 - Implemented "palette shift" graphic effect (used in 闘神都市II). - Implemented `grDrawFillCircle` command (used in グレイメルカ). - Fixed color update bug in 256-color games. - Fixed crash by out-of-bounds variable access in debugger. ## 2.5.0 - 2022-07-17 - Supported JPEG image format on Android. ## 2.4.0 - 2022-07-02 - Windows: Added "Restart" menu command. - Fixed a bug where wrong music is played in Kichikuou Rance (System3.9 version). #32 - Supported mouse wheel input. - Fixed a bug in `MF` command. - Implemented `grEffectMoveView` command. ## 2.3.0 - 2022-04-08 - Code for screen transition effects have been substantially rewritten (but you probably wouldn't notice any difference). - Fixed bugs in System3.9 games. - Android: Enabled joystick input. - Debugger improvements. ## 2.2.0 - 2021-12-03 From now on, the Android and Windows versions will use a common version number. - Android: Use custom fonts specified in `.xsys35c`. (#20) - Supported "王子さまLv1". - Debugger improvements. - Fixed some bugs and compatibility issues. ## 2.1.0 - 2021-11-07 - Fixed issues with MangaGamer version of Rance 5D. - Fixed garbled text in some System3.9 games with Unicode mode. - Display a message box on fatal errors. - Various debugger improvements. ## 2.0.1 - 2021-10-24 - Fixed crash during shutdown. ## 2.0.0 - 2021-10-23 - Added window menubar from which you can save screenshots, change fullscreen mode, and enable message skipping. - Added debugger support that can be used from Visual Studio Code. See [vscode-system3x](https://github.com/kichikuou/vscode-system3x) for details. - Many bug fixes and compatibility fixes. xsystem35-sdl2-2.14.3/CMakeLists.txt000066400000000000000000000161301477263212400171000ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.13) # Rebuild external projects when download URL changes if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0") cmake_policy(SET CMP0135 NEW) endif() project(xsystem35 LANGUAGES C) set(CMAKE_C_STANDARD 99) enable_testing() set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE) set(XSYSTEM35_VERSION "2.14.3") include(CheckSymbolExists) include(FetchContent) # A wrapper around pkg_check_modules() that skips the check if ENABLE_ is false macro (optional_pkg_check_modules prefix) option(ENABLE_${prefix} "Use ${prefix} if available" ON) if (ENABLE_${prefix}) pkg_check_modules(${prefix} ${ARGN}) endif() endmacro() # Generates a static library from pkg_check_modules() result function(add_static_library name pkg) add_library(${name} INTERFACE) set_target_properties(${name} PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${${pkg}_STATIC_INCLUDE_DIRS}" INTERFACE_COMPILE_OPTIONS "${${pkg}_STATIC_CFLAGS_OTHER}" INTERFACE_LINK_LIBRARIES "${${pkg}_STATIC_LIBRARIES}" INTERFACE_LINK_OPTIONS "${${pkg}_STATIC_LDFLAGS_OTHER}") endfunction() function(fetch_webp) FetchContent_Declare( libwebp URL https://storage.googleapis.com/downloads.webmproject.org/releases/webp/libwebp-1.5.0.tar.gz URL_HASH SHA1=b21aa842136dc59a72a38776a5aa73f4d0b00ac5 ) set(WEBP_BUILD_ANIM_UTILS OFF CACHE BOOL "Build animation utilities." FORCE) set(WEBP_BUILD_CWEBP OFF CACHE BOOL "Build the cwebp command line tool." FORCE) set(WEBP_BUILD_DWEBP OFF CACHE BOOL "Build the dwebp command line tool." FORCE) set(WEBP_BUILD_GIF2WEBP OFF CACHE BOOL "Build the gif2webp conversion tool." FORCE) set(WEBP_BUILD_IMG2WEBP OFF CACHE BOOL "Build the img2webp animation tool." FORCE) set(WEBP_BUILD_VWEBP OFF CACHE BOOL "Build the vwebp viewer tool." FORCE) set(WEBP_BUILD_WEBPINFO OFF CACHE BOOL "Build the webpinfo command line tool." FORCE) set(WEBP_BUILD_LIBWEBPMUX OFF CACHE BOOL "Build the libwebpmux library." FORCE) set(WEBP_BUILD_WEBPMUX OFF CACHE BOOL "Build the webpmux command line tool." FORCE) set(WEBP_BUILD_EXTRAS OFF CACHE BOOL "Build extras." FORCE) set(WEBP_BUILD_WEBP_JS OFF CACHE BOOL "Emscripten build of webp.js." FORCE) FetchContent_MakeAvailable(libwebp) add_library(WebP ALIAS webp) endfunction() check_symbol_exists(getlogin "unistd.h" HAVE_GETLOGIN) check_symbol_exists(mmap "sys/mman.h" HAVE_MMAP) check_symbol_exists(sigaction "signal.h" HAVE_SIGACTION) if (EMSCRIPTEN) function(add_emscripten_library name option) add_library(${name} INTERFACE) set_target_properties(${name} PROPERTIES INTERFACE_COMPILE_OPTIONS ${option} INTERFACE_LINK_OPTIONS ${option}) endfunction() add_emscripten_library(zlib -sUSE_ZLIB=1) add_emscripten_library(sdl2 -sUSE_SDL=2) add_emscripten_library(sdl2_ttf -sUSE_SDL_TTF=2) set(DEFAULT_FONT_PATH /fonts/) fetch_webp() set(HAVE_WEBP 1) elseif (ANDROID) add_library(sdl2 ALIAS SDL2) add_library(sdl2_ttf ALIAS SDL2_ttf) add_library(sdl2_mixer ALIAS SDL2_mixer) find_library(ndk_log log) find_library(ndk_zlib z) add_library(zlib INTERFACE) set_target_properties(zlib PROPERTIES INTERFACE_LINK_LIBRARIES ${ndk_zlib}) fetch_webp() set(HAVE_WEBP 1) else() # non-emscripten, non-android if (WIN32) set(ZLIB_USE_STATIC_LIBS ON) endif() find_package(ZLIB REQUIRED) add_library(zlib ALIAS ZLIB::ZLIB) include(FindPkgConfig) optional_pkg_check_modules(GTK3 IMPORTED_TARGET gtk+-3.0) if (GTK3_FOUND) set(ENABLE_GTK 1) endif() pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2) pkg_check_modules(SDL2TTF REQUIRED IMPORTED_TARGET SDL2_ttf) pkg_check_modules(SDL2MIXER REQUIRED IMPORTED_TARGET SDL2_mixer) if (WIN32) add_static_library(sdl2 SDL2) add_static_library(sdl2_ttf SDL2TTF) # Workaround for linking error set_property(TARGET sdl2_ttf PROPERTY INTERFACE_LINK_LIBRARIES $) add_static_library(sdl2_mixer SDL2MIXER) else() add_library(sdl2 ALIAS PkgConfig::SDL2) add_library(sdl2_ttf ALIAS PkgConfig::SDL2TTF) add_library(sdl2_mixer ALIAS PkgConfig::SDL2MIXER) set(DEFAULT_FONT_PATH ${CMAKE_INSTALL_PREFIX}/share/xsystem35/fonts/) endif() option(ENABLE_DEBUGGER "Enable built-in debugger" ON) if (ENABLE_DEBUGGER) pkg_check_modules(cJSON IMPORTED_TARGET libcjson) if (cJSON_FOUND) if (WIN32) add_static_library(cJSON cJSON) else () add_library(cJSON ALIAS PkgConfig::cJSON) endif() else () # libcjson-dev of Debian buster / Ubuntu 20.04 does not install pkgconfig files. find_library(cJSON cjson) if (NOT cJSON) message(FATAL_ERROR "libcjson is required but not found.") endif() add_library(cJSON ALIAS ${cJSON}) endif() endif() optional_pkg_check_modules(WEBP IMPORTED_TARGET libwebp) if (WEBP_FOUND) set(HAVE_WEBP 1) if (WIN32) add_static_library(WebP WEBP) else() add_library(WebP ALIAS PkgConfig::WEBP) endif() endif() if (WIN32) optional_pkg_check_modules(PORTMIDI IMPORTED_TARGET portmidi) if (PORTMIDI_FOUND) set(HAVE_PORTMIDI 1) add_static_library(portmidi_static PORTMIDI) set(PORTMIDI portmidi_static) endif() else () find_library(PORTMIDI portmidi) if (PORTMIDI) set(HAVE_PORTMIDI 1) endif() endif() endif() # Menu if (ENABLE_GTK) # i18n support (currently only menus are translated) include(FindIntl) include(FindGettext) if (Intl_FOUND AND GETTEXT_FOUND) set(ENABLE_NLS 1) add_compile_definitions(LOCALEDIR="${CMAKE_INSTALL_PREFIX}/share/locale") include_directories(${Intl_INCLUDE_DIRS}) link_libraries(${Intl_LIBRARIES}) endif() endif() # PCM audio if (EMSCRIPTEN) list(APPEND SUMMARY_AUDIO "Emscripten") else() list(APPEND SUMMARY_AUDIO "SDL_mixer") endif() # CDROM if (EMSCRIPTEN) list(APPEND SUMMARY_CDROM "Emscripten") else() list(APPEND SUMMARY_CDROM "SDL_mixer (wav|mp3|ogg...)") endif() set(DEFAULT_PLAYLIST_PATH "playlist.txt" CACHE STRING "Default playlist path") # MIDI if (EMSCRIPTEN) list(APPEND SUMMARY_MIDI "Emscripten") elseif (ANDROID) list(APPEND SUMMARY_MIDI "Android") else() list(APPEND SUMMARY_MIDI "SDL_mixer") set(ENABLE_MIDI_SDLMIXER 1) if (HAVE_PORTMIDI) list(APPEND SUMMARY_MIDI "PortMidi") set(ENABLE_MIDI_PORTMIDI 1) endif() endif() function (print_summary name) if (ARGN) list(JOIN ARGN ", " MSG) else() set(MSG "none") endif() message(" ${name}: ${MSG}") endfunction() message("xsystem35 summary:") message("------------------") print_summary(audio ${SUMMARY_AUDIO}) print_summary(cdrom ${SUMMARY_CDROM}) print_summary(midi ${SUMMARY_MIDI}) message("------------------") set(PACKAGE ${PROJECT_NAME}) configure_file(config.h.in config.h) include_directories(${CMAKE_CURRENT_BINARY_DIR}) add_compile_definitions($<$:DEBUG>) add_subdirectory(src) add_subdirectory(tools) add_subdirectory(fonts) if (NOT ANDROID AND NOT EMSCRIPTEN) add_subdirectory(doc) endif() if (ENABLE_NLS) add_subdirectory(po) endif() add_subdirectory(modules) target_link_libraries(xsystem35 PRIVATE modules) xsystem35-sdl2-2.14.3/COPYING000066400000000000000000000431271477263212400154010ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc. 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Library General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 19yy This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) 19yy name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Library General Public License instead of this License. xsystem35-sdl2-2.14.3/README.md000066400000000000000000000101551477263212400156200ustar00rootroot00000000000000# xsytem35-sdl2 This is a multi-platform port of `xsystem35`, a free implementation of AliceSoft's System 3.x game engine. ## Compatibility See the [game compatibility table](game_compatibility.md) for a list of games that can be played with xsystem35-sdl2. ## Unique Features In addition to the original System 3.x functionalities, xsystem35-sdl2 offers the following features: ### Playing Audio Files as Virtual CD Music Many System 3.x games feature music as audio tracks on the CD-ROM. xsystem35 can play music from audio files, eliminating the need to insert CDs. To use ripped audio files, create a file named `playlist.txt` in the game directory and list the paths to your tracks, one per line. For example: ``` # The first line is not used BGM/track02.mp3 BGM/track03.mp3 ... ``` The first line is not used because the first track on a game CD is typically a data track. Some games have integrated music as MIDI. In such cases, the music won't play using the virtual CD feature. If you encounter a `Cannot load MIDI` error message, you might need to set the `SDL_SOUNDFONTS` environment variable to point to an `.sf2` file. For example: ``` SDL_SOUNDFONTS=/usr/share/soundfonts/GeneralUser.sf2 xsystem35 ``` ### Unicode Translation Support While the original System 3.x only supported Shift_JIS (a Japanese character encoding), xsystem35 supports Unicode and can run games translated into languages other than Japanese and English. For instructions on how to build a game with Unicode support, see the [xsys35c](https://github.com/kichikuou/xsys35c) documentation. ### Debugging xsystem35 features a built-in debugger that allows you to step through the game and examine or modify game variables. There are two ways to use the debugger: - Through [Visual Studio Code](https://code.visualstudio.com/) (recommended): The [vscode-system3x](https://github.com/kichikuou/vscode-system3x) extension provides a graphical debugging interface for System 3.x. - Using the CLI Debugger: Running xsystem35 with the `-debug` option will launch the debugger with a console interface. Type `help` to see a list of available commands. ## Installation Prebuilt packages for Windows and Android can be downloaded from the [Releases](https://github.com/kichikuou/xsystem35-sdl2/releases) page. Note for Windows: - The 64-bit version supports Windows 10 or later. For older versions of Windows, please use the 32-bit version. - Debugging is supported only in the 64-bit version. For other platforms, refer to the [Building](#building) section. ## Running ### Windows Copy `xsystem35.exe` to the game folder and run it. ### Android See [android/README.md](android/README.md#usage). ### Other Platforms Run xsystem35 from within the game directory. ```bash $ cd /path/to/game_directory $ xsystem35 ``` See [xsystem35 command manual](doc/xsystem35.6.adoc) for detailed usage. ## Building ### Linux (Debian / Ubuntu) ```bash $ sudo apt install build-essential cmake libgtk-3-dev libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev libwebp-dev libportmidi-dev libcjson-dev asciidoctor $ mkdir -p out/debug $ cd out/debug $ cmake -DCMAKE_BUILD_TYPE=Debug ../../ $ make && make install ``` ### MacOS [Homebrew](https://brew.sh/) is required. ```bash $ brew install cmake pkg-config sdl2 sdl2_mixer sdl2_ttf webp portmidi cjson asciidoctor $ mkdir -p out/debug $ cd out/debug $ cmake -DCMAKE_BUILD_TYPE=Debug ../../ $ make && make install ``` ### Windows [MSYS2](https://www.msys2.org) is required. ```bash $ pacman -S cmake mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-SDL2 mingw-w64-ucrt-x86_64-SDL2_ttf mingw-w64-ucrt-x86_64-SDL2_mixer mingw-w64-ucrt-x86_64-libwebp mingw-w64-ucrt-x86_64-portmidi mingw-w64-ucrt-x86_64-cjson $ mkdir -p out/debug $ cd out/debug $ cmake -G"MSYS Makefiles" -DCMAKE_BUILD_TYPE=Debug ../../ $ make ``` ### Emscripten ```bash $ mkdir -p out/wasm $ cd out/wasm $ emcmake cmake -DCMAKE_BUILD_TYPE=MinSizeRel ../../ $ make ``` To use the generated binary, check out [Kichikuou on Web](https://github.com/kichikuou/web) and copy `out/xsystem35.*` into its `docs` directory. ### Android See [android/README.md](android/). xsystem35-sdl2-2.14.3/android/000077500000000000000000000000001477263212400157575ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/android/README.md000066400000000000000000000054551477263212400172470ustar00rootroot00000000000000# xsystem35 for Android Minimum supported Android version: 5.0 ## Download You can download prebuilt APKs [here](https://github.com/kichikuou/xsystem35-sdl2/releases). ## Build ### Using Android Studio Open this directory as an Android Studio project. ### Command Line Build Set environment variables and run the `gradlew` script in this directory. Example build instructions (for Debian bookworm): ```sh # Install necessary packages sudo apt install git wget unzip default-jdk-headless ninja-build # Install Android SDK / NDK export ANDROID_SDK_ROOT=$HOME/android-sdk mkdir -p $ANDROID_SDK_ROOT/cmdline-tools wget https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip unzip commandlinetools-linux-11076708_latest.zip -d $ANDROID_SDK_ROOT/cmdline-tools mv $ANDROID_SDK_ROOT/cmdline-tools/cmdline-tools $ANDROID_SDK_ROOT/cmdline-tools/tools yes | $ANDROID_SDK_ROOT/cmdline-tools/tools/bin/sdkmanager --licenses $ANDROID_SDK_ROOT/cmdline-tools/tools/bin/sdkmanager ndk-bundle 'cmake;3.22.1' export ANDROID_NDK_HOME=$ANDROID_SDK_ROOT/ndk-bundle # Clone and build xsystem35 git clone https://github.com/kichikuou/xsystem35-sdl2.git cd xsystem35-sdl2/android ./gradlew build # or ./gradlew installDebug if you have a connected device ``` ## Usage ### Basic Usage 1. Create a ZIP file containing all the game files and BGM files (see [below](#preparing-a-zip) for details), and transfer it to your device. 2. Open the app. A list of installed games will be displayed. Since no games have been installed yet, only the "Install from ZIP" button will be visible. Tap it. 3. Select the ZIP file you created in step 1. 4. The game will start. To simulate a right-click, tap the black bars on either the left or right, or top or bottom of the screen. ### Preparing a ZIP - Include all files from the `GAMEDATA` folder (such as `.ALD` files and others). `.EXE` and `.DLL` files are not necessary, but you can include them if you want. - Music files (`.mp3`, `.ogg`, or `.wav`) whose filenames end with a number will be recognized as BGM files. For example: - `Track2.mp3` - `15.ogg` - `rance4_03.wav` (Note: The filename shouldn't be `rance403.wav`, as it would be treated as the 403rd track.) Note: This ZIP format is also compatible with [Kichikuou on Web](http://kichikuou.github.io/web/). ### Miscellaneous - You can export or import save files via the game list's option menu. - To uninstall a game, long-tap its title in the game list. ## Known Issues - Android versions older than 7.0 cannot handle ZIP files containing Shift-JIS filenames. This issue occurs with some ZIP files distributed on [retroc.net](http://retropc.net/alice/). If you encounter the error message "This type of ZIP is not supported," unzip the file on your PC and re-archive it using modern ZIP creation software. xsystem35-sdl2-2.14.3/android/app/000077500000000000000000000000001477263212400165375ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/android/app/build.gradle000066400000000000000000000054351477263212400210250ustar00rootroot00000000000000def buildAsLibrary = project.hasProperty('BUILD_AS_LIBRARY') def buildAsApplication = !buildAsLibrary if (buildAsApplication) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' } apply plugin: 'kotlin-android' android { if (buildAsApplication) { namespace "io.github.kichikuou.xsystem35" } compileSdkVersion 34 defaultConfig { minSdkVersion 21 targetSdkVersion 34 versionCode 32 versionName "2.14.3" + gitRevision() externalNativeBuild { cmake { arguments "-DANDROID_APP_PLATFORM=android-19", "-DANDROID_STL=c++_static" // abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' abiFilters 'armeabi-v7a', 'arm64-v8a' } } } signingConfigs { release { storeFile rootProject.file('keystore.jks') storePassword System.getenv('KEYSTORE_PASSWORD') keyAlias System.getenv('KEY_ALIAS') keyPassword System.getenv('KEY_PASSWORD') } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' if (rootProject.file('keystore.jks').exists()) { signingConfig signingConfigs.release } } } applicationVariants.all { variant -> tasks["merge${variant.name.capitalize()}Assets"] .dependsOn("externalNativeBuild${variant.name.capitalize()}") } if (!project.hasProperty('EXCLUDE_NATIVE_LIBS')) { sourceSets.main { jniLibs.srcDir 'libs' } aaptOptions { // Disable asset compression for fonts. // Without this, text rendering will be very slow. noCompress 'ttf', 'otf' } externalNativeBuild { cmake { version '3.22.1' path 'jni/CMakeLists.txt' } } } lint { abortOnError false } if (buildAsLibrary) { libraryVariants.all { variant -> variant.outputs.each { output -> def outputFile = output.outputFile if (outputFile != null && outputFile.name.endsWith(".aar")) { def fileName = "org.libsdl.app.aar" output.outputFile = new File(outputFile.parent, fileName) } } } } } dependencies { implementation fileTree(include: ['*.jar'], dir: 'libs') implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' } static String gitRevision() { ' (git ' + "git rev-parse --short HEAD".execute().text.trim() + ')' } xsystem35-sdl2-2.14.3/android/app/jni/000077500000000000000000000000001477263212400173175ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/android/app/jni/Android.mk000066400000000000000000000000451477263212400212270ustar00rootroot00000000000000include $(call all-subdir-makefiles) xsystem35-sdl2-2.14.3/android/app/jni/Application.mk000066400000000000000000000004111477263212400221070ustar00rootroot00000000000000 # Uncomment this if you're using STL in your project # You can find more information here: # https://developer.android.com/ndk/guides/cpp-support # APP_STL := c++_shared APP_ABI := armeabi-v7a arm64-v8a x86 x86_64 # Min runtime API level APP_PLATFORM=android-16 xsystem35-sdl2-2.14.3/android/app/jni/CMakeLists.txt000066400000000000000000000047731477263212400220720ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.14) project(GAME) set(PROJECT_ROOT_DIR ../../..) # Compilation of SDL and companion libraries include(FetchContent) FetchContent_Declare( SDL URL https://github.com/libsdl-org/SDL/releases/download/release-2.32.4/SDL2-2.32.4.tar.gz URL_HASH SHA1=4fb68f43891ca4def414cc7cf0f3b2a053cb6aaf ) FetchContent_Declare( SDL_ttf URL https://github.com/libsdl-org/SDL_ttf/releases/download/release-2.22.0/SDL2_ttf-2.22.0.tar.gz URL_HASH SHA1=da5e86b601ad299a697878fab1af6f3be47b529d ) FetchContent_Declare( SDL_mixer URL https://github.com/libsdl-org/SDL_mixer/releases/download/release-2.8.0/SDL2_mixer-2.8.0.tar.gz URL_HASH SHA1=a58c69f9d00e44833b9e00e1adb58d85759ca499 ) set(SDL2TTF_SAMPLES OFF CACHE BOOL "Build the SDL2_ttf sample program(s)" FORCE) set(SDL2TTF_INSTALL OFF CACHE BOOL "Enable SDL2_ttf install target" FORCE) set(SDL2TTF_VENDORED ON CACHE BOOL "Use vendored third-party libraries" FORCE) set(SDL2MIXER_OPUS OFF CACHE BOOL "Enable Opus music" FORCE) set(SDL2MIXER_FLAC OFF CACHE BOOL "Enable FLAC music" FORCE) set(SDL2MIXER_MOD OFF CACHE BOOL "Support loading MOD music" FORCE) set(SDL2MIXER_MIDI OFF CACHE BOOL "Enable MIDI music" FORCE) set(SDL2MIXER_WAVPACK OFF CACHE BOOL "Enable WavPack music" FORCE) set(SDL2MIXER_SAMPLES OFF CACHE BOOL "Build the SDL2_mixer sample program(s)" FORCE) set(SDL2MIXER_INSTALL OFF CACHE BOOL "Enable SDL2_mixer install target" FORCE) FetchContent_MakeAvailable(SDL SDL_ttf SDL_mixer) # The main CMakeLists.txt of xsystem35 add_subdirectory(${PROJECT_ROOT_DIR} xsystem35) # Copy asset files set(ASSETS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../src/main/assets) file(MAKE_DIRECTORY ${ASSETS_DIR}/licenses) file(COPY_FILE ${PROJECT_ROOT_DIR}/fonts/MTLc3m.ttf ${ASSETS_DIR}/MTLc3m.ttf) file(COPY_FILE ${PROJECT_ROOT_DIR}/fonts/mincho.otf ${ASSETS_DIR}/mincho.otf) file(COPY_FILE ${PROJECT_ROOT_DIR}/COPYING ${ASSETS_DIR}/licenses/xsystem35) file(COPY_FILE ${PROJECT_ROOT_DIR}/fonts/MTLc3m.ttf.license ${ASSETS_DIR}/licenses/MTLc3m) file(COPY_FILE ${PROJECT_ROOT_DIR}/fonts/mincho.otf.license ${ASSETS_DIR}/licenses/mincho) file(COPY_FILE ${sdl_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL) file(COPY_FILE ${sdl_ttf_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL_ttf) file(COPY_FILE ${sdl_ttf_SOURCE_DIR}/external/freetype/docs/GPLv2.TXT ${ASSETS_DIR}/licenses/freetype) file(COPY_FILE ${sdl_ttf_SOURCE_DIR}/external/harfbuzz/COPYING ${ASSETS_DIR}/licenses/harfbuzz) file(COPY_FILE ${sdl_mixer_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL_mixer) xsystem35-sdl2-2.14.3/android/app/jni/src/000077500000000000000000000000001477263212400201065ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/android/app/jni/src/Android.mk000066400000000000000000000005501477263212400220170ustar00rootroot00000000000000LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE := main SDL_PATH := ../SDL LOCAL_C_INCLUDES := $(LOCAL_PATH)/$(SDL_PATH)/include # Add your application source files here... LOCAL_SRC_FILES := YourSourceHere.c LOCAL_SHARED_LIBRARIES := SDL2 LOCAL_LDLIBS := -lGLESv1_CM -lGLESv2 -lOpenSLES -llog -landroid include $(BUILD_SHARED_LIBRARY) xsystem35-sdl2-2.14.3/android/app/jni/src/CMakeLists.txt000066400000000000000000000002731477263212400226500ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.6) project(MY_APP) find_library(SDL2 SDL2) add_library(main SHARED) target_sources(main PRIVATE YourSourceHere.c) target_link_libraries(main SDL2) xsystem35-sdl2-2.14.3/android/app/proguard-rules.pro000066400000000000000000000074041477263212400222410ustar00rootroot00000000000000# Add project specific ProGuard rules here. # By default, the flags in this file are appended to flags specified # in [sdk]/tools/proguard/proguard-android.txt # You can edit the include path and order by changing the proguardFiles # directive in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # Add any project specific keep options here: # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} -keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLInputConnection { void nativeCommitText(java.lang.String, int); void nativeGenerateScancodeForUnichar(char); } -keep,includedescriptorclasses class org.libsdl.app.SDLActivity { # for some reason these aren't compatible with allowoptimization modifier boolean supportsRelativeMouse(); void setWindowStyle(boolean); } -keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLActivity { java.lang.String nativeGetHint(java.lang.String); # Java-side doesn't use this, so it gets minified, but C-side still tries to register it boolean onNativeSoftReturnKey(); void onNativeKeyboardFocusLost(); boolean isScreenKeyboardShown(); android.util.DisplayMetrics getDisplayDPI(); java.lang.String clipboardGetText(); boolean clipboardHasText(); void clipboardSetText(java.lang.String); int createCustomCursor(int[], int, int, int, int); void destroyCustomCursor(int); android.content.Context getContext(); boolean getManifestEnvironmentVariables(); android.view.Surface getNativeSurface(); void initTouch(); boolean isAndroidTV(); boolean isChromebook(); boolean isDeXMode(); boolean isTablet(); void manualBackButton(); int messageboxShowMessageBox(int, java.lang.String, java.lang.String, int[], int[], java.lang.String[], int[]); void minimizeWindow(); int openURL(java.lang.String); void requestPermission(java.lang.String, int); int showToast(java.lang.String, int, int, int, int); boolean sendMessage(int, int); boolean setActivityTitle(java.lang.String); boolean setCustomCursor(int); void setOrientation(int, int, boolean, java.lang.String); boolean setRelativeMouseEnabled(boolean); boolean setSystemCursor(int); boolean shouldMinimizeOnFocusLoss(); boolean showTextInput(int, int, int, int); } -keep,includedescriptorclasses,allowoptimization class org.libsdl.app.HIDDeviceManager { boolean initialize(boolean, boolean); boolean openDevice(int); int sendOutputReport(int, byte[]); int sendFeatureReport(int, byte[]); boolean getFeatureReport(int, byte[]); void closeDevice(int); } -keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLAudioManager { int[] getAudioOutputDevices(); int[] getAudioInputDevices(); int[] audioOpen(int, int, int, int, int); void audioWriteFloatBuffer(float[]); void audioWriteShortBuffer(short[]); void audioWriteByteBuffer(byte[]); void audioClose(); int[] captureOpen(int, int, int, int, int); int captureReadFloatBuffer(float[], boolean); int captureReadShortBuffer(short[], boolean); int captureReadByteBuffer(byte[], boolean); void captureClose(); void audioSetThreadPriority(boolean, int); native int nativeSetupJNI(); native void removeAudioDevice(boolean, int); native void addAudioDevice(boolean, int); } -keep,includedescriptorclasses,allowoptimization class org.libsdl.app.SDLControllerManager { void pollInputDevices(); void pollHapticDevices(); void hapticRun(int, float, int); void hapticStop(int); } xsystem35-sdl2-2.14.3/android/app/src/000077500000000000000000000000001477263212400173265ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/android/app/src/main/000077500000000000000000000000001477263212400202525ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/android/app/src/main/AndroidManifest.xml000066400000000000000000000102221477263212400240400ustar00rootroot00000000000000 xsystem35-sdl2-2.14.3/android/app/src/main/java/000077500000000000000000000000001477263212400211735ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/android/app/src/main/java/io/000077500000000000000000000000001477263212400216025ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/android/app/src/main/java/io/github/000077500000000000000000000000001477263212400230645ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/android/app/src/main/java/io/github/kichikuou/000077500000000000000000000000001477263212400250575ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/android/app/src/main/java/io/github/kichikuou/xsystem35/000077500000000000000000000000001477263212400267435ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/android/app/src/main/java/io/github/kichikuou/xsystem35/GameActivity.kt000066400000000000000000000150441477263212400316750ustar00rootroot00000000000000/* Copyright (C) 2019 * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ package io.github.kichikuou.xsystem35 import android.app.AlertDialog import android.media.MediaPlayer import android.os.Bundle import android.text.InputType import android.util.Log import android.widget.EditText import android.widget.NumberPicker import org.libsdl.app.SDLActivity import java.io.File import java.io.IOException // Intent for this activity must have the following extra: // - EXTRA_GAME_ROOT (string): A path to the game installation. // - EXTRA_SAVE_DIRECTORY (string): A path to the save data directory. class GameActivity : SDLActivity() { companion object { const val EXTRA_GAME_ROOT = "GAME_ROOT" const val EXTRA_SAVE_DIRECTORY = "SAVE_DIRECTORY" const val EXTRA_ARCHIVE_NAME = "ARCHIVE_NAME" } private lateinit var gameRoot: File private val midi = MidiPlayer() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) gameRoot = File(intent.getStringExtra(EXTRA_GAME_ROOT)!!) } override fun onStop() { super.onStop() midi.onActivityStop() } override fun onResume() { super.onResume() midi.onActivityResume() } override fun getLibraries(): Array { return arrayOf("SDL2", "xsystem35") } override fun getArguments(): Array { return arrayOf( "-gamedir", intent.getStringExtra(EXTRA_GAME_ROOT)!!, "-savedir", intent.getStringExtra(EXTRA_SAVE_DIRECTORY)!!) } override fun setTitle(title: CharSequence?) { super.setTitle(title) var str = title?.toString()?.substringAfter(':', "") if (str.isNullOrEmpty()) { str = intent.getStringExtra(EXTRA_ARCHIVE_NAME) if (str == null) return } File(gameRoot, Launcher.TITLE_FILE).writeText(str) Launcher.updateGameList() } private fun textInputDialog(msg: String, oldVal: String, maxLen: Int, result: Array) { val input = EditText(this) input.inputType = InputType.TYPE_CLASS_TEXT input.setText(oldVal) AlertDialog.Builder(this) .setMessage(msg) .setView(input) .setPositiveButton(R.string.ok) {_, _ -> val s = input.text.toString() result[0] = if (s.length <= maxLen) s else s.substring(0, maxLen) } .setNegativeButton(R.string.cancel) {_, _ -> } .setOnDismissListener { synchronized(result) { @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") (result as Object).notify() } } .show() } private fun numberInputDialog(msg: String, min: Int, max: Int, initial: Int, result: IntArray) { val input = NumberPicker(this) input.minValue = min input.maxValue = max input.value = initial AlertDialog.Builder(this) .setMessage(msg) .setView(input) .setPositiveButton(R.string.ok) {_, _ -> input.clearFocus() result[0] = input.value } .setNegativeButton(R.string.cancel) {_, _ -> } .setOnDismissListener { synchronized(result) { @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") (result as Object).notify() } } .show() } // The functions below are called in the SDL thread by JNI. @Suppress("unused") fun midiStart(path: String, loop: Boolean) = midi.start(path, loop) @Suppress("unused") fun midiStop() = midi.stop() @Suppress("unused") fun midiCurrentPosition() = midi.currentPosition() @Suppress("unused") fun inputString(msg: String, oldVal: String, maxLen: Int): String? { val result = arrayOfNulls(1) runOnUiThread { textInputDialog(msg, oldVal, maxLen, result) } // Block the calling thread. synchronized(result) { try { @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") (result as Object).wait() } catch (ex: InterruptedException) { ex.printStackTrace() } } return result[0] } @Suppress("unused") fun inputNumber(msg: String, min: Int, max: Int, initial: Int): Int { val result = intArrayOf(-1) runOnUiThread { numberInputDialog(msg, min, max, initial, result) } // Block the calling thread. synchronized(result) { try { @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") (result as Object).wait() } catch (ex: InterruptedException) { ex.printStackTrace() } } return result[0] } } private class MidiPlayer { private val player = MediaPlayer() private var playing = false private var playerPaused = false fun start(path: String, loop: Boolean) { try { player.apply { reset() setDataSource(path) isLooping = loop prepare() start() } playing = true } catch (e: IOException) { Log.e("midiStart", "Cannot play midi", e) player.reset() } } fun stop() { if (playing && player.isPlaying) { player.stop() playing = false } } fun currentPosition(): Int { return if (playing) player.currentPosition else 0 } fun onActivityStop() { if (playing && player.isPlaying) { player.pause() playerPaused = true } } fun onActivityResume() { if (playerPaused) { player.start() playerPaused = false } } } xsystem35-sdl2-2.14.3/android/app/src/main/java/io/github/kichikuou/xsystem35/Launcher.kt000066400000000000000000000234371477263212400310550ustar00rootroot00000000000000/* Copyright (C) 2020 * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ package io.github.kichikuou.xsystem35 import android.os.Build import android.util.Log import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.* import java.nio.charset.Charset import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream private var gLauncher: Launcher? = null interface LauncherObserver { fun onGameListChange() fun onInstallProgress(path: String) fun onInstallSuccess(path: File, archiveName: String?) fun onInstallFailure(msgId: Int) } class Launcher private constructor(private val rootDir: File) { companion object { const val SAVE_DIR = "save" const val TITLE_FILE = "title.txt" const val GAMEDIR_FILE = "game_directory.txt" const val PLAYLIST_FILE = "playlist.txt" const val OLD_PLAYLIST_FILE = "playlist2.txt" fun getInstance(rootDir: File): Launcher { if (gLauncher == null) { gLauncher = Launcher(rootDir) } return gLauncher!! } fun updateGameList() { gLauncher?.updateGameList() } } data class Entry(val path: File, val title: String, val timestamp: Long) val games = arrayListOf() val titles: List get() = games.map(Entry::title) var observer: LauncherObserver? = null var isInstalling = false private set init { updateGameList() } @OptIn(DelicateCoroutinesApi::class) fun install(input: InputStream, archiveName: String?) { val dir = createDirForGame() isInstalling = true GlobalScope.launch(Dispatchers.Main) { try { val gameDir = withContext(Dispatchers.IO) { extractFiles(input, dir) { msg -> GlobalScope.launch(Dispatchers.Main) { observer?.onInstallProgress(msg) } } } observer?.onInstallSuccess(gameDir, archiveName) } catch (e: InstallFailureException) { observer?.onInstallFailure(e.msgId) } catch (e: Exception) { Log.e("launcher", "Failed to extract ZIP", e) observer?.onInstallFailure(R.string.zip_extraction_error) } isInstalling = false } } fun uninstall(id: Int) { games[id].path.deleteRecursively() games.removeAt(id) observer?.onGameListChange() } private fun updateGameList() { var saveDirFound = false games.clear() for (path in rootDir.listFiles() ?: emptyArray()) { if (!path.isDirectory) continue if (path.name == SAVE_DIR) { saveDirFound = true continue } try { val gameDirFile = File(path, GAMEDIR_FILE) val gamePath = if (gameDirFile.exists()) File(path, gameDirFile.readText()) else path val titleFile = File(gamePath, TITLE_FILE) val title = titleFile.readText() games.add(Entry(gamePath, title, titleFile.lastModified())) migratePlaylist(path) } catch (e: IOException) { // Incomplete game installation. Delete it. path.deleteRecursively() } } games.sortByDescending(Entry::timestamp) if (!saveDirFound) { File(rootDir, SAVE_DIR).mkdir() } } private fun createDirForGame(): File { var i = 0 while (true) { val f = File(rootDir, i++.toString()) if (!f.exists() && f.mkdir()) { return f } } } // Throws IOException fun exportSaveData(output: OutputStream) { ZipOutputStream(output.buffered()).use { zip -> for (path in File(rootDir, SAVE_DIR).listFiles() ?: emptyArray()) { if (path.isDirectory || path.name.endsWith(".asd.")) continue val pathInZip = "${SAVE_DIR}/${path.name}" Log.i("exportSaveData", pathInZip) zip.putNextEntry(ZipEntry(pathInZip)) path.inputStream().buffered().use { it.copyTo(zip) } } } } fun importSaveData(input: InputStream): Int? { try { var imported = false forEachZipEntry(input) { zipEntry, zip -> // Process only files directly under save/ if (zipEntry.isDirectory || !zipEntry.name.startsWith("save/") || zipEntry.name.count{it == '/'} != 1) return@forEachZipEntry Log.i("importSaveData", zipEntry.name) FileOutputStream(File(rootDir, zipEntry.name)).buffered().use { zip.copyTo(it) } imported = true } return if (imported) null else R.string.no_data_to_import } catch (e: UTFDataFormatException) { // Attempted to read Shift_JIS zip in Android < 7 return R.string.unsupported_zip } catch (e: IOException) { Log.e("launcher", "Failed to extract ZIP", e) return R.string.zip_extraction_error } } private fun extractFiles(input: InputStream, outDir: File, progressCallback: (String) -> Unit): File { val configWriter = GameConfigWriter() val hadDecodeError = forEachZipEntry(input) { zipEntry, zip -> Log.i("extractFiles", zipEntry.name) val path = File(outDir, zipEntry.name) if (zipEntry.isDirectory) return@forEachZipEntry path.parentFile?.mkdirs() progressCallback(zipEntry.name) FileOutputStream(path).buffered().use { zip.copyTo(it) } configWriter.maybeAdd(zipEntry.name) } if (!configWriter.ready) { if (hadDecodeError) throw InstallFailureException(R.string.unsupported_zip) throw InstallFailureException(R.string.cannot_find_ald) } configWriter.write(outDir) return configWriter.gameDir?.let { File(outDir, it) } ?: outDir } // Xsystem35-sdl2 2.3.0 - 2.11.1 used playlist2.txt. Rename it to playlist.txt. private fun migratePlaylist(dir: File) { val oldPlaylist = File(dir, OLD_PLAYLIST_FILE) if (oldPlaylist.exists()) { oldPlaylist.renameTo(File(dir, PLAYLIST_FILE)) } } class InstallFailureException(val msgId: Int) : Exception() // A helper class which generates GAMEDIR_FILE and PLAYLIST_FILE. private class GameConfigWriter { var ready = false private set var gameDir: String? = null private set private val aldRegex = """.*?s[a-z]\.ald""".toRegex(RegexOption.IGNORE_CASE) private val audioRegex = """.*?(\d+)\.(wav|mp3|ogg)""".toRegex(RegexOption.IGNORE_CASE) private val audioFiles: Array = arrayOfNulls(100) fun maybeAdd(path: String) { aldRegex.matchEntire(path)?.let { gameDir = File(path).parent ready = true } audioRegex.matchEntire(path)?.let { val track = it.groupValues[1].toInt() if (0 < track && track <= audioFiles.size) audioFiles[track - 1] = path } } fun write(outDir: File) { // Generate GAMEDIR_FILE gameDir?.let { File(outDir, GAMEDIR_FILE).writeText(it) } // Generate PLAYLIST_FILE val absGameDir = gameDir?.let { File(outDir, it) } ?: outDir val playlistFile = File(absGameDir, PLAYLIST_FILE) if (!playlistFile.exists()) { val prefixToRemove = gameDir?.let { "$it/" } ?: "" val playlist = audioFiles.joinToString("\n") { it?.removePrefix(prefixToRemove) ?: "" }.trimEnd('\n') playlistFile.writeText(playlist) } } } } private fun forEachZipEntry(input: InputStream, action: (ZipEntry, ZipInputStream) -> Unit): Boolean { val zip = if (Build.VERSION.SDK_INT >= 24) { ZipInputStream(input.buffered(), Charset.forName("Shift_JIS")) } else { ZipInputStream(input.buffered()) } var hadDecodeError = false zip.use { while (true) { try { val zipEntry = zip.nextEntry ?: break action(zipEntry, zip) } catch (e: UTFDataFormatException) { // Attempted to read Shift_JIS zip in Android < 7 Log.w("forEachZipEntry", "UTFDataFormatException: skipping a zip entry") zip.closeEntry() hadDecodeError = true } } } return hadDecodeError } xsystem35-sdl2-2.14.3/android/app/src/main/java/io/github/kichikuou/xsystem35/LauncherActivity.kt000066400000000000000000000203571477263212400325700ustar00rootroot00000000000000/* Copyright (C) 2019 * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * */ package io.github.kichikuou.xsystem35 import android.app.* import android.content.Intent import android.net.Uri import android.os.Bundle import android.provider.OpenableColumns import android.util.Log import android.view.Menu import android.view.MenuItem import android.widget.ArrayAdapter import android.widget.ListView import android.widget.TextView import android.widget.Toast import java.io.* private const val CONTENT_TYPE_ZIP = "application/zip" private const val INSTALL_REQUEST = 1 private const val SAVEDATA_EXPORT_REQUEST = 2 private const val SAVEDATA_IMPORT_REQUEST = 3 private const val STATE_PROGRESS_TEXT = "progressText" class LauncherActivity : Activity(), LauncherObserver { private lateinit var launcher: Launcher private var progressDialog: Dialog? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.launcher) launcher = Launcher.getInstance(filesDir) launcher.observer = this if (launcher.isInstalling) { showProgressDialog(savedInstanceState) } onGameListChange() val listView = findViewById(R.id.list) listView.setOnItemClickListener { _, _, position, _ -> onListItemClick(position) } listView.setOnItemLongClickListener { _, _, position, _ -> onItemLongClick(position) } } override fun onDestroy() { launcher.observer = null dismissProgressDialog() super.onDestroy() } override fun onSaveInstanceState(outState: Bundle) { progressDialog?.let { outState.putCharSequence(STATE_PROGRESS_TEXT, it.findViewById(R.id.text).text) } super.onSaveInstanceState(outState) } private fun onListItemClick(position: Int) { if (position < launcher.games.size) { startGame(launcher.games[position].path, null) } else { val i = Intent(Intent.ACTION_GET_CONTENT) i.type = CONTENT_TYPE_ZIP startActivityForResult(Intent.createChooser(i, getString(R.string.choose_a_file)), INSTALL_REQUEST) } } private fun onItemLongClick(position: Int): Boolean { if (position < launcher.games.size) { AlertDialog.Builder(this).setTitle(R.string.uninstall_dialog_title) .setMessage(getString(R.string.uninstall_dialog_message, launcher.games[position].title)) .setPositiveButton(R.string.ok) {_, _ -> uninstall(position)} .setNegativeButton(R.string.cancel) {_, _ -> } .show() } return true } override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.launcher_menu, menu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { R.id.export_savedata -> { val i = Intent(Intent.ACTION_CREATE_DOCUMENT) i.type = CONTENT_TYPE_ZIP i.putExtra(Intent.EXTRA_TITLE, "savedata.zip") startActivityForResult(i, SAVEDATA_EXPORT_REQUEST) true } R.id.import_savedata -> { val i = Intent(Intent.ACTION_GET_CONTENT) i.type = CONTENT_TYPE_ZIP startActivityForResult(Intent.createChooser(i, getString(R.string.choose_a_file)), SAVEDATA_IMPORT_REQUEST) true } R.id.licenses -> { val intent = Intent(this, LicensesMenuActivity::class.java) startActivity(intent) true } else -> super.onOptionsItemSelected(item) } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { if (resultCode != RESULT_OK) return val uri = data?.data ?: return when (requestCode) { INSTALL_REQUEST -> { val input = contentResolver.openInputStream(uri) ?: return showProgressDialog() launcher.install(input, getArchiveName(uri)) } SAVEDATA_EXPORT_REQUEST -> try { launcher.exportSaveData(contentResolver.openOutputStream(uri)!!) Toast.makeText(this, R.string.save_data_export_success, Toast.LENGTH_SHORT).show() } catch (e: IOException) { Log.e("launcher", "Failed to export savedata", e) errorDialog(R.string.save_data_export_error) } SAVEDATA_IMPORT_REQUEST -> { val input = contentResolver.openInputStream(uri) ?: return val errMsgId = launcher.importSaveData(input) if (errMsgId == null) { Toast.makeText(this, R.string.save_data_import_success, Toast.LENGTH_SHORT).show() } else { errorDialog(errMsgId) } } } } override fun onGameListChange() { val items = launcher.titles.toMutableList() items.add(getString(R.string.install_from_zip)) val listView = findViewById(R.id.list) listView.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, items) } override fun onInstallProgress(path: String) { progressDialog?.findViewById(R.id.text)?.text = getString(R.string.install_progress, path) } override fun onInstallSuccess(path: File, archiveName: String?) { dismissProgressDialog() startGame(path, archiveName) } override fun onInstallFailure(msgId: Int) { dismissProgressDialog() errorDialog(msgId) } private fun startGame(path: File, archiveName: String?) { val i = Intent() i.setClass(applicationContext, GameActivity::class.java) i.putExtra(GameActivity.EXTRA_GAME_ROOT, path.path) i.putExtra(GameActivity.EXTRA_SAVE_DIRECTORY, File(filesDir, Launcher.SAVE_DIR).path) i.putExtra(GameActivity.EXTRA_ARCHIVE_NAME, archiveName) startActivity(i) } private fun uninstall(id: Int) { launcher.uninstall(id) } private fun showProgressDialog(savedInstanceState: Bundle? = null) { progressDialog = Dialog(this) progressDialog!!.apply { setTitle(R.string.install_dialog_title) setCancelable(false) setContentView(R.layout.progress_dialog) savedInstanceState?.let { findViewById(R.id.text)?.text = it.getCharSequence(STATE_PROGRESS_TEXT) } show() } } private fun dismissProgressDialog() { progressDialog?.dismiss() progressDialog = null } private fun errorDialog(msgId: Int) { AlertDialog.Builder(this).setTitle(R.string.error_dialog_title) .setMessage(msgId) .setPositiveButton(R.string.ok) {_, _ -> } .show() } private fun getArchiveName(uri: Uri): String? { val cursor = contentResolver.query(uri, null, null, null, null, null) cursor?.use { if (it.moveToFirst()) { val column = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) if (column >= 0) { val fname = it.getString(column) return if (fname.endsWith(".zip", true)) fname.dropLast(4) else fname } } } return null } } xsystem35-sdl2-2.14.3/android/app/src/main/java/io/github/kichikuou/xsystem35/LicensesActivity.kt000066400000000000000000000014021477263212400325620ustar00rootroot00000000000000package io.github.kichikuou.xsystem35 import android.app.Activity import android.os.Bundle import android.widget.TextView import java.io.BufferedReader class LicensesActivity : Activity() { companion object { const val EXTRA_DISPLAY_NAME = "DISPLAY_NAME" const val EXTRA_FILE_NAME = "FILE_NAME" } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_licenses) actionBar?.title = intent.getStringExtra(EXTRA_DISPLAY_NAME) val path = "licenses/" + intent.getStringExtra(EXTRA_FILE_NAME) val text = assets.open(path).bufferedReader().use(BufferedReader::readText) findViewById(R.id.license_text).text = text } }LicensesMenuActivity.kt000066400000000000000000000035611477263212400333400ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/android/app/src/main/java/io/github/kichikuou/xsystem35package io.github.kichikuou.xsystem35 import android.app.Activity import android.content.Intent import android.os.Bundle import android.widget.ListView import android.widget.SimpleAdapter class LicensesMenuActivity : Activity() { class Entry(val displayName: String, val fileName: String, val url: String) private val entries: ArrayList = arrayListOf( Entry("xsystem35-sdl2", "xsystem35", "https://github.com/kichikuou/xsystem35-sdl2"), Entry("SDL", "SDL", "https://www.libsdl.org/"), Entry("SDL_mixer", "SDL_mixer", "https://github.com/libsdl-org/SDL_mixer"), Entry("SDL_ttf", "SDL_ttf", "https://github.com/libsdl-org/SDL_ttf"), Entry("FreeType", "freetype", "https://freetype.org/"), Entry("HarfBuzz", "harfbuzz", "https://harfbuzz.github.io/"), Entry("MotoyaLCedar W3 mono", "MTLc3m", "https://github.com/aosp-mirror/platform_frameworks_base/tree/lollipop-release/data/fonts"), Entry("Source Han Serif", "mincho", "https://github.com/adobe-fonts/source-han-serif/"), ) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_licenses_menu) val items = entries.map { mapOf("name" to it.displayName, "url" to it.url)} val listView = findViewById(R.id.list) listView.adapter = SimpleAdapter(this, items, android.R.layout.simple_list_item_2, arrayOf("name", "url"), intArrayOf(android.R.id.text1, android.R.id.text2)) listView.setOnItemClickListener { _, _, pos, _ -> val intent = Intent(this, LicensesActivity::class.java).apply { putExtra(LicensesActivity.EXTRA_DISPLAY_NAME, entries[pos].displayName) putExtra(LicensesActivity.EXTRA_FILE_NAME, entries[pos].fileName) } startActivity(intent) } } }xsystem35-sdl2-2.14.3/android/app/src/main/java/org/000077500000000000000000000000001477263212400217625ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/android/app/src/main/java/org/libsdl/000077500000000000000000000000001477263212400232335ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/android/app/src/main/java/org/libsdl/app/000077500000000000000000000000001477263212400240135ustar00rootroot00000000000000xsystem35-sdl2-2.14.3/android/app/src/main/java/org/libsdl/app/HIDDevice.java000066400000000000000000000011571477263212400264060ustar00rootroot00000000000000package 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 sendFeatureReport(byte[] report); public int sendOutputReport(byte[] report); public boolean getFeatureReport(byte[] report); public void setFrozen(boolean frozen); public void close(); public void shutdown(); } xsystem35-sdl2-2.14.3/android/app/src/main/java/org/libsdl/app/HIDDeviceBLESteamController.java000066400000000000000000000571501477263212400317730ustar00rootroot00000000000000package 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 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 public final UUID steamControllerService = UUID.fromString("100F6C32-1735-4313-B402-38567131E5F3"); static public final UUID inputCharacteristic = UUID.fromString("100F6C33-1735-4313-B402-38567131E5F3"); static public 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); } } public HIDDeviceBLESteamController(HIDDeviceManager manager, BluetoothDevice device) { mManager = manager; mDevice = device; mDeviceId = mManager.getDeviceIDForIdentifier(getIdentifier()); mIsRegistered = false; mIsChromebook = mManager.getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); mOperations = new LinkedList(); mHandler = new Handler(Looper.getMainLooper()); mGatt = connectGatt(); // final HIDDeviceBLESteamController finalThis = this; // mHandler.postDelayed(new Runnable() { // @Override // public void run() { // finalThis.checkConnectionForChromebookIssue(); // } // }, CHROMEBOOK_CONNECTION_CHECK_INTERVAL); } public String getIdentifier() { return String.format("SteamController.%s", mDevice.getAddress()); } public 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); } public 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); } public void writeCharacteristic(UUID uuid, byte[] value) { GattOperation op = HIDDeviceBLESteamController.GattOperation.writeCharacteristic(mGatt, uuid, value); queueGattOperation(op); } public void readCharacteristic(UUID uuid) { GattOperation op = HIDDeviceBLESteamController.GattOperation.readCharacteristic(mGatt, uuid); queueGattOperation(op); } ////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////// BluetoothGattCallback overridden methods ////////////////////////////////////////////////////////////////////////////////////////////////////// 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. } 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); } } } 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.HIDDeviceFeatureReport(getId(), characteristic.getValue()); } finishCurrentGattOperation(); } 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); setRegistered(); } } finishCurrentGattOperation(); } 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()); } } public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { //Log.v(TAG, "onDescriptorRead status=" + status); } 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(); } public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { //Log.v(TAG, "onReliableWriteCompleted status=" + status); } public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { //Log.v(TAG, "onReadRemoteRssi status=" + status); } 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 sendFeatureReport(byte[] report) { if (!isRegistered()) { Log.e(TAG, "Attempted sendFeatureReport before Steam Controller is registered!"); if (mIsConnected) { probeService(this); } return -1; } // 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, "sendFeatureReport " + HexDump.dumpHexString(actual_report)); writeCharacteristic(reportCharacteristic, actual_report); return report.length; } @Override public int sendOutputReport(byte[] report) { if (!isRegistered()) { Log.e(TAG, "Attempted sendOutputReport before Steam Controller is registered!"); if (mIsConnected) { probeService(this); } return -1; } //Log.v(TAG, "sendFeatureReport " + HexDump.dumpHexString(report)); writeCharacteristic(reportCharacteristic, report); return report.length; } @Override public boolean getFeatureReport(byte[] report) { if (!isRegistered()) { Log.e(TAG, "Attempted getFeatureReport before Steam Controller is registered!"); if (mIsConnected) { probeService(this); } return false; } //Log.v(TAG, "getFeatureReport"); readCharacteristic(reportCharacteristic); return true; } @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(); } } xsystem35-sdl2-2.14.3/android/app/src/main/java/org/libsdl/app/HIDDeviceManager.java000066400000000000000000000657141477263212400277120ustar00rootroot00000000000000package 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; public static HIDDeviceManager acquire(Context context) { if (sManagerRefCount == 0) { sManager = new HIDDeviceManager(context); } ++sManagerRefCount; return sManager; } public static void release(HIDDeviceManager manager) { if (manager == sManager) { --sManagerRefCount; if (sManagerRefCount == 0) { sManager.close(); sManager = null; } } } private Context mContext; private HashMap mDevicesById = new HashMap(); private HashMap mBluetoothDevices = new HashMap(); private int mNextDeviceId = 0; private SharedPreferences mSharedPreferences = null; private boolean mIsChromebook = false; private UsbManager mUsbManager; private Handler mHandler; private BluetoothManager mBluetoothManager; private List 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 = mContext.getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); // if (shouldClear) { // SharedPreferences.Editor spedit = mSharedPreferences.edit(); // spedit.clear(); // spedit.commit(); // } // else { mNextDeviceId = mSharedPreferences.getInt("next_device_id", 0); } } public Context getContext() { return mContext; } public 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.commit(); 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); 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 0x2dc8, // 8BitDo 0x2e24, // Hyperkin 0x3537, // GameSir }; 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 devices = new ArrayList(); 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()); } } } } 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) || (Build.VERSION.SDK_INT < 18 /* Android 4.3 (JELLY_BEAN_MR2) */)) { 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); mContext.registerReceiver(mBluetoothBroadcast, filter); if (mIsChromebook) { mHandler = new Handler(Looper.getMainLooper()); mLastBluetoothDevices = new ArrayList(); // 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. public void chromebookConnectionHandler() { if (!mIsChromebook) { return; } ArrayList disconnected = new ArrayList(); ArrayList connected = new ArrayList(); List 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); } public 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; } public 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); } } public 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 ////////////////////////////////////////////////////////////////////////////////////////////////////// public boolean initialize(boolean usb, boolean bluetooth) { Log.v(TAG, "initialize(" + usb + ", " + bluetooth + ")"); if (usb) { initializeUSB(); } if (bluetooth) { initializeBluetooth(); } return true; } public 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; } if (Build.VERSION.SDK_INT >= 33 /* Android 14.0 (U) */) { Intent intent = new Intent(HIDDeviceManager.ACTION_USB_PERMISSION); intent.setPackage(mContext.getPackageName()); mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, intent, flags)); } else { mUsbManager.requestPermission(usbDevice, PendingIntent.getBroadcast(mContext, 0, new Intent(HIDDeviceManager.ACTION_USB_PERMISSION), 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; } public int sendOutputReport(int deviceID, byte[] report) { try { //Log.v(TAG, "sendOutputReport deviceID=" + deviceID + " length=" + report.length); HIDDevice device; device = getDevice(deviceID); if (device == null) { HIDDeviceDisconnected(deviceID); return -1; } return device.sendOutputReport(report); } catch (Exception e) { Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); } return -1; } public int sendFeatureReport(int deviceID, byte[] report) { try { //Log.v(TAG, "sendFeatureReport deviceID=" + deviceID + " length=" + report.length); HIDDevice device; device = getDevice(deviceID); if (device == null) { HIDDeviceDisconnected(deviceID); return -1; } return device.sendFeatureReport(report); } catch (Exception e) { Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); } return -1; } public boolean getFeatureReport(int deviceID, byte[] report) { try { //Log.v(TAG, "getFeatureReport deviceID=" + deviceID); HIDDevice device; device = getDevice(deviceID); if (device == null) { HIDDeviceDisconnected(deviceID); return false; } return device.getFeatureReport(report); } catch (Exception e) { Log.e(TAG, "Got exception: " + Log.getStackTraceString(e)); } return false; } public 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); 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 HIDDeviceFeatureReport(int deviceID, byte[] report); } xsystem35-sdl2-2.14.3/android/app/src/main/java/org/libsdl/app/HIDDeviceUSB.java000066400000000000000000000216201477263212400267550ustar00rootroot00000000000000package org.libsdl.app; import android.hardware.usb.*; import android.os.Build; import android.util.Log; import java.util.Arrays; 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; } public String getIdentifier() { return String.format("%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; if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { 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 = null; if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { result = mDevice.getManufacturerName(); } if (result == null) { result = String.format("%x", getVendorId()); } return result; } @Override public String getProductName() { String result = null; if (Build.VERSION.SDK_INT >= 21 /* Android 5.0 (LOLLIPOP) */) { result = mDevice.getProductName(); } if (result == null) { result = String.format("%x", getProductId()); } return result; } @Override public UsbDevice getDevice() { return mDevice; } public 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 sendFeatureReport(byte[] report) { 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, "sendFeatureReport() returned " + res + " on device " + getDeviceName()); return -1; } if (skipped_report_id) { ++length; } return length; } @Override public int sendOutputReport(byte[] report) { int r = mConnection.bulkTransfer(mOutputEndpoint, report, report.length, 1000); if (r != report.length) { Log.w(TAG, "sendOutputReport() returned " + r + " on device " + getDeviceName()); } return r; } @Override public boolean getFeatureReport(byte[] report) { 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 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*/, (3/*HID feature*/ << 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.HIDDeviceFeatureReport(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); } } } } } } xsystem35-sdl2-2.14.3/android/app/src/main/java/org/libsdl/app/SDL.java000066400000000000000000000066431477263212400253110ustar00rootroot00000000000000package org.libsdl.app; import android.content.Context; import java.lang.Class; 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 public static void setupJNI() { SDLActivity.nativeSetupJNI(); SDLAudioManager.nativeSetupJNI(); SDLControllerManager.nativeSetupJNI(); } // This function should be called each time the activity is started public static void initialize() { setContext(null); SDLActivity.initialize(); SDLAudioManager.initialize(); SDLControllerManager.initialize(); } // This function stores the current activity (SDL or not) public static void setContext(Context context) { SDLAudioManager.setContext(context); mContext = context; } public static Context getContext() { return mContext; } public static void loadLibrary(String libraryName) throws UnsatisfiedLinkError, SecurityException, NullPointerException { loadLibrary(libraryName, mContext); } public 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 Context mContext; } xsystem35-sdl2-2.14.3/android/app/src/main/java/org/libsdl/app/SDLActivity.java000066400000000000000000002253701477263212400270260ustar00rootroot00000000000000package org.libsdl.app; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.UiModeManager; import android.content.ClipboardManager; import android.content.ClipData; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Bitmap; import android.graphics.Color; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.hardware.Sensor; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Message; import android.text.Editable; import android.text.InputType; import android.text.Selection; import android.util.DisplayMetrics; import android.util.Log; import android.util.SparseArray; import android.view.Display; import android.view.Gravity; import android.view.InputDevice; import android.view.KeyEvent; import android.view.PointerIcon; import android.view.Surface; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; import android.widget.Button; import android.widget.EditText; import android.widget.LinearLayout; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import java.util.Hashtable; import java.util.Locale; /** SDL Activity */ public class SDLActivity extends Activity implements View.OnSystemUiVisibilityChangeListener { private static final String TAG = "SDL"; private static final int SDL_MAJOR_VERSION = 2; private static final int SDL_MINOR_VERSION = 32; private static final int SDL_MICRO_VERSION = 4; /* // Display InputType.SOURCE/CLASS of events and devices // // SDLActivity.debugSource(device.getSources(), "device[" + device.getName() + "]"); // SDLActivity.debugSource(event.getSource(), "event"); public static void debugSource(int sources, String prefix) { int s = sources; int s_copy = sources; String cls = ""; String src = ""; int tst = 0; int FLAG_TAINTED = 0x80000000; if ((s & InputDevice.SOURCE_CLASS_BUTTON) != 0) cls += " BUTTON"; if ((s & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) cls += " JOYSTICK"; if ((s & InputDevice.SOURCE_CLASS_POINTER) != 0) cls += " POINTER"; if ((s & InputDevice.SOURCE_CLASS_POSITION) != 0) cls += " POSITION"; if ((s & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) cls += " TRACKBALL"; int s2 = s_copy & ~InputDevice.SOURCE_ANY; // keep class bits s2 &= ~( InputDevice.SOURCE_CLASS_BUTTON | InputDevice.SOURCE_CLASS_JOYSTICK | InputDevice.SOURCE_CLASS_POINTER | InputDevice.SOURCE_CLASS_POSITION | InputDevice.SOURCE_CLASS_TRACKBALL); if (s2 != 0) cls += "Some_Unknown"; s2 = s_copy & InputDevice.SOURCE_ANY; // keep source only, no class; if (Build.VERSION.SDK_INT >= 23) { tst = InputDevice.SOURCE_BLUETOOTH_STYLUS; if ((s & tst) == tst) src += " BLUETOOTH_STYLUS"; s2 &= ~tst; } tst = InputDevice.SOURCE_DPAD; if ((s & tst) == tst) src += " DPAD"; s2 &= ~tst; tst = InputDevice.SOURCE_GAMEPAD; if ((s & tst) == tst) src += " GAMEPAD"; s2 &= ~tst; if (Build.VERSION.SDK_INT >= 21) { tst = InputDevice.SOURCE_HDMI; if ((s & tst) == tst) src += " HDMI"; s2 &= ~tst; } tst = InputDevice.SOURCE_JOYSTICK; if ((s & tst) == tst) src += " JOYSTICK"; s2 &= ~tst; tst = InputDevice.SOURCE_KEYBOARD; if ((s & tst) == tst) src += " KEYBOARD"; s2 &= ~tst; tst = InputDevice.SOURCE_MOUSE; if ((s & tst) == tst) src += " MOUSE"; s2 &= ~tst; if (Build.VERSION.SDK_INT >= 26) { tst = InputDevice.SOURCE_MOUSE_RELATIVE; if ((s & tst) == tst) src += " MOUSE_RELATIVE"; s2 &= ~tst; tst = InputDevice.SOURCE_ROTARY_ENCODER; if ((s & tst) == tst) src += " ROTARY_ENCODER"; s2 &= ~tst; } tst = InputDevice.SOURCE_STYLUS; if ((s & tst) == tst) src += " STYLUS"; s2 &= ~tst; tst = InputDevice.SOURCE_TOUCHPAD; if ((s & tst) == tst) src += " TOUCHPAD"; s2 &= ~tst; tst = InputDevice.SOURCE_TOUCHSCREEN; if ((s & tst) == tst) src += " TOUCHSCREEN"; s2 &= ~tst; if (Build.VERSION.SDK_INT >= 18) { tst = InputDevice.SOURCE_TOUCH_NAVIGATION; if ((s & tst) == tst) src += " TOUCH_NAVIGATION"; s2 &= ~tst; } tst = InputDevice.SOURCE_TRACKBALL; if ((s & tst) == tst) src += " TRACKBALL"; s2 &= ~tst; tst = InputDevice.SOURCE_ANY; if ((s & tst) == tst) src += " ANY"; s2 &= ~tst; if (s == FLAG_TAINTED) src += " FLAG_TAINTED"; s2 &= ~FLAG_TAINTED; if (s2 != 0) src += " Some_Unknown"; Log.v(TAG, prefix + "int=" + s_copy + " CLASS={" + cls + " } source(s):" + src); } */ public static boolean mIsResumedCalled, mHasFocus; public static final boolean mHasMultiWindow = (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */); // Cursor types // private static final int SDL_SYSTEM_CURSOR_NONE = -1; private static final int SDL_SYSTEM_CURSOR_ARROW = 0; private static final int SDL_SYSTEM_CURSOR_IBEAM = 1; private static final int SDL_SYSTEM_CURSOR_WAIT = 2; private static final int SDL_SYSTEM_CURSOR_CROSSHAIR = 3; private static final int SDL_SYSTEM_CURSOR_WAITARROW = 4; private static final int SDL_SYSTEM_CURSOR_SIZENWSE = 5; private static final int SDL_SYSTEM_CURSOR_SIZENESW = 6; private static final int SDL_SYSTEM_CURSOR_SIZEWE = 7; private static final int SDL_SYSTEM_CURSOR_SIZENS = 8; private static final int SDL_SYSTEM_CURSOR_SIZEALL = 9; private static final int SDL_SYSTEM_CURSOR_NO = 10; private static final int SDL_SYSTEM_CURSOR_HAND = 11; protected static final int SDL_ORIENTATION_UNKNOWN = 0; protected static final int SDL_ORIENTATION_LANDSCAPE = 1; protected static final int SDL_ORIENTATION_LANDSCAPE_FLIPPED = 2; protected static final int SDL_ORIENTATION_PORTRAIT = 3; protected static final int SDL_ORIENTATION_PORTRAIT_FLIPPED = 4; protected static int mCurrentOrientation; protected static Locale mCurrentLocale; // Handle the state of the native layer public enum NativeState { INIT, RESUMED, PAUSED } public static NativeState mNextNativeState; public static NativeState mCurrentNativeState; /** If shared libraries (e.g. SDL or the native application) could not be loaded. */ public static boolean mBrokenLibraries = true; // Main components protected static SDLActivity mSingleton; protected static SDLSurface mSurface; protected static DummyEdit mTextEdit; protected static boolean mScreenKeyboardShown; protected static ViewGroup mLayout; protected static SDLClipboardHandler mClipboardHandler; protected static Hashtable mCursors; protected static int mLastCursorID; protected static SDLGenericMotionListener_API12 mMotionListener; protected static HIDDeviceManager mHIDDeviceManager; // This is what SDL runs in. It invokes SDL_main(), eventually protected static Thread mSDLThread; protected static SDLGenericMotionListener_API12 getMotionListener() { if (mMotionListener == null) { if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) { mMotionListener = new SDLGenericMotionListener_API26(); } else if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) { mMotionListener = new SDLGenericMotionListener_API24(); } else { mMotionListener = new SDLGenericMotionListener_API12(); } } return mMotionListener; } /** * This method returns the name of the shared object with the application entry point * It can be overridden by derived classes. */ protected String getMainSharedObject() { String library; String[] libraries = SDLActivity.mSingleton.getLibraries(); if (libraries.length > 0) { library = "lib" + libraries[libraries.length - 1] + ".so"; } else { library = "libmain.so"; } return getContext().getApplicationInfo().nativeLibraryDir + "/" + library; } /** * This method returns the name of the application entry point * It can be overridden by derived classes. */ protected String getMainFunction() { return "SDL_main"; } /** * This method is called by SDL before loading the native shared libraries. * It can be overridden to provide names of shared libraries to be loaded. * The default implementation returns the defaults. It never returns null. * An array returned by a new implementation must at least contain "SDL2". * Also keep in mind that the order the libraries are loaded may matter. * @return names of shared libraries to be loaded (e.g. "SDL2", "main"). */ protected String[] getLibraries() { return new String[] { "SDL2", // "SDL2_image", // "SDL2_mixer", // "SDL2_net", // "SDL2_ttf", "main" }; } // Load the .so public void loadLibraries() { for (String lib : getLibraries()) { SDL.loadLibrary(lib, this); } } /** * This method is called by SDL before starting the native application thread. * It can be overridden to provide the arguments after the application name. * The default implementation returns an empty array. It never returns null. * @return arguments for the native application. */ protected String[] getArguments() { return new String[0]; } public static void initialize() { // The static nature of the singleton and Android quirkyness force us to initialize everything here // Otherwise, when exiting the app and returning to it, these variables *keep* their pre exit values mSingleton = null; mSurface = null; mTextEdit = null; mLayout = null; mClipboardHandler = null; mCursors = new Hashtable(); mLastCursorID = 0; mSDLThread = null; mIsResumedCalled = false; mHasFocus = true; mNextNativeState = NativeState.INIT; mCurrentNativeState = NativeState.INIT; } protected SDLSurface createSDLSurface(Context context) { return new SDLSurface(context); } // Setup @Override protected void onCreate(Bundle savedInstanceState) { Log.v(TAG, "Device: " + Build.DEVICE); Log.v(TAG, "Model: " + Build.MODEL); Log.v(TAG, "onCreate()"); super.onCreate(savedInstanceState); try { Thread.currentThread().setName("SDLActivity"); } catch (Exception e) { Log.v(TAG, "modify thread properties failed " + e.toString()); } // Load shared libraries String errorMsgBrokenLib = ""; try { loadLibraries(); mBrokenLibraries = false; /* success */ } catch(UnsatisfiedLinkError e) { System.err.println(e.getMessage()); mBrokenLibraries = true; errorMsgBrokenLib = e.getMessage(); } catch(Exception e) { System.err.println(e.getMessage()); mBrokenLibraries = true; errorMsgBrokenLib = e.getMessage(); } if (!mBrokenLibraries) { String expected_version = String.valueOf(SDL_MAJOR_VERSION) + "." + String.valueOf(SDL_MINOR_VERSION) + "." + String.valueOf(SDL_MICRO_VERSION); String version = nativeGetVersion(); if (!version.equals(expected_version)) { mBrokenLibraries = true; errorMsgBrokenLib = "SDL C/Java version mismatch (expected " + expected_version + ", got " + version + ")"; } } if (mBrokenLibraries) { mSingleton = this; AlertDialog.Builder dlgAlert = new AlertDialog.Builder(this); dlgAlert.setMessage("An error occurred while trying to start the application. Please try again and/or reinstall." + System.getProperty("line.separator") + System.getProperty("line.separator") + "Error: " + errorMsgBrokenLib); dlgAlert.setTitle("SDL Error"); dlgAlert.setPositiveButton("Exit", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog,int id) { // if this button is clicked, close current activity SDLActivity.mSingleton.finish(); } }); dlgAlert.setCancelable(false); dlgAlert.create().show(); return; } // Set up JNI SDL.setupJNI(); // Initialize state SDL.initialize(); // So we can call stuff from static callbacks mSingleton = this; SDL.setContext(this); mClipboardHandler = new SDLClipboardHandler(); mHIDDeviceManager = HIDDeviceManager.acquire(this); // Set up the surface mSurface = createSDLSurface(this); mLayout = new RelativeLayout(this); mLayout.addView(mSurface); // Get our current screen orientation and pass it down. mCurrentOrientation = SDLActivity.getCurrentOrientation(); // Only record current orientation SDLActivity.onNativeOrientationChanged(mCurrentOrientation); try { if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { mCurrentLocale = getContext().getResources().getConfiguration().locale; } else { mCurrentLocale = getContext().getResources().getConfiguration().getLocales().get(0); } } catch(Exception ignored) { } setContentView(mLayout); setWindowStyle(false); getWindow().getDecorView().setOnSystemUiVisibilityChangeListener(this); // Get filename from "Open with" of another application Intent intent = getIntent(); if (intent != null && intent.getData() != null) { String filename = intent.getData().getPath(); if (filename != null) { Log.v(TAG, "Got filename: " + filename); SDLActivity.onNativeDropFile(filename); } } } protected void pauseNativeThread() { mNextNativeState = NativeState.PAUSED; mIsResumedCalled = false; if (SDLActivity.mBrokenLibraries) { return; } SDLActivity.handleNativeState(); } protected void resumeNativeThread() { mNextNativeState = NativeState.RESUMED; mIsResumedCalled = true; if (SDLActivity.mBrokenLibraries) { return; } SDLActivity.handleNativeState(); } // Events @Override protected void onPause() { Log.v(TAG, "onPause()"); super.onPause(); if (mHIDDeviceManager != null) { mHIDDeviceManager.setFrozen(true); } if (!mHasMultiWindow) { pauseNativeThread(); } } @Override protected void onResume() { Log.v(TAG, "onResume()"); super.onResume(); if (mHIDDeviceManager != null) { mHIDDeviceManager.setFrozen(false); } if (!mHasMultiWindow) { resumeNativeThread(); } } @Override protected void onStop() { Log.v(TAG, "onStop()"); super.onStop(); if (mHasMultiWindow) { pauseNativeThread(); } } @Override protected void onStart() { Log.v(TAG, "onStart()"); super.onStart(); if (mHasMultiWindow) { resumeNativeThread(); } } public static int getCurrentOrientation() { int result = SDL_ORIENTATION_UNKNOWN; Activity activity = (Activity)getContext(); if (activity == null) { return result; } Display display = activity.getWindowManager().getDefaultDisplay(); switch (display.getRotation()) { case Surface.ROTATION_0: result = SDL_ORIENTATION_PORTRAIT; break; case Surface.ROTATION_90: result = SDL_ORIENTATION_LANDSCAPE; break; case Surface.ROTATION_180: result = SDL_ORIENTATION_PORTRAIT_FLIPPED; break; case Surface.ROTATION_270: result = SDL_ORIENTATION_LANDSCAPE_FLIPPED; break; } return result; } @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); Log.v(TAG, "onWindowFocusChanged(): " + hasFocus); if (SDLActivity.mBrokenLibraries) { return; } mHasFocus = hasFocus; if (hasFocus) { mNextNativeState = NativeState.RESUMED; SDLActivity.getMotionListener().reclaimRelativeMouseModeIfNeeded(); SDLActivity.handleNativeState(); nativeFocusChanged(true); } else { nativeFocusChanged(false); if (!mHasMultiWindow) { mNextNativeState = NativeState.PAUSED; SDLActivity.handleNativeState(); } } } @Override public void onLowMemory() { Log.v(TAG, "onLowMemory()"); super.onLowMemory(); if (SDLActivity.mBrokenLibraries) { return; } SDLActivity.nativeLowMemory(); } @Override public void onConfigurationChanged(Configuration newConfig) { Log.v(TAG, "onConfigurationChanged()"); super.onConfigurationChanged(newConfig); if (SDLActivity.mBrokenLibraries) { return; } if (mCurrentLocale == null || !mCurrentLocale.equals(newConfig.locale)) { mCurrentLocale = newConfig.locale; SDLActivity.onNativeLocaleChanged(); } } @Override protected void onDestroy() { Log.v(TAG, "onDestroy()"); if (mHIDDeviceManager != null) { HIDDeviceManager.release(mHIDDeviceManager); mHIDDeviceManager = null; } SDLAudioManager.release(this); if (SDLActivity.mBrokenLibraries) { super.onDestroy(); return; } if (SDLActivity.mSDLThread != null) { // Send Quit event to "SDLThread" thread SDLActivity.nativeSendQuit(); // Wait for "SDLThread" thread to end try { SDLActivity.mSDLThread.join(); } catch(Exception e) { Log.v(TAG, "Problem stopping SDLThread: " + e); } } SDLActivity.nativeQuit(); super.onDestroy(); } @Override public void onBackPressed() { // Check if we want to block the back button in case of mouse right click. // // If we do, the normal hardware back button will no longer work and people have to use home, // but the mouse right click will work. // boolean trapBack = SDLActivity.nativeGetHintBoolean("SDL_ANDROID_TRAP_BACK_BUTTON", false); if (trapBack) { // Exit and let the mouse handler handle this button (if appropriate) return; } // Default system back button behavior. if (!isFinishing()) { super.onBackPressed(); } } // Called by JNI from SDL. public static void manualBackButton() { mSingleton.pressBackButton(); } // Used to get us onto the activity's main thread public void pressBackButton() { runOnUiThread(new Runnable() { @Override public void run() { if (!SDLActivity.this.isFinishing()) { SDLActivity.this.superOnBackPressed(); } } }); } // Used to access the system back behavior. public void superOnBackPressed() { super.onBackPressed(); } @Override public boolean dispatchKeyEvent(KeyEvent event) { if (SDLActivity.mBrokenLibraries) { return false; } int keyCode = event.getKeyCode(); // Ignore certain special keys so they're handled by Android if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_CAMERA || keyCode == KeyEvent.KEYCODE_ZOOM_IN || /* API 11 */ keyCode == KeyEvent.KEYCODE_ZOOM_OUT /* API 11 */ ) { return false; } return super.dispatchKeyEvent(event); } /* Transition to next state */ public static void handleNativeState() { if (mNextNativeState == mCurrentNativeState) { // Already in same state, discard. return; } // Try a transition to init state if (mNextNativeState == NativeState.INIT) { mCurrentNativeState = mNextNativeState; return; } // Try a transition to paused state if (mNextNativeState == NativeState.PAUSED) { if (mSDLThread != null) { nativePause(); } if (mSurface != null) { mSurface.handlePause(); } mCurrentNativeState = mNextNativeState; return; } // Try a transition to resumed state if (mNextNativeState == NativeState.RESUMED) { if (mSurface.mIsSurfaceReady && mHasFocus && mIsResumedCalled) { if (mSDLThread == null) { // This is the entry point to the C app. // Start up the C app thread and enable sensor input for the first time // FIXME: Why aren't we enabling sensor input at start? mSDLThread = new Thread(new SDLMain(), "SDLThread"); mSurface.enableSensor(Sensor.TYPE_ACCELEROMETER, true); mSDLThread.start(); // No nativeResume(), don't signal Android_ResumeSem } else { nativeResume(); } mSurface.handleResume(); mCurrentNativeState = mNextNativeState; } } } // Messages from the SDLMain thread static final int COMMAND_CHANGE_TITLE = 1; static final int COMMAND_CHANGE_WINDOW_STYLE = 2; static final int COMMAND_TEXTEDIT_HIDE = 3; static final int COMMAND_SET_KEEP_SCREEN_ON = 5; protected static final int COMMAND_USER = 0x8000; protected static boolean mFullscreenModeActive; /** * This method is called by SDL if SDL did not handle a message itself. * This happens if a received message contains an unsupported command. * Method can be overwritten to handle Messages in a different class. * @param command the command of the message. * @param param the parameter of the message. May be null. * @return if the message was handled in overridden method. */ protected boolean onUnhandledMessage(int command, Object param) { return false; } /** * A Handler class for Messages from native SDL applications. * It uses current Activities as target (e.g. for the title). * static to prevent implicit references to enclosing object. */ protected static class SDLCommandHandler extends Handler { @Override public void handleMessage(Message msg) { Context context = SDL.getContext(); if (context == null) { Log.e(TAG, "error handling message, getContext() returned null"); return; } switch (msg.arg1) { case COMMAND_CHANGE_TITLE: if (context instanceof Activity) { ((Activity) context).setTitle((String)msg.obj); } else { Log.e(TAG, "error handling message, getContext() returned no Activity"); } break; case COMMAND_CHANGE_WINDOW_STYLE: if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { if (context instanceof Activity) { Window window = ((Activity) context).getWindow(); if (window != null) { if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { int flags = View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.INVISIBLE; window.getDecorView().setSystemUiVisibility(flags); window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); SDLActivity.mFullscreenModeActive = true; } else { int flags = View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE; window.getDecorView().setSystemUiVisibility(flags); window.addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); SDLActivity.mFullscreenModeActive = false; } if (Build.VERSION.SDK_INT >= 28 /* Android 9 (Pie) */) { window.getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; } } } else { Log.e(TAG, "error handling message, getContext() returned no Activity"); } } break; case COMMAND_TEXTEDIT_HIDE: if (mTextEdit != null) { // Note: On some devices setting view to GONE creates a flicker in landscape. // Setting the View's sizes to 0 is similar to GONE but without the flicker. // The sizes will be set to useful values when the keyboard is shown again. mTextEdit.setLayoutParams(new RelativeLayout.LayoutParams(0, 0)); InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(mTextEdit.getWindowToken(), 0); mScreenKeyboardShown = false; mSurface.requestFocus(); } break; case COMMAND_SET_KEEP_SCREEN_ON: { if (context instanceof Activity) { Window window = ((Activity) context).getWindow(); if (window != null) { if ((msg.obj instanceof Integer) && ((Integer) msg.obj != 0)) { window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } else { window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); } } } break; } default: if ((context instanceof SDLActivity) && !((SDLActivity) context).onUnhandledMessage(msg.arg1, msg.obj)) { Log.e(TAG, "error handling message, command is " + msg.arg1); } } } } // Handler for the messages Handler commandHandler = new SDLCommandHandler(); // Send a message from the SDLMain thread boolean sendCommand(int command, Object data) { Message msg = commandHandler.obtainMessage(); msg.arg1 = command; msg.obj = data; boolean result = commandHandler.sendMessage(msg); if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) { if (command == COMMAND_CHANGE_WINDOW_STYLE) { // Ensure we don't return until the resize has actually happened, // or 500ms have passed. boolean bShouldWait = false; if (data instanceof Integer) { // Let's figure out if we're already laid out fullscreen or not. Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); DisplayMetrics realMetrics = new DisplayMetrics(); display.getRealMetrics(realMetrics); boolean bFullscreenLayout = ((realMetrics.widthPixels == mSurface.getWidth()) && (realMetrics.heightPixels == mSurface.getHeight())); if ((Integer) data == 1) { // If we aren't laid out fullscreen or actively in fullscreen mode already, we're going // to change size and should wait for surfaceChanged() before we return, so the size // is right back in native code. If we're already laid out fullscreen, though, we're // not going to change size even if we change decor modes, so we shouldn't wait for // surfaceChanged() -- which may not even happen -- and should return immediately. bShouldWait = !bFullscreenLayout; } else { // If we're laid out fullscreen (even if the status bar and nav bar are present), // or are actively in fullscreen, we're going to change size and should wait for // surfaceChanged before we return, so the size is right back in native code. bShouldWait = bFullscreenLayout; } } if (bShouldWait && (SDLActivity.getContext() != null)) { // We'll wait for the surfaceChanged() method, which will notify us // when called. That way, we know our current size is really the // size we need, instead of grabbing a size that's still got // the navigation and/or status bars before they're hidden. // // We'll wait for up to half a second, because some devices // take a surprisingly long time for the surface resize, but // then we'll just give up and return. // synchronized (SDLActivity.getContext()) { try { SDLActivity.getContext().wait(500); } catch (InterruptedException ie) { ie.printStackTrace(); } } } } } return result; } // C functions we call public static native String nativeGetVersion(); public static native int nativeSetupJNI(); public static native int nativeRunMain(String library, String function, Object arguments); public static native void nativeLowMemory(); public static native void nativeSendQuit(); public static native void nativeQuit(); public static native void nativePause(); public static native void nativeResume(); public static native void nativeFocusChanged(boolean hasFocus); public static native void onNativeDropFile(String filename); public static native void nativeSetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, float rate); public static native void onNativeResize(); public static native void onNativeKeyDown(int keycode); public static native void onNativeKeyUp(int keycode); public static native boolean onNativeSoftReturnKey(); public static native void onNativeKeyboardFocusLost(); public static native void onNativeMouse(int button, int action, float x, float y, boolean relative); public static native void onNativeTouch(int touchDevId, int pointerFingerId, int action, float x, float y, float p); public static native void onNativeAccel(float x, float y, float z); public static native void onNativeClipboardChanged(); public static native void onNativeSurfaceCreated(); public static native void onNativeSurfaceChanged(); public static native void onNativeSurfaceDestroyed(); public static native String nativeGetHint(String name); public static native boolean nativeGetHintBoolean(String name, boolean default_value); public static native void nativeSetenv(String name, String value); public static native void onNativeOrientationChanged(int orientation); public static native void nativeAddTouch(int touchId, String name); public static native void nativePermissionResult(int requestCode, boolean result); public static native void onNativeLocaleChanged(); /** * This method is called by SDL using JNI. */ public static boolean setActivityTitle(String title) { // Called from SDLMain() thread and can't directly affect the view return mSingleton.sendCommand(COMMAND_CHANGE_TITLE, title); } /** * This method is called by SDL using JNI. */ public static void setWindowStyle(boolean fullscreen) { // Called from SDLMain() thread and can't directly affect the view mSingleton.sendCommand(COMMAND_CHANGE_WINDOW_STYLE, fullscreen ? 1 : 0); } /** * This method is called by SDL using JNI. * This is a static method for JNI convenience, it calls a non-static method * so that is can be overridden */ public static void setOrientation(int w, int h, boolean resizable, String hint) { if (mSingleton != null) { mSingleton.setOrientationBis(w, h, resizable, hint); } } /** * This can be overridden */ public void setOrientationBis(int w, int h, boolean resizable, String hint) { int orientation_landscape = -1; int orientation_portrait = -1; /* If set, hint "explicitly controls which UI orientations are allowed". */ if (hint.contains("LandscapeRight") && hint.contains("LandscapeLeft")) { orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE; } else if (hint.contains("LandscapeLeft")) { orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; } else if (hint.contains("LandscapeRight")) { orientation_landscape = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; } /* exact match to 'Portrait' to distinguish with PortraitUpsideDown */ boolean contains_Portrait = hint.contains("Portrait ") || hint.endsWith("Portrait"); if (contains_Portrait && hint.contains("PortraitUpsideDown")) { orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT; } else if (contains_Portrait) { orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; } else if (hint.contains("PortraitUpsideDown")) { orientation_portrait = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; } boolean is_landscape_allowed = (orientation_landscape != -1); boolean is_portrait_allowed = (orientation_portrait != -1); int req; /* Requested orientation */ /* No valid hint, nothing is explicitly allowed */ if (!is_portrait_allowed && !is_landscape_allowed) { if (resizable) { /* All orientations are allowed, respecting user orientation lock setting */ req = ActivityInfo.SCREEN_ORIENTATION_FULL_USER; } else { /* Fixed window and nothing specified. Get orientation from w/h of created window */ req = (w > h ? ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE : ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT); } } else { /* At least one orientation is allowed */ if (resizable) { if (is_portrait_allowed && is_landscape_allowed) { /* hint allows both landscape and portrait, promote to full user */ req = ActivityInfo.SCREEN_ORIENTATION_FULL_USER; } else { /* Use the only one allowed "orientation" */ req = (is_landscape_allowed ? orientation_landscape : orientation_portrait); } } else { /* Fixed window and both orientations are allowed. Choose one. */ if (is_portrait_allowed && is_landscape_allowed) { req = (w > h ? orientation_landscape : orientation_portrait); } else { /* Use the only one allowed "orientation" */ req = (is_landscape_allowed ? orientation_landscape : orientation_portrait); } } } Log.v(TAG, "setOrientation() requestedOrientation=" + req + " width=" + w +" height="+ h +" resizable=" + resizable + " hint=" + hint); mSingleton.setRequestedOrientation(req); } /** * This method is called by SDL using JNI. */ public static void minimizeWindow() { if (mSingleton == null) { return; } Intent startMain = new Intent(Intent.ACTION_MAIN); startMain.addCategory(Intent.CATEGORY_HOME); startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); mSingleton.startActivity(startMain); } /** * This method is called by SDL using JNI. */ public static boolean shouldMinimizeOnFocusLoss() { /* if (Build.VERSION.SDK_INT >= 24) { if (mSingleton == null) { return true; } if (mSingleton.isInMultiWindowMode()) { return false; } if (mSingleton.isInPictureInPictureMode()) { return false; } } return true; */ return false; } /** * This method is called by SDL using JNI. */ public static boolean isScreenKeyboardShown() { if (mTextEdit == null) { return false; } if (!mScreenKeyboardShown) { return false; } InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); return imm.isAcceptingText(); } /** * This method is called by SDL using JNI. */ public static boolean supportsRelativeMouse() { // DeX mode in Samsung Experience 9.0 and earlier doesn't support relative mice properly under // Android 7 APIs, and simply returns no data under Android 8 APIs. // // This is fixed in Samsung Experience 9.5, which corresponds to Android 8.1.0, and // thus SDK version 27. If we are in DeX mode and not API 27 or higher, as a result, // we should stick to relative mode. // if (Build.VERSION.SDK_INT < 27 /* Android 8.1 (O_MR1) */ && isDeXMode()) { return false; } return SDLActivity.getMotionListener().supportsRelativeMouse(); } /** * This method is called by SDL using JNI. */ public static boolean setRelativeMouseEnabled(boolean enabled) { if (enabled && !supportsRelativeMouse()) { return false; } return SDLActivity.getMotionListener().setRelativeMouseEnabled(enabled); } /** * This method is called by SDL using JNI. */ public static boolean sendMessage(int command, int param) { if (mSingleton == null) { return false; } return mSingleton.sendCommand(command, param); } /** * This method is called by SDL using JNI. */ public static Context getContext() { return SDL.getContext(); } /** * This method is called by SDL using JNI. */ public static boolean isAndroidTV() { UiModeManager uiModeManager = (UiModeManager) getContext().getSystemService(UI_MODE_SERVICE); if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) { return true; } if (Build.MANUFACTURER.equals("MINIX") && Build.MODEL.equals("NEO-U1")) { return true; } if (Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.equals("X96-W")) { return true; } return Build.MANUFACTURER.equals("Amlogic") && Build.MODEL.startsWith("TV"); } public static double getDiagonal() { DisplayMetrics metrics = new DisplayMetrics(); Activity activity = (Activity)getContext(); if (activity == null) { return 0.0; } activity.getWindowManager().getDefaultDisplay().getMetrics(metrics); double dWidthInches = metrics.widthPixels / (double)metrics.xdpi; double dHeightInches = metrics.heightPixels / (double)metrics.ydpi; return Math.sqrt((dWidthInches * dWidthInches) + (dHeightInches * dHeightInches)); } /** * This method is called by SDL using JNI. */ public static boolean isTablet() { // If our diagonal size is seven inches or greater, we consider ourselves a tablet. return (getDiagonal() >= 7.0); } /** * This method is called by SDL using JNI. */ public static boolean isChromebook() { if (getContext() == null) { return false; } return getContext().getPackageManager().hasSystemFeature("org.chromium.arc.device_management"); } /** * This method is called by SDL using JNI. */ public static boolean isDeXMode() { if (Build.VERSION.SDK_INT < 24 /* Android 7.0 (N) */) { return false; } try { final Configuration config = getContext().getResources().getConfiguration(); final Class configClass = config.getClass(); return configClass.getField("SEM_DESKTOP_MODE_ENABLED").getInt(configClass) == configClass.getField("semDesktopModeEnabled").getInt(config); } catch(Exception ignored) { return false; } } /** * This method is called by SDL using JNI. */ public static DisplayMetrics getDisplayDPI() { return getContext().getResources().getDisplayMetrics(); } /** * This method is called by SDL using JNI. */ public static boolean getManifestEnvironmentVariables() { try { if (getContext() == null) { return false; } ApplicationInfo applicationInfo = getContext().getPackageManager().getApplicationInfo(getContext().getPackageName(), PackageManager.GET_META_DATA); Bundle bundle = applicationInfo.metaData; if (bundle == null) { return false; } String prefix = "SDL_ENV."; final int trimLength = prefix.length(); for (String key : bundle.keySet()) { if (key.startsWith(prefix)) { String name = key.substring(trimLength); String value = bundle.get(key).toString(); nativeSetenv(name, value); } } /* environment variables set! */ return true; } catch (Exception e) { Log.v(TAG, "exception " + e.toString()); } return false; } // This method is called by SDLControllerManager's API 26 Generic Motion Handler. public static View getContentView() { return mLayout; } static class ShowTextInputTask implements Runnable { /* * This is used to regulate the pan&scan method to have some offset from * the bottom edge of the input region and the top edge of an input * method (soft keyboard) */ static final int HEIGHT_PADDING = 15; public int x, y, w, h; public ShowTextInputTask(int x, int y, int w, int h) { this.x = x; this.y = y; this.w = w; this.h = h; /* Minimum size of 1 pixel, so it takes focus. */ if (this.w <= 0) { this.w = 1; } if (this.h + HEIGHT_PADDING <= 0) { this.h = 1 - HEIGHT_PADDING; } } @Override public void run() { RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(w, h + HEIGHT_PADDING); params.leftMargin = x; params.topMargin = y; if (mTextEdit == null) { mTextEdit = new DummyEdit(SDL.getContext()); mLayout.addView(mTextEdit, params); } else { mTextEdit.setLayoutParams(params); } mTextEdit.setVisibility(View.VISIBLE); mTextEdit.requestFocus(); InputMethodManager imm = (InputMethodManager) SDL.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); imm.showSoftInput(mTextEdit, 0); mScreenKeyboardShown = true; } } /** * This method is called by SDL using JNI. */ public static boolean showTextInput(int x, int y, int w, int h) { // Transfer the task to the main thread as a Runnable return mSingleton.commandHandler.post(new ShowTextInputTask(x, y, w, h)); } public static boolean isTextInputEvent(KeyEvent event) { // Key pressed with Ctrl should be sent as SDL_KEYDOWN/SDL_KEYUP and not SDL_TEXTINPUT if (event.isCtrlPressed()) { return false; } return event.isPrintingKey() || event.getKeyCode() == KeyEvent.KEYCODE_SPACE; } public static boolean handleKeyEvent(View v, int keyCode, KeyEvent event, InputConnection ic) { int deviceId = event.getDeviceId(); int source = event.getSource(); if (source == InputDevice.SOURCE_UNKNOWN) { InputDevice device = InputDevice.getDevice(deviceId); if (device != null) { source = device.getSources(); } } // if (event.getAction() == KeyEvent.ACTION_DOWN) { // Log.v("SDL", "key down: " + keyCode + ", deviceId = " + deviceId + ", source = " + source); // } else if (event.getAction() == KeyEvent.ACTION_UP) { // Log.v("SDL", "key up: " + keyCode + ", deviceId = " + deviceId + ", source = " + source); // } // Dispatch the different events depending on where they come from // Some SOURCE_JOYSTICK, SOURCE_DPAD or SOURCE_GAMEPAD are also SOURCE_KEYBOARD // So, we try to process them as JOYSTICK/DPAD/GAMEPAD events first, if that fails we try them as KEYBOARD // // Furthermore, it's possible a game controller has SOURCE_KEYBOARD and // SOURCE_JOYSTICK, while its key events arrive from the keyboard source // So, retrieve the device itself and check all of its sources if (SDLControllerManager.isDeviceSDLJoystick(deviceId)) { // Note that we process events with specific key codes here if (event.getAction() == KeyEvent.ACTION_DOWN) { if (SDLControllerManager.onNativePadDown(deviceId, keyCode) == 0) { return true; } } else if (event.getAction() == KeyEvent.ACTION_UP) { if (SDLControllerManager.onNativePadUp(deviceId, keyCode) == 0) { return true; } } } if ((source & InputDevice.SOURCE_MOUSE) == InputDevice.SOURCE_MOUSE) { // on some devices key events are sent for mouse BUTTON_BACK/FORWARD presses // they are ignored here because sending them as mouse input to SDL is messy if ((keyCode == KeyEvent.KEYCODE_BACK) || (keyCode == KeyEvent.KEYCODE_FORWARD)) { switch (event.getAction()) { case KeyEvent.ACTION_DOWN: case KeyEvent.ACTION_UP: // mark the event as handled or it will be handled by system // handling KEYCODE_BACK by system will call onBackPressed() return true; } } } if (event.getAction() == KeyEvent.ACTION_DOWN) { if (isTextInputEvent(event)) { if (ic != null) { ic.commitText(String.valueOf((char) event.getUnicodeChar()), 1); } else { SDLInputConnection.nativeCommitText(String.valueOf((char) event.getUnicodeChar()), 1); } } onNativeKeyDown(keyCode); return true; } else if (event.getAction() == KeyEvent.ACTION_UP) { onNativeKeyUp(keyCode); return true; } return false; } /** * This method is called by SDL using JNI. */ public static Surface getNativeSurface() { if (SDLActivity.mSurface == null) { return null; } return SDLActivity.mSurface.getNativeSurface(); } // Input /** * This method is called by SDL using JNI. */ public static void initTouch() { int[] ids = InputDevice.getDeviceIds(); for (int id : ids) { InputDevice device = InputDevice.getDevice(id); /* Allow SOURCE_TOUCHSCREEN and also Virtual InputDevices because they can send TOUCHSCREEN events */ if (device != null && ((device.getSources() & InputDevice.SOURCE_TOUCHSCREEN) == InputDevice.SOURCE_TOUCHSCREEN || device.isVirtual())) { int touchDevId = device.getId(); /* * Prevent id to be -1, since it's used in SDL internal for synthetic events * Appears when using Android emulator, eg: * adb shell input mouse tap 100 100 * adb shell input touchscreen tap 100 100 */ if (touchDevId < 0) { touchDevId -= 1; } nativeAddTouch(touchDevId, device.getName()); } } } // Messagebox /** Result of current messagebox. Also used for blocking the calling thread. */ protected final int[] messageboxSelection = new int[1]; /** * This method is called by SDL using JNI. * Shows the messagebox from UI thread and block calling thread. * buttonFlags, buttonIds and buttonTexts must have same length. * @param buttonFlags array containing flags for every button. * @param buttonIds array containing id for every button. * @param buttonTexts array containing text for every button. * @param colors null for default or array of length 5 containing colors. * @return button id or -1. */ public int messageboxShowMessageBox( final int flags, final String title, final String message, final int[] buttonFlags, final int[] buttonIds, final String[] buttonTexts, final int[] colors) { messageboxSelection[0] = -1; // sanity checks if ((buttonFlags.length != buttonIds.length) && (buttonIds.length != buttonTexts.length)) { return -1; // implementation broken } // collect arguments for Dialog final Bundle args = new Bundle(); args.putInt("flags", flags); args.putString("title", title); args.putString("message", message); args.putIntArray("buttonFlags", buttonFlags); args.putIntArray("buttonIds", buttonIds); args.putStringArray("buttonTexts", buttonTexts); args.putIntArray("colors", colors); // trigger Dialog creation on UI thread runOnUiThread(new Runnable() { @Override public void run() { messageboxCreateAndShow(args); } }); // block the calling thread synchronized (messageboxSelection) { try { messageboxSelection.wait(); } catch (InterruptedException ex) { ex.printStackTrace(); return -1; } } // return selected value return messageboxSelection[0]; } protected void messageboxCreateAndShow(Bundle args) { // TODO set values from "flags" to messagebox dialog // get colors int[] colors = args.getIntArray("colors"); int backgroundColor; int textColor; int buttonBorderColor; int buttonBackgroundColor; int buttonSelectedColor; if (colors != null) { int i = -1; backgroundColor = colors[++i]; textColor = colors[++i]; buttonBorderColor = colors[++i]; buttonBackgroundColor = colors[++i]; buttonSelectedColor = colors[++i]; } else { backgroundColor = Color.TRANSPARENT; textColor = Color.TRANSPARENT; buttonBorderColor = Color.TRANSPARENT; buttonBackgroundColor = Color.TRANSPARENT; buttonSelectedColor = Color.TRANSPARENT; } // create dialog with title and a listener to wake up calling thread final AlertDialog dialog = new AlertDialog.Builder(this).create(); dialog.setTitle(args.getString("title")); dialog.setCancelable(false); dialog.setOnDismissListener(new DialogInterface.OnDismissListener() { @Override public void onDismiss(DialogInterface unused) { synchronized (messageboxSelection) { messageboxSelection.notify(); } } }); // create text TextView message = new TextView(this); message.setGravity(Gravity.CENTER); message.setText(args.getString("message")); if (textColor != Color.TRANSPARENT) { message.setTextColor(textColor); } // create buttons int[] buttonFlags = args.getIntArray("buttonFlags"); int[] buttonIds = args.getIntArray("buttonIds"); String[] buttonTexts = args.getStringArray("buttonTexts"); final SparseArray