Add SDL3 support alongside SDL2

Centralize the differences between SDL2 and SDL3 behind compatibility
interfaces and select the appropriate implementations with the
SYSTEM3_SDL_VERSION CMake option. The default is SDL2 except on Android,
which supports only SDL3.
This commit is contained in:
kichikuou
2026-09-15 15:21:22 +09:00
parent 3ce86d3bde
commit bdb93bf7ed
61 changed files with 1645 additions and 6094 deletions
+2
View File
@@ -4,6 +4,7 @@ on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
name: Android SDL3
steps:
- uses: actions/checkout@v6
@@ -26,5 +27,6 @@ jobs:
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: system3-android-sdl3
path: android/app/build/outputs/apk/release/app-release.apk
archive: false
+11 -5
View File
@@ -8,6 +8,10 @@ env:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
sdl-version: [2, 3]
name: Emscripten SDL${{ matrix.sdl-version }}
steps:
- uses: actions/checkout@v6
@@ -32,9 +36,11 @@ jobs:
- name: Build
run: |
mkdir -p out/wasm
cd out/wasm
emcmake cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_COMPILE_WARNING_AS_ERROR=YES ../../
mkdir -p out/wasm-sdl${{ matrix.sdl-version }}
cd out/wasm-sdl${{ matrix.sdl-version }}
emcmake cmake -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_COMPILE_WARNING_AS_ERROR=YES \
-DSYSTEM3_SDL_VERSION=${{ matrix.sdl-version }} ../..
make -j4
mkdir system3
mv system3.* system3/
@@ -42,5 +48,5 @@ jobs:
- name: Upload artifacts
uses: actions/upload-artifact@v7
with:
name: system3-wasm
path: out/wasm/system3
name: system3-wasm-sdl${{ matrix.sdl-version }}
path: out/wasm-sdl${{ matrix.sdl-version }}/system3
+12 -4
View File
@@ -3,12 +3,13 @@ on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-26.04
strategy:
matrix:
build-type: ["Debug", "Release"]
sdl-version: [2, 3]
name: Linux ${{ matrix.build-type }}
name: Linux SDL${{ matrix.sdl-version }} ${{ matrix.build-type }}
steps:
- uses: actions/checkout@v6
with:
@@ -17,11 +18,18 @@ jobs:
- name: Install Deps
run: |
sudo apt update
sudo apt install libsdl2-dev libsdl2-ttf-dev librtmidi-dev nlohmann-json3-dev ninja-build
sudo apt install librtmidi-dev nlohmann-json3-dev ninja-build
if [ "${{ matrix.sdl-version }}" = 2 ]; then
sudo apt install libsdl2-dev libsdl2-ttf-dev
else
sudo apt install libsdl3-dev libsdl3-ttf-dev
fi
- name: Build
run: |
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=${{ matrix.build-type }}
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \
-DSYSTEM3_SDL_VERSION=${{ matrix.sdl-version }}
cmake --build build
- name: Test
+3 -1
View File
@@ -5,6 +5,7 @@ jobs:
build:
runs-on: ubuntu-latest
container: devkitpro/devkita64:latest
name: Nintendo Switch SDL2
steps:
- uses: actions/checkout@v6
@@ -15,7 +16,8 @@ jobs:
run: |
mkdir -p out/switch
cd out/switch
/opt/devkitpro/portlibs/switch/bin/aarch64-none-elf-cmake -DCMAKE_BUILD_TYPE=Release ../../
/opt/devkitpro/portlibs/switch/bin/aarch64-none-elf-cmake \
-DCMAKE_BUILD_TYPE=Release -DSYSTEM3_SDL_VERSION=2 ../../
make -j4
mkdir system3
mv system3.* system3/
+10 -3
View File
@@ -6,13 +6,20 @@ jobs:
runs-on: windows-latest
strategy:
matrix:
arch: ["Win32", "x64"]
include:
- arch: "Win32"
sdl-version: 2
package: "system3-sdl2-msvc-32bit"
- arch: "x64"
sdl-version: 2
package: "system3-sdl2-msvc-64bit"
name: MSVS ${{ matrix.arch }}
- arch: "Win32"
sdl-version: 3
package: "system3-sdl3-msvc-32bit"
- arch: "x64"
sdl-version: 3
package: "system3-sdl3-msvc-64bit"
name: MSVS SDL${{ matrix.sdl-version }} ${{ matrix.arch }}
steps:
- name: Checkout
@@ -24,7 +31,7 @@ jobs:
run: |
mkdir out
cd out
cmake -A ${{ matrix.arch }} -DCMAKE_COMPILE_WARNING_AS_ERROR=YES ../
cmake -A ${{ matrix.arch }} -DCMAKE_COMPILE_WARNING_AS_ERROR=YES -DSYSTEM3_SDL_VERSION=${{ matrix.sdl-version }} ../
cmake --build . --config Release
cmake --install . --prefix artifacts
cp ../COPYING.txt artifacts/
+14 -4
View File
@@ -10,14 +10,23 @@ jobs:
include:
- sys: mingw32
package: "system3-sdl2-32bit"
sdl-version: 2
sdl-deps: "SDL2:p SDL2_ttf:p"
deps: ""
- sys: ucrt64
package: "system3-sdl2-64bit"
sdl-version: 2
sdl-deps: "SDL2:p SDL2_ttf:p"
deps: "nlohmann-json:p"
- sys: ucrt64
package: "system3-sdl3-64bit"
sdl-version: 3
sdl-deps: "sdl3:p sdl3-ttf:p"
deps: "nlohmann-json:p"
defaults:
run:
shell: msys2 {0}
name: MSYS2 ${{ matrix.sys }}
name: MSYS2 SDL${{ matrix.sdl-version }} ${{ matrix.sys }}
steps:
- name: Set up MSYS2
@@ -25,8 +34,7 @@ jobs:
with:
msystem: ${{ matrix.sys }}
pacboy: >-
SDL2:p
SDL2_ttf:p
${{ matrix.sdl-deps }}
rtmidi:p
${{ matrix.deps }}
@@ -48,7 +56,9 @@ jobs:
- name: Build
run: |
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_COMPILE_WARNING_AS_ERROR=YES
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_COMPILE_WARNING_AS_ERROR=YES \
-DSYSTEM3_SDL_VERSION=${{ matrix.sdl-version }}
cmake --build build
- name: Test
+2
View File
@@ -3,7 +3,9 @@ android/.gradle
android/.idea/
android/app/.cxx/
android/app/.externalNativeBuild
android/app/aars/
android/app/build
android/app/src/main/assets
android/keystore.jks
android/licenses/SDL.txt
android/local.properties
+101 -38
View File
@@ -9,6 +9,29 @@ project(System3 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
include(CTest)
if (ANDROID)
set(_SYSTEM3_DEFAULT_SDL_VERSION "3")
else()
set(_SYSTEM3_DEFAULT_SDL_VERSION "2")
endif()
set(SYSTEM3_SDL_VERSION "${_SYSTEM3_DEFAULT_SDL_VERSION}" CACHE STRING
"SDL major version (2 or 3)")
set_property(CACHE SYSTEM3_SDL_VERSION PROPERTY STRINGS 2 3)
if (NOT SYSTEM3_SDL_VERSION STREQUAL "2" AND
NOT SYSTEM3_SDL_VERSION STREQUAL "3")
message(FATAL_ERROR "SYSTEM3_SDL_VERSION must be 2 or 3")
endif()
add_library(system3_sdl INTERFACE)
target_compile_definitions(system3_sdl INTERFACE
SYSTEM3_SDL_VERSION=${SYSTEM3_SDL_VERSION})
if (SYSTEM3_SDL_VERSION STREQUAL "2")
set(SYSTEM3_AUDIO_BACKEND src/generic/mako.cpp)
else()
set(SYSTEM3_AUDIO_BACKEND src/generic/mako_sdl3.cpp)
endif()
# Generates a static library from pkg_check_modules() result
function(add_static_library name pkg)
add_library(${name} INTERFACE)
@@ -38,14 +61,18 @@ endif()
if (ANDROID)
if (NOT SYSTEM3_SDL_VERSION STREQUAL "3")
message(FATAL_ERROR "Android supports SDL3 only")
endif()
add_library(system3 SHARED)
target_sources(system3 PRIVATE
src/android/nact_android.cpp
src/generic/mako.cpp
${SYSTEM3_AUDIO_BACKEND}
)
find_library(ndk_log log)
target_link_libraries(system3 PRIVATE SDL2 SDL2_ttf ymfm ${ndk_log})
target_link_libraries(system3_sdl INTERFACE SDL3::SDL3 SDL3_ttf::SDL3_ttf)
target_link_libraries(system3 PRIVATE ymfm ${ndk_log})
elseif (EMSCRIPTEN)
option(JSPI "Use JavaScript Promise Integration instead of Asyncify" OFF)
@@ -56,9 +83,14 @@ elseif (EMSCRIPTEN)
src/emscripten/nact_emscripten.cpp
src/emscripten/mako.cpp
)
set(LIBS -sUSE_SDL=2 -sUSE_SDL_TTF=2)
target_compile_options(system3 PRIVATE ${LIBS})
target_link_options(system3 PRIVATE ${LIBS})
set(LIBS
-sUSE_SDL=${SYSTEM3_SDL_VERSION}
-sUSE_SDL_TTF=${SYSTEM3_SDL_VERSION})
if (SYSTEM3_SDL_VERSION STREQUAL "3")
list(APPEND LIBS -Wno-experimental)
endif()
target_compile_options(system3_sdl INTERFACE ${LIBS})
target_link_options(system3_sdl INTERFACE ${LIBS})
target_link_libraries(system3 PRIVATE idbfs.js ymfm)
# Without optimizations, Asyncify generates very large code.
@@ -92,20 +124,43 @@ else() # NOT (ANDROID OR EMSCRIPTEN)
if (MSVC)
include(FetchContent)
FetchContent_Declare(sdl2
URL https://github.com/libsdl-org/SDL/releases/download/release-2.32.10/SDL2-devel-2.32.10-VC.zip
URL_HASH SHA1=27f5179346a0b0db80c4dd1769c7c9d62b9a91f3)
FetchContent_MakeAvailable(sdl2)
find_package(SDL2 REQUIRED CONFIG PATHS ${sdl2_SOURCE_DIR}/cmake NO_DEFAULT_PATH)
get_target_property(SDL2_DLL SDL2::SDL2 IMPORTED_LOCATION)
if (SYSTEM3_SDL_VERSION STREQUAL "2")
FetchContent_Declare(sdl
URL https://github.com/libsdl-org/SDL/releases/download/release-2.32.10/SDL2-devel-2.32.10-VC.zip
URL_HASH SHA1=27f5179346a0b0db80c4dd1769c7c9d62b9a91f3)
FetchContent_MakeAvailable(sdl)
find_package(SDL2 REQUIRED CONFIG PATHS ${sdl_SOURCE_DIR}/cmake NO_DEFAULT_PATH)
set(SDL_TARGET SDL2::SDL2)
set(SDL_MAIN_TARGET SDL2::SDL2main)
FetchContent_Declare(sdl2_ttf
URL https://github.com/libsdl-org/SDL_ttf/releases/download/release-2.24.0/SDL2_ttf-devel-2.24.0-VC.zip
URL_HASH SHA1=2d18b9a4fc2ec0eee80de2a946b088d4e6efd0ee)
FetchContent_MakeAvailable(sdl2_ttf)
find_package(SDL2_ttf REQUIRED CONFIG PATHS ${sdl2_ttf_SOURCE_DIR}/cmake NO_DEFAULT_PATH)
get_target_property(SDL2TTF_DLL SDL2_ttf::SDL2_ttf IMPORTED_LOCATION)
get_filename_component(SDL2TTF_LIBDIR ${SDL2TTF_DLL} DIRECTORY)
FetchContent_Declare(sdl_ttf
URL https://github.com/libsdl-org/SDL_ttf/releases/download/release-2.24.0/SDL2_ttf-devel-2.24.0-VC.zip
URL_HASH SHA1=2d18b9a4fc2ec0eee80de2a946b088d4e6efd0ee)
FetchContent_MakeAvailable(sdl_ttf)
find_package(SDL2_ttf REQUIRED CONFIG PATHS ${sdl_ttf_SOURCE_DIR}/cmake NO_DEFAULT_PATH)
set(SDL_TTF_TARGET SDL2_ttf::SDL2_ttf)
else()
FetchContent_Declare(sdl
URL https://github.com/libsdl-org/SDL/releases/download/release-3.4.16/SDL3-devel-3.4.16-VC.zip
URL_HASH SHA1=edcf1f567837e7464cf80df402661b41acd78dbf)
FetchContent_MakeAvailable(sdl)
find_package(SDL3 REQUIRED CONFIG PATHS ${sdl_SOURCE_DIR}/cmake NO_DEFAULT_PATH)
set(SDL_TARGET SDL3::SDL3)
set(SDL_MAIN_TARGET "")
FetchContent_Declare(sdl_ttf
URL https://github.com/libsdl-org/SDL_ttf/releases/download/release-3.2.2/SDL3_ttf-devel-3.2.2-VC.zip
URL_HASH SHA1=3786bc016d89ca4cf9739e4d342ca29e3e29c137)
FetchContent_MakeAvailable(sdl_ttf)
find_package(SDL3_ttf REQUIRED CONFIG PATHS ${sdl_ttf_SOURCE_DIR}/cmake NO_DEFAULT_PATH)
set(SDL_TTF_TARGET SDL3_ttf::SDL3_ttf)
endif()
get_target_property(SDL_DLL ${SDL_TARGET} IMPORTED_LOCATION)
get_target_property(SDL_TTF_DLL ${SDL_TTF_TARGET} IMPORTED_LOCATION)
get_filename_component(SDL_LIBDIR ${SDL_DLL} DIRECTORY)
get_filename_component(SDL_TTF_LIBDIR ${SDL_TTF_DLL} DIRECTORY)
target_link_libraries(system3_sdl INTERFACE
${SDL_MAIN_TARGET} ${SDL_TARGET} ${SDL_TTF_TARGET})
FetchContent_Declare(rtmidi
URL https://github.com/thestk/rtmidi/archive/refs/tags/6.0.0.zip
@@ -127,36 +182,40 @@ else() # NOT (ANDROID OR EMSCRIPTEN)
target_compile_definitions(system3 PRIVATE _CRT_NONSTDC_NO_DEPRECATE _CRT_SECURE_NO_WARNINGS)
target_compile_options(system3 PRIVATE /utf-8 /wd4244)
target_link_libraries(system3 PRIVATE SDL2::SDL2main SDL2::SDL2 SDL2_ttf::SDL2_ttf)
add_custom_command(TARGET system3 POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:SDL2::SDL2>" "$<TARGET_FILE_DIR:system3>"
"$<TARGET_FILE:${SDL_TARGET}>" "$<TARGET_FILE_DIR:system3>"
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:SDL2_ttf::SDL2_ttf>" "$<TARGET_FILE_DIR:system3>"
"$<TARGET_FILE:${SDL_TTF_TARGET}>" "$<TARGET_FILE_DIR:system3>"
COMMENT "Copying SDL runtime DLLs"
VERBATIM)
set_target_properties(system3 PROPERTIES
WIN32_EXECUTABLE TRUE
VS_DEBUGGER_ENVIRONMENT "PATH=${SDL2_LIBDIR}$<SEMICOLON>${SDL2TTF_LIBDIR}$<SEMICOLON>$ENV{PATH}")
VS_DEBUGGER_ENVIRONMENT "PATH=${SDL_LIBDIR}$<SEMICOLON>${SDL_TTF_LIBDIR}$<SEMICOLON>$ENV{PATH}")
set_directory_properties(PROPERTIES VS_STARTUP_PROJECT system3)
install(FILES ${SDL2_DLL} ${SDL2TTF_DLL} DESTINATION .)
install(FILES ${SDL_DLL} ${SDL_TTF_DLL} DESTINATION .)
install(TARGETS system3 RUNTIME DESTINATION .)
else()
include(FindPkgConfig)
pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2)
pkg_check_modules(SDL2TTF REQUIRED IMPORTED_TARGET SDL2_ttf)
if (MINGW)
add_static_library(sdl2 SDL2)
add_static_library(sdl2_ttf SDL2TTF)
# Workaround for linking error
set_property(TARGET sdl2_ttf PROPERTY INTERFACE_LINK_LIBRARIES
$<LINK_GROUP:RESCAN,${SDL2TTF_STATIC_LIBRARIES}>)
if (SYSTEM3_SDL_VERSION STREQUAL "2")
pkg_check_modules(SDL REQUIRED IMPORTED_TARGET sdl2)
pkg_check_modules(SDLTTF REQUIRED IMPORTED_TARGET SDL2_ttf)
else()
add_library(sdl2 ALIAS PkgConfig::SDL2)
add_library(sdl2_ttf ALIAS PkgConfig::SDL2TTF)
pkg_check_modules(SDL REQUIRED IMPORTED_TARGET sdl3)
pkg_check_modules(SDLTTF REQUIRED IMPORTED_TARGET sdl3-ttf)
endif()
target_link_libraries(system3 PRIVATE sdl2 sdl2_ttf)
if (MINGW)
add_static_library(sdl SDL)
add_static_library(sdl_ttf SDLTTF)
# Workaround for linking error
set_property(TARGET sdl_ttf PROPERTY INTERFACE_LINK_LIBRARIES
$<LINK_GROUP:RESCAN,${SDLTTF_STATIC_LIBRARIES}>)
else()
add_library(sdl ALIAS PkgConfig::SDL)
add_library(sdl_ttf ALIAS PkgConfig::SDLTTF)
endif()
target_link_libraries(system3_sdl INTERFACE sdl sdl_ttf)
find_package(nlohmann_json 3.2.0)
if (nlohmann_json_FOUND)
@@ -180,8 +239,8 @@ else() # NOT (ANDROID OR EMSCRIPTEN)
if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
target_sources(system3 PRIVATE
src/win/nact_win.cpp
src/generic/mako.cpp
src/win/resource.rc
${SYSTEM3_AUDIO_BACKEND}
)
target_compile_definitions(system3 PRIVATE NOMINMAX)
target_link_libraries(system3 PRIVATE winmm)
@@ -191,7 +250,7 @@ else() # NOT (ANDROID OR EMSCRIPTEN)
else()
target_sources(system3 PRIVATE
src/generic/nact_generic.cpp
src/generic/mako.cpp
${SYSTEM3_AUDIO_BACKEND}
)
set(RESOURCE_PATH ${CMAKE_INSTALL_PREFIX}/share/system3/)
install(TARGETS system3 RUNTIME DESTINATION bin)
@@ -243,6 +302,9 @@ if (ENABLE_DEBUGGER)
endif()
if(NINTENDO_SWITCH)
if (SYSTEM3_SDL_VERSION STREQUAL "3")
message(FATAL_ERROR "Nintendo Switch currently supports SDL2 only")
endif()
set(RESOURCE_PATH romfs:/)
nx_generate_nacp(system3.nacp
@@ -259,16 +321,17 @@ if(NINTENDO_SWITCH)
endif()
if (NOT EMSCRIPTEN)
target_sources(system3 PRIVATE src/sys/mako_music.cpp)
target_sources(system3 PRIVATE src/sys/music_decoder.cpp)
target_include_directories(system3 PRIVATE deps/dr_libs deps/stb)
# dr_mp3 and stb_vorbis are amalgamated into this translation unit; silence
# their warnings so they are not promoted to errors by WARNING_AS_ERROR.
set_source_files_properties(src/sys/mako_music.cpp PROPERTIES
set_source_files_properties(src/sys/music_decoder.cpp PROPERTIES
COMPILE_OPTIONS "$<IF:$<CXX_COMPILER_ID:MSVC>,/w,-w>")
endif()
target_compile_definitions(system3 PRIVATE RESOURCE_PATH="${RESOURCE_PATH}")
target_include_directories(system3 PRIVATE src src/sys)
target_link_libraries(system3 PRIVATE system3_sdl)
if (BUILD_TESTING AND NOT ANDROID AND NOT EMSCRIPTEN AND NOT NINTENDO_SWITCH)
add_subdirectory(test)
+30 -12
View File
@@ -1,6 +1,6 @@
# System3 for SDL2
# System3 for SDL2/SDL3
This is an SDL2 port of
This is an SDL port of
[System3 for Win32](http://takeda-toshiya.my.coocan.jp/alice/) by Takeda
Toshiya. It supports multiple platforms, including Android and Emscripten.
@@ -124,6 +124,9 @@ the game ID. You need to specify the `game` option in `system3.ini`.
## Building from Source
Desktop and Emscripten builds use SDL2 by default. Pass CMake option
`-DSYSTEM3_SDL_VERSION=3` to select SDL3.
### Linux (Debian, Ubuntu)
```bash
@@ -131,11 +134,13 @@ $ git submodule update --init
$ sudo apt install g++ cmake libsdl2-dev libsdl2-ttf-dev librtmidi-dev nlohmann-json3-dev
$ mkdir -p out/debug
$ cd out/debug
$ cmake -DCMAKE_BUILD_TYPE=Debug ../../
$ cmake -DCMAKE_BUILD_TYPE=Debug -DSYSTEM3_SDL_VERSION=2 ../../
$ make
$ sudo make install
```
For SDL3, install `libsdl3-dev libsdl3-ttf-dev` and use `-DSYSTEM3_SDL_VERSION=3`.
### MacOS
```bash
@@ -143,11 +148,13 @@ $ git submodule update --init
$ brew install cmake pkg-config sdl2 sdl2_ttf rtmidi nlohmann-json
$ mkdir -p out/debug
$ cd out/debug
$ cmake -DCMAKE_BUILD_TYPE=Debug ../../
$ cmake -DCMAKE_BUILD_TYPE=Debug -DSYSTEM3_SDL_VERSION=2 ../../
$ make
$ sudo make install
```
For SDL3, install `sdl3 sdl3_ttf` and use `-DSYSTEM3_SDL_VERSION=3`.
### Windows (MSYS2)
```bash
@@ -155,27 +162,36 @@ $ git submodule update --init
$ pacman -S make 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-rtmidi mingw-w64-ucrt-x86_64-nlohmann-json
$ mkdir -p out/debug
$ cd out/debug
$ cmake -G"MSYS Makefiles" -DCMAKE_BUILD_TYPE=Debug ../../
$ cmake -G"MSYS Makefiles" -DCMAKE_BUILD_TYPE=Debug -DSYSTEM3_SDL_VERSION=2 ../../
$ make
```
For SDL3, install `mingw-w64-ucrt-x86_64-sdl3 mingw-w64-ucrt-x86_64-sdl3-ttf`
and use `-DSYSTEM3_SDL_VERSION=3`.
### Windows (Microsoft Visual Studio)
- Install Visual Studio 2026 with the **Desktop development with C++**
workload.
- On the Visual Studio start screen, select **Clone a repository** and clone
this repository.
- Wait until the Output window reports `CMake generation finished.`
- Wait until the Output window reports `CMake generation finished.` The
automatically generated default configuration builds with SDL2.
- To build with SDL3, select **Manage Configurations** from the configuration
dropdown, create a separate Configure Preset with the CMake cache variable
`SYSTEM3_SDL_VERSION` set to `3`, and select that preset. Visual Studio then
configures the SDL3 build automatically.
- Select **Build > Build All** to build the project.
The executable and its runtime DLLs are generated under
`out/build/x64-Debug`:
The executable and its runtime DLLs are generated under the active preset's
build directory (`out/build/x64-Debug` for the default configuration):
- `system3.exe`
- `SDL2.dll`
- `SDL2_ttf.dll`
- `SDL2.dll` or `SDL3.dll`
- `SDL2_ttf.dll` or `SDL3_ttf.dll`
To run a game, copy all three files to the game folder and run `system3.exe`.
To run a game, copy `system3.exe` and the two matching SDL DLLs to the game
folder, then run `system3.exe`.
### Emscripten
@@ -183,10 +199,12 @@ To run a game, copy all three files to the game folder and run `system3.exe`.
$ git submodule update --init
$ mkdir -p out/wasm
$ cd out/wasm
$ emcmake cmake -DCMAKE_BUILD_TYPE=Release ../../
$ emcmake cmake -DCMAKE_BUILD_TYPE=Release -DSYSTEM3_SDL_VERSION=2 ../../
$ make
```
For SDL3, use `-DSYSTEM3_SDL_VERSION=3`.
### Android
See [android/README.md](android/README.md).
+37 -1
View File
@@ -8,10 +8,45 @@ else {
}
apply plugin: 'kotlin-android'
// Use SDL3's official prebuilt Android Archive (AAR). The AAR provides SDL3
// to the native CMake build through Prefab. If it is not available locally,
// download the official Android package and extract the AAR and license.
def sdlAarUrl = 'https://github.com/libsdl-org/SDL/releases/download/release-3.4.16/SDL3-devel-3.4.16-android.zip'
def sdlAarName = 'SDL3-3.4.16.aar'
def sdlLicenseName = 'SDL.txt'
def aarsDir = file('aars')
def licensesDir = file('../licenses')
aarsDir.mkdirs()
licensesDir.mkdirs()
def aarFile = new File(aarsDir, sdlAarName)
def licenseFile = new File(licensesDir, sdlLicenseName)
if (!aarFile.exists() || !licenseFile.exists()) {
logger.lifecycle("Downloading ${sdlAarName} ...")
def tmpZip = File.createTempFile('sdl-aar', '.zip')
new URL(sdlAarUrl).withInputStream { input ->
tmpZip.withOutputStream { it << input }
}
copy {
from zipTree(tmpZip)
include sdlAarName
into aarsDir
}
copy {
from zipTree(tmpZip)
include 'LICENSE.txt'
into licensesDir
rename 'LICENSE.txt', sdlLicenseName
}
tmpZip.delete()
}
android {
if (buildAsApplication) {
namespace "io.github.kichikuou.system3"
}
buildFeatures {
prefab true
}
compileSdkVersion 34
defaultConfig {
minSdkVersion 21
@@ -20,7 +55,7 @@ android {
versionName "1.8.0" + gitRevision()
externalNativeBuild {
cmake {
arguments "-DANDROID_APP_PLATFORM=android-19", "-DANDROID_STL=c++_static"
arguments "-DANDROID_STL=c++_static", "-DSYSTEM3_SDL_VERSION=3"
// abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
@@ -84,6 +119,7 @@ android {
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation files(aarFile)
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
}
-1
View File
@@ -1 +0,0 @@
include $(call all-subdir-makefiles)
-10
View File
@@ -1,10 +0,0 @@
# 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
+15 -19
View File
@@ -3,35 +3,31 @@ cmake_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.10/SDL2-2.32.10.tar.gz
URL_HASH SHA1=ce98fa93e31836a751feca374ab28a0770b63c16
)
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
)
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)
find_package(SDL3 REQUIRED CONFIG)
FetchContent_MakeAvailable(SDL)
include(FetchContent)
set(SDLTTF_VENDORED ON CACHE BOOL "Use vendored third-party libraries" FORCE)
set(SDLTTF_HARFBUZZ OFF CACHE BOOL "Use harfbuzz to improve text shaping" FORCE)
set(SDLTTF_PLUTOSVG OFF CACHE BOOL "Enable plutosvg for color SVG fonts" FORCE)
set(SDLTTF_SAMPLES OFF CACHE BOOL "Build the SDL3_ttf sample program(s)" FORCE)
set(SDLTTF_INSTALL OFF CACHE BOOL "Enable SDL3_ttf install target" FORCE)
FetchContent_Declare(SDL_ttf
GIT_REPOSITORY https://github.com/libsdl-org/SDL_ttf.git
GIT_TAG release-3.2.2
GIT_SHALLOW TRUE
GIT_SUBMODULES external/freetype)
FetchContent_MakeAvailable(SDL_ttf)
# The main CMakeLists.txt of system3
add_subdirectory(${PROJECT_ROOT_DIR} src)
# Copy asset files
# Copy license files into the assets directory so they can be shown in-app.
set(ASSETS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../src/main/assets)
set(LICENSES_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../../licenses)
file(MAKE_DIRECTORY ${ASSETS_DIR}/licenses)
file(COPY_FILE ${PROJECT_ROOT_DIR}/COPYING.txt ${ASSETS_DIR}/licenses/system3)
file(COPY_FILE ${PROJECT_ROOT_DIR}/resources/fonts/MTLc3m.ttf.license ${ASSETS_DIR}/licenses/MTLc3m)
file(COPY_FILE ${PROJECT_ROOT_DIR}/deps/ymfm/LICENSE ${ASSETS_DIR}/licenses/ymfm)
file(COPY_FILE ${sdl_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL)
file(COPY_FILE ${LICENSES_DIR}/SDL.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)
-18
View File
@@ -1,18 +0,0 @@
LOCAL_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)
-13
View File
@@ -1,13 +0,0 @@
cmake_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)
@@ -50,7 +50,7 @@ class GameActivity : SDLActivity() {
}
override fun getLibraries(): Array<String> {
return arrayOf("SDL2", "system3")
return arrayOf("SDL3", "system3")
}
override fun getArguments(): Array<String> {
@@ -13,7 +13,6 @@ class LicensesMenuActivity : Activity() {
Entry("SDL", "SDL", "https://www.libsdl.org/"),
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("ymfm", "ymfm", "https://github.com/aaronsgiles/ymfm"),
Entry("MotoyaLCedar W3 mono", "MTLc3m", "https://github.com/aosp-mirror/platform_frameworks_base/tree/lollipop-release/data/fonts"),
)
@@ -1,22 +0,0 @@
package org.libsdl.app;
import android.hardware.usb.UsbDevice;
interface HIDDevice
{
public int getId();
public int getVendorId();
public int getProductId();
public String getSerialNumber();
public int getVersion();
public String getManufacturerName();
public String getProductName();
public UsbDevice getDevice();
public boolean open();
public int sendFeatureReport(byte[] report);
public int sendOutputReport(byte[] report);
public boolean getFeatureReport(byte[] report);
public void setFrozen(boolean frozen);
public void close();
public void shutdown();
}
@@ -1,650 +0,0 @@
package org.libsdl.app;
import android.content.Context;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothGatt;
import android.bluetooth.BluetoothGattCallback;
import android.bluetooth.BluetoothGattCharacteristic;
import android.bluetooth.BluetoothGattDescriptor;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.bluetooth.BluetoothGattService;
import android.hardware.usb.UsbDevice;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.os.*;
//import com.android.internal.util.HexDump;
import java.lang.Runnable;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.UUID;
class HIDDeviceBLESteamController extends BluetoothGattCallback implements HIDDevice {
private static final String TAG = "hidapi";
private HIDDeviceManager mManager;
private BluetoothDevice mDevice;
private int mDeviceId;
private BluetoothGatt mGatt;
private boolean mIsRegistered = false;
private boolean mIsConnected = false;
private boolean mIsChromebook = false;
private boolean mIsReconnecting = false;
private boolean mFrozen = false;
private LinkedList<GattOperation> mOperations;
GattOperation mCurrentOperation = null;
private Handler mHandler;
private static final int TRANSPORT_AUTO = 0;
private static final int TRANSPORT_BREDR = 1;
private static final int TRANSPORT_LE = 2;
private static final int CHROMEBOOK_CONNECTION_CHECK_INTERVAL = 10000;
static 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<GattOperation>();
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();
}
}
@@ -1,698 +0,0 @@
package org.libsdl.app;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
import android.os.Build;
import android.util.Log;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.hardware.usb.*;
import android.os.Handler;
import android.os.Looper;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
public class HIDDeviceManager {
private static final String TAG = "hidapi";
private static final String ACTION_USB_PERMISSION = "org.libsdl.app.USB_PERMISSION";
private static HIDDeviceManager sManager;
private static int sManagerRefCount = 0;
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<Integer, HIDDevice> mDevicesById = new HashMap<Integer, HIDDevice>();
private HashMap<BluetoothDevice, HIDDeviceBLESteamController> mBluetoothDevices = new HashMap<BluetoothDevice, HIDDeviceBLESteamController>();
private int mNextDeviceId = 0;
private SharedPreferences mSharedPreferences = null;
private boolean mIsChromebook = false;
private UsbManager mUsbManager;
private Handler mHandler;
private BluetoothManager mBluetoothManager;
private List<BluetoothDevice> mLastBluetoothDevices;
private final BroadcastReceiver mUsbBroadcast = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(UsbManager.ACTION_USB_DEVICE_ATTACHED)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDeviceAttached(usbDevice);
} else if (action.equals(UsbManager.ACTION_USB_DEVICE_DETACHED)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDeviceDetached(usbDevice);
} else if (action.equals(HIDDeviceManager.ACTION_USB_PERMISSION)) {
UsbDevice usbDevice = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
handleUsbDevicePermission(usbDevice, intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false));
}
}
};
private final BroadcastReceiver mBluetoothBroadcast = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
// Bluetooth device was connected. If it was a Steam Controller, handle it
if (action.equals(BluetoothDevice.ACTION_ACL_CONNECTED)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Log.d(TAG, "Bluetooth device connected: " + device);
if (isSteamController(device)) {
connectBluetoothDevice(device);
}
}
// Bluetooth device was disconnected, remove from controller manager (if any)
if (action.equals(BluetoothDevice.ACTION_ACL_DISCONNECTED)) {
BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
Log.d(TAG, "Bluetooth device disconnected: " + device);
disconnectBluetoothDevice(device);
}
}
};
private HIDDeviceManager(final Context context) {
mContext = context;
HIDDeviceRegisterCallback();
mSharedPreferences = mContext.getSharedPreferences("hidapi", Context.MODE_PRIVATE);
mIsChromebook = 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<Integer> devices = new ArrayList<Integer>();
for (HIDDevice device : mDevicesById.values()) {
if (usbDevice.equals(device.getDevice())) {
devices.add(device.getId());
}
}
for (int id : devices) {
HIDDevice device = mDevicesById.get(id);
mDevicesById.remove(id);
device.shutdown();
HIDDeviceDisconnected(id);
}
}
private void handleUsbDevicePermission(UsbDevice usbDevice, boolean permission_granted) {
for (HIDDevice device : mDevicesById.values()) {
if (usbDevice.equals(device.getDevice())) {
boolean opened = false;
if (permission_granted) {
opened = device.open();
}
HIDDeviceOpenResult(device.getId(), opened);
}
}
}
private void connectHIDDeviceUSB(UsbDevice usbDevice) {
synchronized (this) {
int interface_mask = 0;
for (int interface_index = 0; interface_index < usbDevice.getInterfaceCount(); interface_index++) {
UsbInterface usbInterface = usbDevice.getInterface(interface_index);
if (isHIDDeviceInterface(usbDevice, usbInterface)) {
// Check to see if we've already added this interface
// This happens with the Xbox Series X controller which has a duplicate interface 0, which is inactive
int interface_id = usbInterface.getId();
if ((interface_mask & (1 << interface_id)) != 0) {
continue;
}
interface_mask |= (1 << interface_id);
HIDDeviceUSB device = new HIDDeviceUSB(this, usbDevice, interface_index);
int id = device.getId();
mDevicesById.put(id, device);
HIDDeviceConnected(id, device.getIdentifier(), device.getVendorId(), device.getProductId(), device.getSerialNumber(), device.getVersion(), device.getManufacturerName(), device.getProductName(), usbInterface.getId(), usbInterface.getInterfaceClass(), usbInterface.getInterfaceSubclass(), usbInterface.getInterfaceProtocol());
}
}
}
}
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<BluetoothDevice>();
// final HIDDeviceManager finalThis = this;
// mHandler.postDelayed(new Runnable() {
// @Override
// public void run() {
// finalThis.chromebookConnectionHandler();
// }
// }, 5000);
}
}
private void shutdownBluetooth() {
try {
mContext.unregisterReceiver(mBluetoothBroadcast);
} catch (Exception e) {
// We may not have registered, that's okay
}
}
// Chromebooks do not pass along ACTION_ACL_CONNECTED / ACTION_ACL_DISCONNECTED properly.
// This function provides a sort of dummy version of that, watching for changes in the
// connected devices and attempting to add controllers as things change.
public void chromebookConnectionHandler() {
if (!mIsChromebook) {
return;
}
ArrayList<BluetoothDevice> disconnected = new ArrayList<BluetoothDevice>();
ArrayList<BluetoothDevice> connected = new ArrayList<BluetoothDevice>();
List<BluetoothDevice> currentConnected = mBluetoothManager.getConnectedDevices(BluetoothProfile.GATT);
for (BluetoothDevice bluetoothDevice : currentConnected) {
if (!mLastBluetoothDevices.contains(bluetoothDevice)) {
connected.add(bluetoothDevice);
}
}
for (BluetoothDevice bluetoothDevice : mLastBluetoothDevices) {
if (!currentConnected.contains(bluetoothDevice)) {
disconnected.add(bluetoothDevice);
}
}
mLastBluetoothDevices = currentConnected;
for (BluetoothDevice bluetoothDevice : disconnected) {
disconnectBluetoothDevice(bluetoothDevice);
}
for (BluetoothDevice bluetoothDevice : connected) {
connectBluetoothDevice(bluetoothDevice);
}
final HIDDeviceManager finalThis = this;
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
finalThis.chromebookConnectionHandler();
}
}, 10000);
}
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);
}
@@ -1,309 +0,0 @@
package 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);
}
}
}
}
}
}
@@ -1,90 +0,0 @@
package 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;
}
File diff suppressed because it is too large Load Diff
@@ -1,514 +0,0 @@
package org.libsdl.app;
import android.content.Context;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.os.Build;
import android.util.Log;
import java.util.Arrays;
public class SDLAudioManager {
protected static final String TAG = "SDLAudio";
protected static AudioTrack mAudioTrack;
protected static AudioRecord mAudioRecord;
protected static Context mContext;
private static final int[] NO_DEVICES = {};
private static AudioDeviceCallback mAudioDeviceCallback;
public static void initialize() {
mAudioTrack = null;
mAudioRecord = null;
mAudioDeviceCallback = null;
if(Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */)
{
mAudioDeviceCallback = new AudioDeviceCallback() {
@Override
public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
Arrays.stream(addedDevices).forEach(deviceInfo -> addAudioDevice(deviceInfo.isSink(), deviceInfo.getId()));
}
@Override
public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) {
Arrays.stream(removedDevices).forEach(deviceInfo -> removeAudioDevice(deviceInfo.isSink(), deviceInfo.getId()));
}
};
}
}
public static void setContext(Context context) {
mContext = context;
if (context != null) {
registerAudioDeviceCallback();
}
}
public static void release(Context context) {
unregisterAudioDeviceCallback(context);
}
// Audio
protected static String getAudioFormatString(int audioFormat) {
switch (audioFormat) {
case AudioFormat.ENCODING_PCM_8BIT:
return "8-bit";
case AudioFormat.ENCODING_PCM_16BIT:
return "16-bit";
case AudioFormat.ENCODING_PCM_FLOAT:
return "float";
default:
return Integer.toString(audioFormat);
}
}
protected static int[] open(boolean isCapture, int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
int channelConfig;
int sampleSize;
int frameSize;
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", requested " + desiredFrames + " frames of " + desiredChannels + " channel " + getAudioFormatString(audioFormat) + " audio at " + sampleRate + " Hz");
/* On older devices let's use known good settings */
if (Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) {
if (desiredChannels > 2) {
desiredChannels = 2;
}
}
/* AudioTrack has sample rate limitation of 48000 (fixed in 5.0.2) */
if (Build.VERSION.SDK_INT < 22 /* Android 5.1 (LOLLIPOP_MR1) */) {
if (sampleRate < 8000) {
sampleRate = 8000;
} else if (sampleRate > 48000) {
sampleRate = 48000;
}
}
if (audioFormat == AudioFormat.ENCODING_PCM_FLOAT) {
int minSDKVersion = (isCapture ? 23 /* Android 6.0 (M) */ : 21 /* Android 5.0 (LOLLIPOP) */);
if (Build.VERSION.SDK_INT < minSDKVersion) {
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
}
}
switch (audioFormat)
{
case AudioFormat.ENCODING_PCM_8BIT:
sampleSize = 1;
break;
case AudioFormat.ENCODING_PCM_16BIT:
sampleSize = 2;
break;
case AudioFormat.ENCODING_PCM_FLOAT:
sampleSize = 4;
break;
default:
Log.v(TAG, "Requested format " + audioFormat + ", getting ENCODING_PCM_16BIT");
audioFormat = AudioFormat.ENCODING_PCM_16BIT;
sampleSize = 2;
break;
}
if (isCapture) {
switch (desiredChannels) {
case 1:
channelConfig = AudioFormat.CHANNEL_IN_MONO;
break;
case 2:
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
break;
default:
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
desiredChannels = 2;
channelConfig = AudioFormat.CHANNEL_IN_STEREO;
break;
}
} else {
switch (desiredChannels) {
case 1:
channelConfig = AudioFormat.CHANNEL_OUT_MONO;
break;
case 2:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
case 3:
channelConfig = AudioFormat.CHANNEL_OUT_STEREO | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
break;
case 4:
channelConfig = AudioFormat.CHANNEL_OUT_QUAD;
break;
case 5:
channelConfig = AudioFormat.CHANNEL_OUT_QUAD | AudioFormat.CHANNEL_OUT_FRONT_CENTER;
break;
case 6:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
break;
case 7:
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1 | AudioFormat.CHANNEL_OUT_BACK_CENTER;
break;
case 8:
if (Build.VERSION.SDK_INT >= 23 /* Android 6.0 (M) */) {
channelConfig = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND;
} else {
Log.v(TAG, "Requested " + desiredChannels + " channels, getting 5.1 surround");
desiredChannels = 6;
channelConfig = AudioFormat.CHANNEL_OUT_5POINT1;
}
break;
default:
Log.v(TAG, "Requested " + desiredChannels + " channels, getting stereo");
desiredChannels = 2;
channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
break;
}
/*
Log.v(TAG, "Speaker configuration (and order of channels):");
if ((channelConfig & 0x00000004) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT");
}
if ((channelConfig & 0x00000008) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT");
}
if ((channelConfig & 0x00000010) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_CENTER");
}
if ((channelConfig & 0x00000020) != 0) {
Log.v(TAG, " CHANNEL_OUT_LOW_FREQUENCY");
}
if ((channelConfig & 0x00000040) != 0) {
Log.v(TAG, " CHANNEL_OUT_BACK_LEFT");
}
if ((channelConfig & 0x00000080) != 0) {
Log.v(TAG, " CHANNEL_OUT_BACK_RIGHT");
}
if ((channelConfig & 0x00000100) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_LEFT_OF_CENTER");
}
if ((channelConfig & 0x00000200) != 0) {
Log.v(TAG, " CHANNEL_OUT_FRONT_RIGHT_OF_CENTER");
}
if ((channelConfig & 0x00000400) != 0) {
Log.v(TAG, " CHANNEL_OUT_BACK_CENTER");
}
if ((channelConfig & 0x00000800) != 0) {
Log.v(TAG, " CHANNEL_OUT_SIDE_LEFT");
}
if ((channelConfig & 0x00001000) != 0) {
Log.v(TAG, " CHANNEL_OUT_SIDE_RIGHT");
}
*/
}
frameSize = (sampleSize * desiredChannels);
// Let the user pick a larger buffer if they really want -- but ye
// gods they probably shouldn't, the minimums are horrifyingly high
// latency already
int minBufferSize;
if (isCapture) {
minBufferSize = AudioRecord.getMinBufferSize(sampleRate, channelConfig, audioFormat);
} else {
minBufferSize = AudioTrack.getMinBufferSize(sampleRate, channelConfig, audioFormat);
}
desiredFrames = Math.max(desiredFrames, (minBufferSize + frameSize - 1) / frameSize);
int[] results = new int[4];
if (isCapture) {
if (mAudioRecord == null) {
mAudioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, sampleRate,
channelConfig, audioFormat, desiredFrames * frameSize);
// see notes about AudioTrack state in audioOpen(), above. Probably also applies here.
if (mAudioRecord.getState() != AudioRecord.STATE_INITIALIZED) {
Log.e(TAG, "Failed during initialization of AudioRecord");
mAudioRecord.release();
mAudioRecord = null;
return null;
}
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) {
mAudioRecord.setPreferredDevice(getOutputAudioDeviceInfo(deviceId));
}
mAudioRecord.startRecording();
}
results[0] = mAudioRecord.getSampleRate();
results[1] = mAudioRecord.getAudioFormat();
results[2] = mAudioRecord.getChannelCount();
} else {
if (mAudioTrack == null) {
mAudioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate, channelConfig, audioFormat, desiredFrames * frameSize, AudioTrack.MODE_STREAM);
// Instantiating AudioTrack can "succeed" without an exception and the track may still be invalid
// Ref: https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/AudioTrack.java
// Ref: http://developer.android.com/reference/android/media/AudioTrack.html#getState()
if (mAudioTrack.getState() != AudioTrack.STATE_INITIALIZED) {
/* Try again, with safer values */
Log.e(TAG, "Failed during initialization of Audio Track");
mAudioTrack.release();
mAudioTrack = null;
return null;
}
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */ && deviceId != 0) {
mAudioTrack.setPreferredDevice(getInputAudioDeviceInfo(deviceId));
}
mAudioTrack.play();
}
results[0] = mAudioTrack.getSampleRate();
results[1] = mAudioTrack.getAudioFormat();
results[2] = mAudioTrack.getChannelCount();
}
results[3] = desiredFrames;
Log.v(TAG, "Opening " + (isCapture ? "capture" : "playback") + ", got " + results[3] + " frames of " + results[2] + " channel " + getAudioFormatString(results[1]) + " audio at " + results[0] + " Hz");
return results;
}
private static AudioDeviceInfo getInputAudioDeviceInfo(int deviceId) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS))
.filter(deviceInfo -> deviceInfo.getId() == deviceId)
.findFirst()
.orElse(null);
} else {
return null;
}
}
private static AudioDeviceInfo getOutputAudioDeviceInfo(int deviceId) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS))
.filter(deviceInfo -> deviceInfo.getId() == deviceId)
.findFirst()
.orElse(null);
} else {
return null;
}
}
private static void registerAudioDeviceCallback() {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
audioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null);
}
}
private static void unregisterAudioDeviceCallback(Context context) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
audioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback);
}
}
/**
* This method is called by SDL using JNI.
*/
public static int[] getAudioOutputDevices() {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)).mapToInt(AudioDeviceInfo::getId).toArray();
} else {
return NO_DEVICES;
}
}
/**
* This method is called by SDL using JNI.
*/
public static int[] getAudioInputDevices() {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
AudioManager audioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
return Arrays.stream(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).mapToInt(AudioDeviceInfo::getId).toArray();
} else {
return NO_DEVICES;
}
}
/**
* This method is called by SDL using JNI.
*/
public static int[] audioOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
return open(false, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId);
}
/**
* This method is called by SDL using JNI.
*/
public static void audioWriteFloatBuffer(float[] buffer) {
if (mAudioTrack == null) {
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
return;
}
if (android.os.Build.VERSION.SDK_INT < 21 /* Android 5.0 (LOLLIPOP) */) {
Log.e(TAG, "Attempted to make an incompatible audio call with uninitialized audio! (floating-point output is supported since Android 5.0 Lollipop)");
return;
}
for (int i = 0; i < buffer.length;) {
int result = mAudioTrack.write(buffer, i, buffer.length - i, AudioTrack.WRITE_BLOCKING);
if (result > 0) {
i += result;
} else if (result == 0) {
try {
Thread.sleep(1);
} catch(InterruptedException e) {
// Nom nom
}
} else {
Log.w(TAG, "SDL audio: error return from write(float)");
return;
}
}
}
/**
* This method is called by SDL using JNI.
*/
public static void audioWriteShortBuffer(short[] buffer) {
if (mAudioTrack == null) {
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
return;
}
for (int i = 0; i < buffer.length;) {
int result = mAudioTrack.write(buffer, i, buffer.length - i);
if (result > 0) {
i += result;
} else if (result == 0) {
try {
Thread.sleep(1);
} catch(InterruptedException e) {
// Nom nom
}
} else {
Log.w(TAG, "SDL audio: error return from write(short)");
return;
}
}
}
/**
* This method is called by SDL using JNI.
*/
public static void audioWriteByteBuffer(byte[] buffer) {
if (mAudioTrack == null) {
Log.e(TAG, "Attempted to make audio call with uninitialized audio!");
return;
}
for (int i = 0; i < buffer.length; ) {
int result = mAudioTrack.write(buffer, i, buffer.length - i);
if (result > 0) {
i += result;
} else if (result == 0) {
try {
Thread.sleep(1);
} catch(InterruptedException e) {
// Nom nom
}
} else {
Log.w(TAG, "SDL audio: error return from write(byte)");
return;
}
}
}
/**
* This method is called by SDL using JNI.
*/
public static int[] captureOpen(int sampleRate, int audioFormat, int desiredChannels, int desiredFrames, int deviceId) {
return open(true, sampleRate, audioFormat, desiredChannels, desiredFrames, deviceId);
}
/** This method is called by SDL using JNI. */
public static int captureReadFloatBuffer(float[] buffer, boolean blocking) {
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
return 0;
} else {
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
}
}
/** This method is called by SDL using JNI. */
public static int captureReadShortBuffer(short[] buffer, boolean blocking) {
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
return mAudioRecord.read(buffer, 0, buffer.length);
} else {
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
}
}
/** This method is called by SDL using JNI. */
public static int captureReadByteBuffer(byte[] buffer, boolean blocking) {
if (Build.VERSION.SDK_INT < 23 /* Android 6.0 (M) */) {
return mAudioRecord.read(buffer, 0, buffer.length);
} else {
return mAudioRecord.read(buffer, 0, buffer.length, blocking ? AudioRecord.READ_BLOCKING : AudioRecord.READ_NON_BLOCKING);
}
}
/** This method is called by SDL using JNI. */
public static void audioClose() {
if (mAudioTrack != null) {
mAudioTrack.stop();
mAudioTrack.release();
mAudioTrack = null;
}
}
/** This method is called by SDL using JNI. */
public static void captureClose() {
if (mAudioRecord != null) {
mAudioRecord.stop();
mAudioRecord.release();
mAudioRecord = null;
}
}
/** This method is called by SDL using JNI. */
public static void audioSetThreadPriority(boolean iscapture, int device_id) {
try {
/* Set thread name */
if (iscapture) {
Thread.currentThread().setName("SDLAudioC" + device_id);
} else {
Thread.currentThread().setName("SDLAudioP" + device_id);
}
/* Set thread priority */
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO);
} catch (Exception e) {
Log.v(TAG, "modify thread properties failed " + e.toString());
}
}
public static native int nativeSetupJNI();
public static native void removeAudioDevice(boolean isCapture, int deviceId);
public static native void addAudioDevice(boolean isCapture, int deviceId);
}
@@ -1,856 +0,0 @@
package org.libsdl.app;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import android.content.Context;
import android.os.Build;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.util.Log;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
public class SDLControllerManager
{
public static native int nativeSetupJNI();
public static native int nativeAddJoystick(int device_id, String name, String desc,
int vendor_id, int product_id,
boolean is_accelerometer, int button_mask,
int naxes, int axis_mask, int nhats, int nballs);
public static native int nativeRemoveJoystick(int device_id);
public static native int nativeAddHaptic(int device_id, String name);
public static native int nativeRemoveHaptic(int device_id);
public static native int onNativePadDown(int device_id, int keycode);
public static native int onNativePadUp(int device_id, int keycode);
public static native void onNativeJoy(int device_id, int axis,
float value);
public static native void onNativeHat(int device_id, int hat_id,
int x, int y);
protected static SDLJoystickHandler mJoystickHandler;
protected static SDLHapticHandler mHapticHandler;
private static final String TAG = "SDLControllerManager";
public static void initialize() {
if (mJoystickHandler == null) {
if (Build.VERSION.SDK_INT >= 19 /* Android 4.4 (KITKAT) */) {
mJoystickHandler = new SDLJoystickHandler_API19();
} else {
mJoystickHandler = new SDLJoystickHandler_API16();
}
}
if (mHapticHandler == null) {
if (Build.VERSION.SDK_INT >= 26 /* Android 8.0 (O) */) {
mHapticHandler = new SDLHapticHandler_API26();
} else {
mHapticHandler = new SDLHapticHandler();
}
}
}
// Joystick glue code, just a series of stubs that redirect to the SDLJoystickHandler instance
public static boolean handleJoystickMotionEvent(MotionEvent event) {
return mJoystickHandler.handleMotionEvent(event);
}
/**
* This method is called by SDL using JNI.
*/
public static void pollInputDevices() {
mJoystickHandler.pollInputDevices();
}
/**
* This method is called by SDL using JNI.
*/
public static void pollHapticDevices() {
mHapticHandler.pollHapticDevices();
}
/**
* This method is called by SDL using JNI.
*/
public static void hapticRun(int device_id, float intensity, int length) {
mHapticHandler.run(device_id, intensity, length);
}
/**
* This method is called by SDL using JNI.
*/
public static void hapticStop(int device_id)
{
mHapticHandler.stop(device_id);
}
// Check if a given device is considered a possible SDL joystick
public static boolean isDeviceSDLJoystick(int deviceId) {
InputDevice device = InputDevice.getDevice(deviceId);
// We cannot use InputDevice.isVirtual before API 16, so let's accept
// only nonnegative device ids (VIRTUAL_KEYBOARD equals -1)
if ((device == null) || (deviceId < 0)) {
return false;
}
int sources = device.getSources();
/* This is called for every button press, so let's not spam the logs */
/*
if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
Log.v(TAG, "Input device " + device.getName() + " has class joystick.");
}
if ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) {
Log.v(TAG, "Input device " + device.getName() + " is a dpad.");
}
if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
Log.v(TAG, "Input device " + device.getName() + " is a gamepad.");
}
*/
return ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0 ||
((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) ||
((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
);
}
}
class SDLJoystickHandler {
/**
* Handles given MotionEvent.
* @param event the event to be handled.
* @return if given event was processed.
*/
public boolean handleMotionEvent(MotionEvent event) {
return false;
}
/**
* Handles adding and removing of input devices.
*/
public void pollInputDevices() {
}
}
/* Actual joystick functionality available for API >= 12 devices */
class SDLJoystickHandler_API16 extends SDLJoystickHandler {
static class SDLJoystick {
public int device_id;
public String name;
public String desc;
public ArrayList<InputDevice.MotionRange> axes;
public ArrayList<InputDevice.MotionRange> hats;
}
static class RangeComparator implements Comparator<InputDevice.MotionRange> {
@Override
public int compare(InputDevice.MotionRange arg0, InputDevice.MotionRange arg1) {
// Some controllers, like the Moga Pro 2, return AXIS_GAS (22) for right trigger and AXIS_BRAKE (23) for left trigger - swap them so they're sorted in the right order for SDL
int arg0Axis = arg0.getAxis();
int arg1Axis = arg1.getAxis();
if (arg0Axis == MotionEvent.AXIS_GAS) {
arg0Axis = MotionEvent.AXIS_BRAKE;
} else if (arg0Axis == MotionEvent.AXIS_BRAKE) {
arg0Axis = MotionEvent.AXIS_GAS;
}
if (arg1Axis == MotionEvent.AXIS_GAS) {
arg1Axis = MotionEvent.AXIS_BRAKE;
} else if (arg1Axis == MotionEvent.AXIS_BRAKE) {
arg1Axis = MotionEvent.AXIS_GAS;
}
// Make sure the AXIS_Z is sorted between AXIS_RY and AXIS_RZ.
// This is because the usual pairing are:
// - AXIS_X + AXIS_Y (left stick).
// - AXIS_RX, AXIS_RY (sometimes the right stick, sometimes triggers).
// - AXIS_Z, AXIS_RZ (sometimes the right stick, sometimes triggers).
// This sorts the axes in the above order, which tends to be correct
// for Xbox-ish game pads that have the right stick on RX/RY and the
// triggers on Z/RZ.
//
// Gamepads that don't have AXIS_Z/AXIS_RZ but use
// AXIS_LTRIGGER/AXIS_RTRIGGER are unaffected by this.
//
// References:
// - https://developer.android.com/develop/ui/views/touch-and-input/game-controllers/controller-input
// - https://www.kernel.org/doc/html/latest/input/gamepad.html
if (arg0Axis == MotionEvent.AXIS_Z) {
arg0Axis = MotionEvent.AXIS_RZ - 1;
} else if (arg0Axis > MotionEvent.AXIS_Z && arg0Axis < MotionEvent.AXIS_RZ) {
--arg0Axis;
}
if (arg1Axis == MotionEvent.AXIS_Z) {
arg1Axis = MotionEvent.AXIS_RZ - 1;
} else if (arg1Axis > MotionEvent.AXIS_Z && arg1Axis < MotionEvent.AXIS_RZ) {
--arg1Axis;
}
return arg0Axis - arg1Axis;
}
}
private final ArrayList<SDLJoystick> mJoysticks;
public SDLJoystickHandler_API16() {
mJoysticks = new ArrayList<SDLJoystick>();
}
@Override
public void pollInputDevices() {
int[] deviceIds = InputDevice.getDeviceIds();
for (int device_id : deviceIds) {
if (SDLControllerManager.isDeviceSDLJoystick(device_id)) {
SDLJoystick joystick = getJoystick(device_id);
if (joystick == null) {
InputDevice joystickDevice = InputDevice.getDevice(device_id);
joystick = new SDLJoystick();
joystick.device_id = device_id;
joystick.name = joystickDevice.getName();
joystick.desc = getJoystickDescriptor(joystickDevice);
joystick.axes = new ArrayList<InputDevice.MotionRange>();
joystick.hats = new ArrayList<InputDevice.MotionRange>();
List<InputDevice.MotionRange> ranges = joystickDevice.getMotionRanges();
Collections.sort(ranges, new RangeComparator());
for (InputDevice.MotionRange range : ranges) {
if ((range.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) != 0) {
if (range.getAxis() == MotionEvent.AXIS_HAT_X || range.getAxis() == MotionEvent.AXIS_HAT_Y) {
joystick.hats.add(range);
} else {
joystick.axes.add(range);
}
}
}
mJoysticks.add(joystick);
SDLControllerManager.nativeAddJoystick(joystick.device_id, joystick.name, joystick.desc,
getVendorId(joystickDevice), getProductId(joystickDevice), false,
getButtonMask(joystickDevice), joystick.axes.size(), getAxisMask(joystick.axes), joystick.hats.size()/2, 0);
}
}
}
/* Check removed devices */
ArrayList<Integer> removedDevices = null;
for (SDLJoystick joystick : mJoysticks) {
int device_id = joystick.device_id;
int i;
for (i = 0; i < deviceIds.length; i++) {
if (device_id == deviceIds[i]) break;
}
if (i == deviceIds.length) {
if (removedDevices == null) {
removedDevices = new ArrayList<Integer>();
}
removedDevices.add(device_id);
}
}
if (removedDevices != null) {
for (int device_id : removedDevices) {
SDLControllerManager.nativeRemoveJoystick(device_id);
for (int i = 0; i < mJoysticks.size(); i++) {
if (mJoysticks.get(i).device_id == device_id) {
mJoysticks.remove(i);
break;
}
}
}
}
}
protected SDLJoystick getJoystick(int device_id) {
for (SDLJoystick joystick : mJoysticks) {
if (joystick.device_id == device_id) {
return joystick;
}
}
return null;
}
@Override
public boolean handleMotionEvent(MotionEvent event) {
int actionPointerIndex = event.getActionIndex();
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_MOVE) {
SDLJoystick joystick = getJoystick(event.getDeviceId());
if (joystick != null) {
for (int i = 0; i < joystick.axes.size(); i++) {
InputDevice.MotionRange range = joystick.axes.get(i);
/* Normalize the value to -1...1 */
float value = (event.getAxisValue(range.getAxis(), actionPointerIndex) - range.getMin()) / range.getRange() * 2.0f - 1.0f;
SDLControllerManager.onNativeJoy(joystick.device_id, i, value);
}
for (int i = 0; i < joystick.hats.size() / 2; i++) {
int hatX = Math.round(event.getAxisValue(joystick.hats.get(2 * i).getAxis(), actionPointerIndex));
int hatY = Math.round(event.getAxisValue(joystick.hats.get(2 * i + 1).getAxis(), actionPointerIndex));
SDLControllerManager.onNativeHat(joystick.device_id, i, hatX, hatY);
}
}
}
return true;
}
public String getJoystickDescriptor(InputDevice joystickDevice) {
String desc = joystickDevice.getDescriptor();
if (desc != null && !desc.isEmpty()) {
return desc;
}
return joystickDevice.getName();
}
public int getProductId(InputDevice joystickDevice) {
return 0;
}
public int getVendorId(InputDevice joystickDevice) {
return 0;
}
public int getAxisMask(List<InputDevice.MotionRange> ranges) {
return -1;
}
public int getButtonMask(InputDevice joystickDevice) {
return -1;
}
}
class SDLJoystickHandler_API19 extends SDLJoystickHandler_API16 {
@Override
public int getProductId(InputDevice joystickDevice) {
return joystickDevice.getProductId();
}
@Override
public int getVendorId(InputDevice joystickDevice) {
return joystickDevice.getVendorId();
}
@Override
public int getAxisMask(List<InputDevice.MotionRange> ranges) {
// For compatibility, keep computing the axis mask like before,
// only really distinguishing 2, 4 and 6 axes.
int axis_mask = 0;
if (ranges.size() >= 2) {
// ((1 << SDL_GAMEPAD_AXIS_LEFTX) | (1 << SDL_GAMEPAD_AXIS_LEFTY))
axis_mask |= 0x0003;
}
if (ranges.size() >= 4) {
// ((1 << SDL_GAMEPAD_AXIS_RIGHTX) | (1 << SDL_GAMEPAD_AXIS_RIGHTY))
axis_mask |= 0x000c;
}
if (ranges.size() >= 6) {
// ((1 << SDL_GAMEPAD_AXIS_LEFT_TRIGGER) | (1 << SDL_GAMEPAD_AXIS_RIGHT_TRIGGER))
axis_mask |= 0x0030;
}
// Also add an indicator bit for whether the sorting order has changed.
// This serves to disable outdated gamecontrollerdb.txt mappings.
boolean have_z = false;
boolean have_past_z_before_rz = false;
for (InputDevice.MotionRange range : ranges) {
int axis = range.getAxis();
if (axis == MotionEvent.AXIS_Z) {
have_z = true;
} else if (axis > MotionEvent.AXIS_Z && axis < MotionEvent.AXIS_RZ) {
have_past_z_before_rz = true;
}
}
if (have_z && have_past_z_before_rz) {
// If both these exist, the compare() function changed sorting order.
// Set a bit to indicate this fact.
axis_mask |= 0x8000;
}
return axis_mask;
}
@Override
public int getButtonMask(InputDevice joystickDevice) {
int button_mask = 0;
int[] keys = new int[] {
KeyEvent.KEYCODE_BUTTON_A,
KeyEvent.KEYCODE_BUTTON_B,
KeyEvent.KEYCODE_BUTTON_X,
KeyEvent.KEYCODE_BUTTON_Y,
KeyEvent.KEYCODE_BACK,
KeyEvent.KEYCODE_MENU,
KeyEvent.KEYCODE_BUTTON_MODE,
KeyEvent.KEYCODE_BUTTON_START,
KeyEvent.KEYCODE_BUTTON_THUMBL,
KeyEvent.KEYCODE_BUTTON_THUMBR,
KeyEvent.KEYCODE_BUTTON_L1,
KeyEvent.KEYCODE_BUTTON_R1,
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT,
KeyEvent.KEYCODE_BUTTON_SELECT,
KeyEvent.KEYCODE_DPAD_CENTER,
// These don't map into any SDL controller buttons directly
KeyEvent.KEYCODE_BUTTON_L2,
KeyEvent.KEYCODE_BUTTON_R2,
KeyEvent.KEYCODE_BUTTON_C,
KeyEvent.KEYCODE_BUTTON_Z,
KeyEvent.KEYCODE_BUTTON_1,
KeyEvent.KEYCODE_BUTTON_2,
KeyEvent.KEYCODE_BUTTON_3,
KeyEvent.KEYCODE_BUTTON_4,
KeyEvent.KEYCODE_BUTTON_5,
KeyEvent.KEYCODE_BUTTON_6,
KeyEvent.KEYCODE_BUTTON_7,
KeyEvent.KEYCODE_BUTTON_8,
KeyEvent.KEYCODE_BUTTON_9,
KeyEvent.KEYCODE_BUTTON_10,
KeyEvent.KEYCODE_BUTTON_11,
KeyEvent.KEYCODE_BUTTON_12,
KeyEvent.KEYCODE_BUTTON_13,
KeyEvent.KEYCODE_BUTTON_14,
KeyEvent.KEYCODE_BUTTON_15,
KeyEvent.KEYCODE_BUTTON_16,
};
int[] masks = new int[] {
(1 << 0), // A -> A
(1 << 1), // B -> B
(1 << 2), // X -> X
(1 << 3), // Y -> Y
(1 << 4), // BACK -> BACK
(1 << 6), // MENU -> START
(1 << 5), // MODE -> GUIDE
(1 << 6), // START -> START
(1 << 7), // THUMBL -> LEFTSTICK
(1 << 8), // THUMBR -> RIGHTSTICK
(1 << 9), // L1 -> LEFTSHOULDER
(1 << 10), // R1 -> RIGHTSHOULDER
(1 << 11), // DPAD_UP -> DPAD_UP
(1 << 12), // DPAD_DOWN -> DPAD_DOWN
(1 << 13), // DPAD_LEFT -> DPAD_LEFT
(1 << 14), // DPAD_RIGHT -> DPAD_RIGHT
(1 << 4), // SELECT -> BACK
(1 << 0), // DPAD_CENTER -> A
(1 << 15), // L2 -> ??
(1 << 16), // R2 -> ??
(1 << 17), // C -> ??
(1 << 18), // Z -> ??
(1 << 20), // 1 -> ??
(1 << 21), // 2 -> ??
(1 << 22), // 3 -> ??
(1 << 23), // 4 -> ??
(1 << 24), // 5 -> ??
(1 << 25), // 6 -> ??
(1 << 26), // 7 -> ??
(1 << 27), // 8 -> ??
(1 << 28), // 9 -> ??
(1 << 29), // 10 -> ??
(1 << 30), // 11 -> ??
(1 << 31), // 12 -> ??
// We're out of room...
0xFFFFFFFF, // 13 -> ??
0xFFFFFFFF, // 14 -> ??
0xFFFFFFFF, // 15 -> ??
0xFFFFFFFF, // 16 -> ??
};
boolean[] has_keys = joystickDevice.hasKeys(keys);
for (int i = 0; i < keys.length; ++i) {
if (has_keys[i]) {
button_mask |= masks[i];
}
}
return button_mask;
}
}
class SDLHapticHandler_API26 extends SDLHapticHandler {
@Override
public void run(int device_id, float intensity, int length) {
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
Log.d("SDL", "Rtest: Vibe with intensity " + intensity + " for " + length);
if (intensity == 0.0f) {
stop(device_id);
return;
}
int vibeValue = Math.round(intensity * 255);
if (vibeValue > 255) {
vibeValue = 255;
}
if (vibeValue < 1) {
stop(device_id);
return;
}
try {
haptic.vib.vibrate(VibrationEffect.createOneShot(length, vibeValue));
}
catch (Exception e) {
// Fall back to the generic method, which uses DEFAULT_AMPLITUDE, but works even if
// something went horribly wrong with the Android 8.0 APIs.
haptic.vib.vibrate(length);
}
}
}
}
class SDLHapticHandler {
static class SDLHaptic {
public int device_id;
public String name;
public Vibrator vib;
}
private final ArrayList<SDLHaptic> mHaptics;
public SDLHapticHandler() {
mHaptics = new ArrayList<SDLHaptic>();
}
public void run(int device_id, float intensity, int length) {
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
haptic.vib.vibrate(length);
}
}
public void stop(int device_id) {
SDLHaptic haptic = getHaptic(device_id);
if (haptic != null) {
haptic.vib.cancel();
}
}
public void pollHapticDevices() {
final int deviceId_VIBRATOR_SERVICE = 999999;
boolean hasVibratorService = false;
int[] deviceIds = InputDevice.getDeviceIds();
// It helps processing the device ids in reverse order
// For example, in the case of the XBox 360 wireless dongle,
// so the first controller seen by SDL matches what the receiver
// considers to be the first controller
for (int i = deviceIds.length - 1; i > -1; i--) {
SDLHaptic haptic = getHaptic(deviceIds[i]);
if (haptic == null) {
InputDevice device = InputDevice.getDevice(deviceIds[i]);
Vibrator vib = device.getVibrator();
if (vib != null) {
if (vib.hasVibrator()) {
haptic = new SDLHaptic();
haptic.device_id = deviceIds[i];
haptic.name = device.getName();
haptic.vib = vib;
mHaptics.add(haptic);
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
}
}
}
}
/* Check VIBRATOR_SERVICE */
Vibrator vib = (Vibrator) SDL.getContext().getSystemService(Context.VIBRATOR_SERVICE);
if (vib != null) {
hasVibratorService = vib.hasVibrator();
if (hasVibratorService) {
SDLHaptic haptic = getHaptic(deviceId_VIBRATOR_SERVICE);
if (haptic == null) {
haptic = new SDLHaptic();
haptic.device_id = deviceId_VIBRATOR_SERVICE;
haptic.name = "VIBRATOR_SERVICE";
haptic.vib = vib;
mHaptics.add(haptic);
SDLControllerManager.nativeAddHaptic(haptic.device_id, haptic.name);
}
}
}
/* Check removed devices */
ArrayList<Integer> removedDevices = null;
for (SDLHaptic haptic : mHaptics) {
int device_id = haptic.device_id;
int i;
for (i = 0; i < deviceIds.length; i++) {
if (device_id == deviceIds[i]) break;
}
if (device_id != deviceId_VIBRATOR_SERVICE || !hasVibratorService) {
if (i == deviceIds.length) {
if (removedDevices == null) {
removedDevices = new ArrayList<Integer>();
}
removedDevices.add(device_id);
}
} // else: don't remove the vibrator if it is still present
}
if (removedDevices != null) {
for (int device_id : removedDevices) {
SDLControllerManager.nativeRemoveHaptic(device_id);
for (int i = 0; i < mHaptics.size(); i++) {
if (mHaptics.get(i).device_id == device_id) {
mHaptics.remove(i);
break;
}
}
}
}
}
protected SDLHaptic getHaptic(int device_id) {
for (SDLHaptic haptic : mHaptics) {
if (haptic.device_id == device_id) {
return haptic;
}
}
return null;
}
}
class SDLGenericMotionListener_API12 implements View.OnGenericMotionListener {
// Generic Motion (mouse hover, joystick...) events go here
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
float x, y;
int action;
switch ( event.getSource() ) {
case InputDevice.SOURCE_JOYSTICK:
return SDLControllerManager.handleJoystickMotionEvent(event);
case InputDevice.SOURCE_MOUSE:
action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
x = event.getX(0);
y = event.getY(0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
default:
break;
}
break;
default:
break;
}
// Event was not managed
return false;
}
public boolean supportsRelativeMouse() {
return false;
}
public boolean inRelativeMode() {
return false;
}
public boolean setRelativeMouseEnabled(boolean enabled) {
return false;
}
public void reclaimRelativeMouseModeIfNeeded()
{
}
public float getEventX(MotionEvent event) {
return event.getX(0);
}
public float getEventY(MotionEvent event) {
return event.getY(0);
}
}
class SDLGenericMotionListener_API24 extends SDLGenericMotionListener_API12 {
// Generic Motion (mouse hover, joystick...) events go here
private boolean mRelativeModeEnabled;
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
// Handle relative mouse mode
if (mRelativeModeEnabled) {
if (event.getSource() == InputDevice.SOURCE_MOUSE) {
int action = event.getActionMasked();
if (action == MotionEvent.ACTION_HOVER_MOVE) {
float x = event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
float y = event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
SDLActivity.onNativeMouse(0, action, x, y, true);
return true;
}
}
}
// Event was not managed, call SDLGenericMotionListener_API12 method
return super.onGenericMotion(v, event);
}
@Override
public boolean supportsRelativeMouse() {
return true;
}
@Override
public boolean inRelativeMode() {
return mRelativeModeEnabled;
}
@Override
public boolean setRelativeMouseEnabled(boolean enabled) {
mRelativeModeEnabled = enabled;
return true;
}
@Override
public float getEventX(MotionEvent event) {
if (mRelativeModeEnabled) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_X);
} else {
return event.getX(0);
}
}
@Override
public float getEventY(MotionEvent event) {
if (mRelativeModeEnabled) {
return event.getAxisValue(MotionEvent.AXIS_RELATIVE_Y);
} else {
return event.getY(0);
}
}
}
class SDLGenericMotionListener_API26 extends SDLGenericMotionListener_API24 {
// Generic Motion (mouse hover, joystick...) events go here
private boolean mRelativeModeEnabled;
@Override
public boolean onGenericMotion(View v, MotionEvent event) {
float x, y;
int action;
switch ( event.getSource() ) {
case InputDevice.SOURCE_JOYSTICK:
return SDLControllerManager.handleJoystickMotionEvent(event);
case InputDevice.SOURCE_MOUSE:
// DeX desktop mouse cursor is a separate non-standard input type.
case InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN:
action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
x = event.getX(0);
y = event.getY(0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
default:
break;
}
break;
case InputDevice.SOURCE_MOUSE_RELATIVE:
action = event.getActionMasked();
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
x = event.getX(0);
y = event.getY(0);
SDLActivity.onNativeMouse(0, action, x, y, true);
return true;
default:
break;
}
break;
default:
break;
}
// Event was not managed
return false;
}
@Override
public boolean supportsRelativeMouse() {
return (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */);
}
@Override
public boolean inRelativeMode() {
return mRelativeModeEnabled;
}
@Override
public boolean setRelativeMouseEnabled(boolean enabled) {
if (!SDLActivity.isDeXMode() || Build.VERSION.SDK_INT >= 27 /* Android 8.1 (O_MR1) */) {
if (enabled) {
SDLActivity.getContentView().requestPointerCapture();
} else {
SDLActivity.getContentView().releasePointerCapture();
}
mRelativeModeEnabled = enabled;
return true;
} else {
return false;
}
}
@Override
public void reclaimRelativeMouseModeIfNeeded()
{
if (mRelativeModeEnabled && !SDLActivity.isDeXMode()) {
SDLActivity.getContentView().requestPointerCapture();
}
}
@Override
public float getEventX(MotionEvent event) {
// Relative mouse in capture mode will only have relative for X/Y
return event.getX(0);
}
@Override
public float getEventY(MotionEvent event) {
// Relative mouse in capture mode will only have relative for X/Y
return event.getY(0);
}
}
@@ -1,405 +0,0 @@
package org.libsdl.app;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Build;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.WindowManager;
/**
SDLSurface. This is what we draw on, so we need to know when it's created
in order to do anything useful.
Because of this, that's where we set up the SDL thread
*/
public class SDLSurface extends SurfaceView implements SurfaceHolder.Callback,
View.OnKeyListener, View.OnTouchListener, SensorEventListener {
// Sensors
protected SensorManager mSensorManager;
protected Display mDisplay;
// Keep track of the surface size to normalize touch events
protected float mWidth, mHeight;
// Is SurfaceView ready for rendering
public boolean mIsSurfaceReady;
// Startup
public SDLSurface(Context context) {
super(context);
getHolder().addCallback(this);
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
setOnKeyListener(this);
setOnTouchListener(this);
mDisplay = ((WindowManager)context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
setOnGenericMotionListener(SDLActivity.getMotionListener());
// Some arbitrary defaults to avoid a potential division by zero
mWidth = 1.0f;
mHeight = 1.0f;
mIsSurfaceReady = false;
}
public void handlePause() {
enableSensor(Sensor.TYPE_ACCELEROMETER, false);
}
public void handleResume() {
setFocusable(true);
setFocusableInTouchMode(true);
requestFocus();
setOnKeyListener(this);
setOnTouchListener(this);
enableSensor(Sensor.TYPE_ACCELEROMETER, true);
}
public Surface getNativeSurface() {
return getHolder().getSurface();
}
// Called when we have a valid drawing surface
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.v("SDL", "surfaceCreated()");
SDLActivity.onNativeSurfaceCreated();
}
// Called when we lose the surface
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.v("SDL", "surfaceDestroyed()");
// Transition to pause, if needed
SDLActivity.mNextNativeState = SDLActivity.NativeState.PAUSED;
SDLActivity.handleNativeState();
mIsSurfaceReady = false;
SDLActivity.onNativeSurfaceDestroyed();
}
// Called when the surface is resized
@Override
public void surfaceChanged(SurfaceHolder holder,
int format, int width, int height) {
Log.v("SDL", "surfaceChanged()");
if (SDLActivity.mSingleton == null) {
return;
}
mWidth = width;
mHeight = height;
int nDeviceWidth = width;
int nDeviceHeight = height;
try
{
if (Build.VERSION.SDK_INT >= 17 /* Android 4.2 (JELLY_BEAN_MR1) */) {
DisplayMetrics realMetrics = new DisplayMetrics();
mDisplay.getRealMetrics( realMetrics );
nDeviceWidth = realMetrics.widthPixels;
nDeviceHeight = realMetrics.heightPixels;
}
} catch(Exception ignored) {
}
synchronized(SDLActivity.getContext()) {
// In case we're waiting on a size change after going fullscreen, send a notification.
SDLActivity.getContext().notifyAll();
}
Log.v("SDL", "Window size: " + width + "x" + height);
Log.v("SDL", "Device size: " + nDeviceWidth + "x" + nDeviceHeight);
SDLActivity.nativeSetScreenResolution(width, height, nDeviceWidth, nDeviceHeight, mDisplay.getRefreshRate());
SDLActivity.onNativeResize();
// Prevent a screen distortion glitch,
// for instance when the device is in Landscape and a Portrait App is resumed.
boolean skip = false;
int requestedOrientation = SDLActivity.mSingleton.getRequestedOrientation();
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT) {
if (mWidth > mHeight) {
skip = true;
}
} else if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE || requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE) {
if (mWidth < mHeight) {
skip = true;
}
}
// Special Patch for Square Resolution: Black Berry Passport
if (skip) {
double min = Math.min(mWidth, mHeight);
double max = Math.max(mWidth, mHeight);
if (max / min < 1.20) {
Log.v("SDL", "Don't skip on such aspect-ratio. Could be a square resolution.");
skip = false;
}
}
// Don't skip in MultiWindow.
if (skip) {
if (Build.VERSION.SDK_INT >= 24 /* Android 7.0 (N) */) {
if (SDLActivity.mSingleton.isInMultiWindowMode()) {
Log.v("SDL", "Don't skip in Multi-Window");
skip = false;
}
}
}
if (skip) {
Log.v("SDL", "Skip .. Surface is not ready.");
mIsSurfaceReady = false;
return;
}
/* If the surface has been previously destroyed by onNativeSurfaceDestroyed, recreate it here */
SDLActivity.onNativeSurfaceChanged();
/* Surface is ready */
mIsSurfaceReady = true;
SDLActivity.mNextNativeState = SDLActivity.NativeState.RESUMED;
SDLActivity.handleNativeState();
}
// Key events
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
return SDLActivity.handleKeyEvent(v, keyCode, event, null);
}
// Touch events
@Override
public boolean onTouch(View v, MotionEvent event) {
/* Ref: http://developer.android.com/training/gestures/multi.html */
int touchDevId = event.getDeviceId();
final int pointerCount = event.getPointerCount();
int action = event.getActionMasked();
int pointerFingerId;
int i = -1;
float x,y,p;
/*
* 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;
}
// 12290 = Samsung DeX mode desktop mouse
// 12290 = 0x3002 = 0x2002 | 0x1002 = SOURCE_MOUSE | SOURCE_TOUCHSCREEN
// 0x2 = SOURCE_CLASS_POINTER
if (event.getSource() == InputDevice.SOURCE_MOUSE || event.getSource() == (InputDevice.SOURCE_MOUSE | InputDevice.SOURCE_TOUCHSCREEN)) {
int mouseButton = 1;
try {
Object object = event.getClass().getMethod("getButtonState").invoke(event);
if (object != null) {
mouseButton = (Integer) object;
}
} catch(Exception ignored) {
}
// We need to check if we're in relative mouse mode and get the axis offset rather than the x/y values
// if we are. We'll leverage our existing mouse motion listener
SDLGenericMotionListener_API12 motionListener = SDLActivity.getMotionListener();
x = motionListener.getEventX(event);
y = motionListener.getEventY(event);
SDLActivity.onNativeMouse(mouseButton, action, x, y, motionListener.inRelativeMode());
} else {
switch(action) {
case MotionEvent.ACTION_MOVE:
for (i = 0; i < pointerCount; i++) {
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_DOWN:
// Primary pointer up/down, the index is always zero
i = 0;
/* fallthrough */
case MotionEvent.ACTION_POINTER_UP:
case MotionEvent.ACTION_POINTER_DOWN:
// Non primary pointer up/down
if (i == -1) {
i = event.getActionIndex();
}
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, action, x, y, p);
break;
case MotionEvent.ACTION_CANCEL:
for (i = 0; i < pointerCount; i++) {
pointerFingerId = event.getPointerId(i);
x = event.getX(i) / mWidth;
y = event.getY(i) / mHeight;
p = event.getPressure(i);
if (p > 1.0f) {
// may be larger than 1.0f on some devices
// see the documentation of getPressure(i)
p = 1.0f;
}
SDLActivity.onNativeTouch(touchDevId, pointerFingerId, MotionEvent.ACTION_UP, x, y, p);
}
break;
default:
break;
}
}
return true;
}
// Sensor events
public void enableSensor(int sensortype, boolean enabled) {
// TODO: This uses getDefaultSensor - what if we have >1 accels?
if (enabled) {
mSensorManager.registerListener(this,
mSensorManager.getDefaultSensor(sensortype),
SensorManager.SENSOR_DELAY_GAME, null);
} else {
mSensorManager.unregisterListener(this,
mSensorManager.getDefaultSensor(sensortype));
}
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// TODO
}
@Override
public void onSensorChanged(SensorEvent event) {
if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
// Since we may have an orientation set, we won't receive onConfigurationChanged events.
// We thus should check here.
int newOrientation;
float x, y;
switch (mDisplay.getRotation()) {
case Surface.ROTATION_90:
x = -event.values[1];
y = event.values[0];
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE;
break;
case Surface.ROTATION_270:
x = event.values[1];
y = -event.values[0];
newOrientation = SDLActivity.SDL_ORIENTATION_LANDSCAPE_FLIPPED;
break;
case Surface.ROTATION_180:
x = -event.values[0];
y = -event.values[1];
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT_FLIPPED;
break;
case Surface.ROTATION_0:
default:
x = event.values[0];
y = event.values[1];
newOrientation = SDLActivity.SDL_ORIENTATION_PORTRAIT;
break;
}
if (newOrientation != SDLActivity.mCurrentOrientation) {
SDLActivity.mCurrentOrientation = newOrientation;
SDLActivity.onNativeOrientationChanged(newOrientation);
}
SDLActivity.onNativeAccel(-x / SensorManager.GRAVITY_EARTH,
y / SensorManager.GRAVITY_EARTH,
event.values[2] / SensorManager.GRAVITY_EARTH);
}
}
// Captured pointer events for API 26.
public boolean onCapturedPointerEvent(MotionEvent event)
{
int action = event.getActionMasked();
float x, y;
switch (action) {
case MotionEvent.ACTION_SCROLL:
x = event.getAxisValue(MotionEvent.AXIS_HSCROLL, 0);
y = event.getAxisValue(MotionEvent.AXIS_VSCROLL, 0);
SDLActivity.onNativeMouse(0, action, x, y, false);
return true;
case MotionEvent.ACTION_HOVER_MOVE:
case MotionEvent.ACTION_MOVE:
x = event.getX(0);
y = event.getY(0);
SDLActivity.onNativeMouse(0, action, x, y, true);
return true;
case MotionEvent.ACTION_BUTTON_PRESS:
case MotionEvent.ACTION_BUTTON_RELEASE:
// Change our action value to what SDL's code expects.
if (action == MotionEvent.ACTION_BUTTON_PRESS) {
action = MotionEvent.ACTION_DOWN;
} else { /* MotionEvent.ACTION_BUTTON_RELEASE */
action = MotionEvent.ACTION_UP;
}
x = event.getX(0);
y = event.getY(0);
int button = event.getButtonState();
SDLActivity.onNativeMouse(button, action, x, y, true);
return true;
}
return false;
}
}
+2 -2
View File
@@ -7,8 +7,8 @@
class JNILocalFrame {
public:
JNILocalFrame(int n) :
env_((JNIEnv*)SDL_AndroidGetJNIEnv()),
context_((jobject)SDL_AndroidGetActivity())
env_((JNIEnv*)SDL_GetAndroidJNIEnv()),
context_((jobject)SDL_GetAndroidActivity())
{
if (env_->PushLocalFrame(n) < 0) {
ERROR("Failed to allocate JVM local references");
-5
View File
@@ -41,9 +41,4 @@ enum CustomEvent {
DISABLE_CD_MENU,
};
// resource.cpp
struct SDL_RWops;
SDL_RWops* open_resource(const char* name, const char* type);
SDL_RWops* open_file(const char* name);
#endif
+1 -1
View File
@@ -3,7 +3,7 @@
#include "debugger/frontend.h"
#include <algorithm>
#include <queue>
#include <SDL.h>
#include "sdl_compat.h"
#include "nlohmann/json.hpp"
#include "common.h"
#include "encoding.h"
+2 -2
View File
@@ -4,7 +4,7 @@
*/
#include <memory>
#include <SDL.h>
#include "sdl_compat.h"
#include <emscripten.h>
#include "mako.h"
#include "fm/mako_ymfm.h"
@@ -118,7 +118,7 @@ void MAKO::play_pcm(int page, int loops)
std::vector<uint8_t> buffer = amse.load(page);
if (!buffer.empty()) {
// AMSE形式 (乙女戦記)
uint32_t amse_size = SDL_SwapLE32(*reinterpret_cast<uint32_t*>(&buffer[8]));
uint32_t amse_size = sdl::Swap32LE(*reinterpret_cast<uint32_t*>(&buffer[8]));
int samples = (amse_size - 12) * 2;
int total = samples + 0x24;
+85 -5
View File
@@ -8,11 +8,11 @@
#include <string>
#include <vector>
#include <limits.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "mako.h"
#include "mako_midi.h"
#include "mako_music.h"
#include "music_decoder.h"
#include "fm/mako_ymfm.h"
#include "config.h"
#include "dri.h"
@@ -20,6 +20,86 @@
namespace {
// Plays a single BGM file (MP3, OGG, or WAV, chosen by file extension),
// decoding on demand and mixing into the audio callback's output buffer.
class Music {
public:
// loops: number of times to play; 0 means loop forever.
Music(const std::string& path, int loops, const SDL_AudioSpec& device_spec)
: decoder(create_music_decoder(path)), loops_(loops)
{
if (!decoder)
return;
const SDL_AudioSpec& src_spec = decoder->spec();
stream = SDL_NewAudioStream(src_spec.format, src_spec.channels, src_spec.freq,
device_spec.format, device_spec.channels, device_spec.freq);
if (!stream) {
WARNING("SDL_NewAudioStream failed: %s", SDL_GetError());
return;
}
playing = true;
}
~Music()
{
if (stream)
SDL_FreeAudioStream(stream);
}
bool is_open() const { return stream != nullptr; }
bool is_playing() const { return playing; }
// Called from the audio callback.
void mix(Uint8* out, int len)
{
if (!stream || !playing)
return;
while (SDL_AudioStreamAvailable(stream) < len && !input_finished)
decode();
Uint8* tmp = SDL_stack_alloc(Uint8, len);
int got = SDL_AudioStreamGet(stream, tmp, len);
if (got > 0)
SDL_MixAudioFormat(out, tmp, AUDIO_S16SYS, got, SDL_MIX_MAXVOLUME);
SDL_stack_free(tmp);
if (input_finished && SDL_AudioStreamAvailable(stream) <= 0)
playing = false;
}
private:
// Decodes and queues one chunk, handling EOF and decoder errors.
void decode()
{
constexpr int CHUNK_FRAMES = 1024;
DecodedChunk chunk = decoder->decode(CHUNK_FRAMES);
if (chunk.frames == 0) {
if (loops_ && --loops_ == 0) {
SDL_AudioStreamFlush(stream);
input_finished = true;
} else {
decoder->seek_start();
}
return;
}
const SDL_AudioSpec& spec = decoder->spec();
int bytes = chunk.frames * SDL_AUDIO_BITSIZE(spec.format) / 8 * spec.channels;
if (SDL_AudioStreamPut(stream, chunk.data, bytes) < 0) {
WARNING("SDL_AudioStreamPut failed: %s", SDL_GetError());
input_finished = true;
}
}
std::unique_ptr<MusicDecoder> decoder;
SDL_AudioStream* stream = nullptr;
int loops_; // number of times to play, 0 for infinite loop
bool playing = false;
bool input_finished = false;
};
const int SAMPLE_RATE = 44100;
#ifdef _WIN32
@@ -36,7 +116,7 @@ const int8_t DPSALL_tracks[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
SDL_AudioDeviceID g_device;
SDL_AudioSpec g_device_spec;
std::unique_ptr<MakoMusic> music;
std::unique_ptr<Music> music;
std::unique_ptr<MakoYmfm> fm;
std::unique_ptr<MAKOMidi> midi;
@@ -182,7 +262,7 @@ void MAKO::play_music(int page)
return;
file = abspath;
#endif
auto m = std::make_unique<MakoMusic>(file, next_loop, g_device_spec);
auto m = std::make_unique<Music>(file, next_loop, g_device_spec);
if (!m->is_open())
return;
SDL_LockAudioDevice(g_device);
@@ -208,7 +288,7 @@ void MAKO::stop_music()
{
if (music || fm) {
SDL_LockAudioDevice(g_device);
std::unique_ptr<MakoMusic> old_music = std::move(music);
std::unique_ptr<Music> old_music = std::move(music);
std::unique_ptr<MakoYmfm> old_fm = std::move(fm);
SDL_UnlockAudioDevice(g_device);
// old_music/old_fm are destroyed here, outside the lock.
+462
View File
@@ -0,0 +1,462 @@
/*
ALICE SOFT SYSTEM 3 for Win32
[ MAKO ]
*/
#include <memory>
#include <string>
#include <vector>
#include <limits.h>
#include "sdl_compat.h"
#include "mako.h"
#include "mako_midi.h"
#include "music_decoder.h"
#include "fm/mako_ymfm.h"
#include "config.h"
#include "dri.h"
#include "game_id.h"
namespace {
// Plays a single BGM file (MP3, OGG, or WAV, chosen by file extension),
// decoding on demand into an audio stream bound to the playback device.
class Music {
public:
// loops: number of times to play; 0 means loop forever.
Music(SDL_AudioDeviceID device, const std::string& path, int loops)
: decoder(create_music_decoder(path)), loops_(loops)
{
if (!decoder)
return;
const SDL_AudioSpec& src_spec = decoder->spec();
SDL_AudioSpec device_spec;
SDL_GetAudioDeviceFormat(device, &device_spec, nullptr);
stream = SDL_CreateAudioStream(&src_spec, &device_spec);
if (!stream) {
WARNING("SDL_CreateAudioStream failed: %s", SDL_GetError());
return;
}
playing = true;
SDL_SetAudioStreamGetCallback(stream, [](void* self, SDL_AudioStream*, int additional_amount, int) {
static_cast<Music*>(self)->audio_callback(additional_amount);
}, this);
SDL_BindAudioStream(device, stream);
}
~Music()
{
if (stream)
SDL_DestroyAudioStream(stream);
}
bool is_open() const { return stream != nullptr; }
bool is_playing() const
{
SDL_LockAudioStream(stream);
bool result = playing;
SDL_UnlockAudioStream(stream);
return result;
}
private:
void audio_callback(int additional_amount)
{
if (!playing)
return;
while (SDL_GetAudioStreamAvailable(stream) < additional_amount && !input_finished)
decode();
if (input_finished && SDL_GetAudioStreamAvailable(stream) <= 0)
playing = false;
}
// Decodes and queues one chunk, handling EOF and decoder errors.
void decode()
{
constexpr int CHUNK_FRAMES = 1024;
DecodedChunk chunk = decoder->decode(CHUNK_FRAMES);
if (chunk.frames == 0) {
if (loops_ && --loops_ == 0) {
SDL_FlushAudioStream(stream);
input_finished = true;
} else {
decoder->seek_start();
}
return;
}
const SDL_AudioSpec& spec = decoder->spec();
int bytes = chunk.frames * SDL_AUDIO_BITSIZE(spec.format) / 8 * spec.channels;
if (!SDL_PutAudioStreamData(stream, chunk.data, bytes)) {
WARNING("SDL_PutAudioStreamData failed: %s", SDL_GetError());
input_finished = true;
}
}
std::unique_ptr<MusicDecoder> decoder;
SDL_AudioStream* stream = nullptr;
int loops_; // number of times to play, 0 for infinite loop
bool playing = false;
bool input_finished = false;
};
#ifdef _WIN32
// Per-game mapping from music numbers to CD tracks. This is necessary to
// forcibly change the sound device with a menu command.
//
// When the game uses "Z 100+x,y" command, the xth element of the array should
// be y. The array must be terminated with -1.
const int8_t RANCE41_tracks[] = {2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,1,-1};
const int8_t RANCE42_tracks[] = {2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,1,-1};
const int8_t DPSALL_tracks[] = {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4,5,1,2,3,-1};
#endif
class FmStream {
public:
FmStream(SDL_AudioDeviceID device, std::vector<uint8_t> data)
: FmStream(device, std::move(data), device_format(device)) {}
~FmStream() { SDL_DestroyAudioStream(stream); } // unbinds and stops fill()
// FM music keeps playing until every channel has looped.
bool is_playing() { int mark, loop; get_mark(&mark, &loop); return !loop; }
void get_mark(int* mark, int* loop) {
SDL_LockAudioStream(stream);
ymfm.get_mark(mark, loop);
SDL_UnlockAudioStream(stream);
}
private:
FmStream(SDL_AudioDeviceID device, std::vector<uint8_t> data, const SDL_AudioSpec& device_spec)
: ymfm(device_spec.freq, std::move(data))
{
SDL_AudioSpec src_spec = { SDL_AUDIO_S16, 2, device_spec.freq };
stream = SDL_CreateAudioStream(&src_spec, &device_spec);
SDL_SetAudioStreamGetCallback(stream, [](void* self, SDL_AudioStream*, int additional_amount, int) {
static_cast<FmStream*>(self)->fill(additional_amount);
}, this);
SDL_BindAudioStream(device, stream);
}
static SDL_AudioSpec device_format(SDL_AudioDeviceID device) {
SDL_AudioSpec spec;
SDL_GetAudioDeviceFormat(device, &spec, nullptr);
return spec;
}
void fill(int additional_amount) {
const int CHUNK = 4096; // bytes; 1024 stereo S16 frames
int16_t buffer[CHUNK / 2];
while (additional_amount > 0) {
int len = additional_amount < CHUNK ? additional_amount : CHUNK;
ymfm.Process(buffer, len / 4);
SDL_PutAudioStreamData(stream, buffer, len);
additional_amount -= len;
}
}
MakoYmfm ymfm;
SDL_AudioStream* stream;
};
// The audio device. Each sound source (music, fm, pcm) creates its own
// SDL_AudioStream and binds it to this device.
SDL_AudioDeviceID g_device;
std::unique_ptr<Music> music;
std::unique_ptr<FmStream> fm;
std::unique_ptr<MAKOMidi> midi;
// PCM playback. pcm_loops is the number of remaining plays, or -1 for an
// infinite loop. pcm_stream's get-callback re-feeds pcm_src.
SDL_AudioStream* pcm_stream;
std::vector<uint8_t> pcm_src;
int pcm_loops;
bool pcm_input_finished;
void SDLCALL pcm_audio_callback(void*, SDL_AudioStream* stream, int additional_amount, int /*total_amount*/)
{
while (SDL_GetAudioStreamAvailable(stream) < additional_amount && !pcm_input_finished) {
if (pcm_loops == 0) {
SDL_FlushAudioStream(stream);
pcm_input_finished = true;
break;
}
SDL_PutAudioStreamData(stream, pcm_src.data(), static_cast<int>(pcm_src.size()));
if (pcm_loops > 0)
pcm_loops--;
}
}
} // namespace
MAKO::MAKO(const Config& config, const GameId& game_id) :
use_fm(config.use_fm),
current_music(0),
next_loop(0),
game_id(game_id)
{
if (!config.playlist.empty())
load_playlist(config.playlist.c_str());
#ifdef _WIN32
if (!is_cd_available()) {
SDL_Event event = {};
event.user.type = sdl_custom_event_type;
event.user.code = DISABLE_CD_MENU;
SDL_PushEvent(&event);
}
#endif
amus.open("AMUS.DAT");
awav.open("AWAV.DAT");
amse.open("AMSE.DAT");
mda.open("AMUS.MDA");
for (int i = 1; i <= 99; i++)
cd_track[i] = 0;
SDL_InitSubSystem(SDL_INIT_AUDIO);
g_device = SDL_OpenAudioDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr);
if (!g_device)
WARNING("Cannot open audio device: %s", SDL_GetError());
midi = std::make_unique<MAKOMidi>(config.midi_device);
if (!midi->is_available())
use_fm = true;
}
MAKO::~MAKO()
{
stop_music();
stop_pcm();
midi.reset();
if (g_device)
SDL_CloseAudioDevice(g_device);
SDL_QuitSubSystem(SDL_INIT_AUDIO);
}
bool MAKO::load_playlist(const char* path)
{
FILE* fp = fopen(path, "r");
if (!fp) {
WARNING("Cannot open %s", path);
return false;
}
char buf[256];
while (fgets(buf, sizeof(buf) - 1, fp)) {
for (char *p = buf; *p; p++) {
if (*p == '\\')
*p = '/';
else if (*p == '\r' || *p == '\n')
*p = '\0';
}
playlist.push_back(buf[0] ? strdup(buf) : NULL);
}
fclose(fp);
return true;
}
void MAKO::play_music(int page)
{
if (current_music == page)
return;
stop_music();
size_t track = page < 100 ? cd_track[page] : 0;
if (track && is_cd_available()) {
if (track >= playlist.size() || !playlist[track])
return;
const char* file = playlist[track];
#ifdef __ANDROID__
// dr_mp3/stb_vorbis open the file via fopen, which requires an absolute
// path on Android.
char abspath[PATH_MAX];
if (!realpath(file, abspath))
return;
file = abspath;
#endif
music = std::make_unique<Music>(g_device, file, next_loop);
if (!music->is_open()) {
music.reset();
return;
}
} else if (use_fm) {
std::vector<uint8_t> data = amus.load(page);
if (data.empty())
return;
fm = std::make_unique<FmStream>(g_device, std::move(data));
} else if (midi->is_available()) {
if (!midi->play(game_id, amus, mda, page, next_loop))
return;
}
current_music = page;
next_loop = 0;
}
void MAKO::stop_music()
{
music.reset();
fm.reset();
if (midi->is_available())
midi->stop();
current_music = 0;
}
bool MAKO::check_music()
{
if (fm)
return fm->is_playing();
if (music)
return music->is_playing();
return midi->is_playing();
}
#ifdef _WIN32
void MAKO::select_sound(BGMDevice dev)
{
int page = current_music;
int old_dev = (1 <= page && page <= 99 && cd_track[page]) ? BGM_CD :
use_fm ? BGM_FM : BGM_MIDI;
switch (dev) {
case BGM_FM:
case BGM_MIDI:
for (int i = 1; i <= 99; i++)
cd_track[i] = 0;
if (midi->is_available())
use_fm = dev == BGM_FM;
else
dev = BGM_FM;
break;
case BGM_CD:
const int8_t* tracks;
switch (game_id.game) {
case GameId::RANCE41:
tracks = RANCE41_tracks;
break;
case GameId::RANCE42:
tracks = RANCE42_tracks;
break;
case GameId::DPS_ALL:
tracks = DPSALL_tracks;
break;
// For the following games, the default mapping (cd_track[i] = i) works.
case GameId::AYUMI_CD:
case GameId::FUNNYBEE_CD:
case GameId::ONLYYOU:
default:
tracks = nullptr;
}
if (tracks) {
for (int i = 0; tracks[i] >= 0; i++)
cd_track[i + 1] = tracks[i];
} else {
for (int i = 1; i <= 99; i++)
cd_track[i] = i;
}
break;
}
if (dev != old_dev && page) {
stop_music();
play_music(page);
}
}
#endif
void MAKO::get_mark(int* mark, int* loop)
{
if (fm) {
fm->get_mark(mark, loop);
return;
}
midi->get_mark(mark, loop);
}
void MAKO::play_pcm(int page, int loops)
{
stop_pcm();
SDL_AudioSpec device_spec;
SDL_GetAudioDeviceFormat(g_device, &device_spec, nullptr);
SDL_AudioStream* stream = nullptr;
std::vector<uint8_t> src;
// WAV形式 (Only You)
std::vector<uint8_t> data = awav.load(page);
if (!data.empty()) {
SDL_AudioSpec spec;
Uint8* wav;
Uint32 wavlen;
if (!SDL_LoadWAV_IO(SDL_IOFromConstMem(data.data(), static_cast<int>(data.size())), 1, &spec, &wav, &wavlen)) {
WARNING("SDL_LoadWAV_IO failed: %s", SDL_GetError());
return;
}
src.assign(wav, wav + wavlen);
SDL_free(wav);
stream = SDL_CreateAudioStream(&spec, &device_spec);
} else {
// AMSE形式 (乙女戦記)
data = amse.load(page);
if (data.empty())
return;
uint32_t amse_size = SDL_Swap32LE(*reinterpret_cast<uint32_t*>(&data[8]));
// 4-bit PCM -> 8-bit PCM, mono, 8000Hz
for (uint32_t i = 12; i < amse_size; i++) {
src.push_back(data[i] & 0xf0);
src.push_back((data[i] & 0x0f) << 4);
}
SDL_AudioSpec src_spec = { SDL_AUDIO_U8, 1, 8000 };
stream = SDL_CreateAudioStream(&src_spec, &device_spec);
}
if (!stream) {
WARNING("SDL_CreateAudioStream failed: %s", SDL_GetError());
return;
}
pcm_src = std::move(src);
pcm_loops = loops ? loops : -1;
pcm_input_finished = false;
pcm_stream = stream;
SDL_SetAudioStreamGetCallback(pcm_stream, pcm_audio_callback, nullptr);
SDL_BindAudioStream(g_device, pcm_stream);
}
void MAKO::stop_pcm()
{
// Destroy the stream first (unbinds and stops its get-callback), then it is
// safe to drop the source buffer the callback was reading.
if (pcm_stream) {
SDL_DestroyAudioStream(pcm_stream);
pcm_stream = nullptr;
}
pcm_src.clear();
pcm_input_finished = false;
}
bool MAKO::check_pcm()
{
if (!pcm_stream)
return false;
SDL_LockAudioStream(pcm_stream);
bool playing = pcm_stream &&
(!pcm_input_finished || SDL_GetAudioStreamAvailable(pcm_stream) > 0);
SDL_UnlockAudioStream(pcm_stream);
return playing;
}
bool MAKO::is_cd_available() const
{
return !playlist.empty();
}
+13 -11
View File
@@ -1,12 +1,14 @@
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#ifdef _WIN32
#include <windows.h>
#undef ERROR
#endif
#include <SDL.h>
#include "common.h"
#include "sdl_compat.h"
SDL_RWops* open_resource(const char* name, const char* type) {
sdl::IOStream* open_resource(const char* name, const char* type) {
#ifdef _WIN32
// On Windows, read from resource.
HINSTANCE hInst = GetModuleHandle(NULL);
@@ -16,25 +18,25 @@ SDL_RWops* open_resource(const char* name, const char* type) {
WARNING("Cannot load resource %s (type: %s)", name, type);
return NULL;
}
return SDL_RWFromConstMem(LockResource(hGlobal), SizeofResource(hInst, hRes));
return sdl::IOFromConstMem(LockResource(hGlobal), SizeofResource(hInst, hRes));
#else
// On Android, read from APK assets.
// On other platforms, read from a file under RESOURCE_PATH.
char path[PATH_MAX];
snprintf(path, PATH_MAX, "%s%s/%s", RESOURCE_PATH, type, name);
return SDL_RWFromFile(path, "rb");
return sdl::IOFromFile(path, "rb");
#endif
}
SDL_RWops* open_file(const char* name) {
sdl::IOStream* open_file(const char* name) {
#ifdef __ANDROID__
// We cannot use SDL_RWFromFile() because it does not resolve relative
// paths using the current directory on Android.
FILE *fp = fopen(name, "rb");
if (!fp)
// SDL_IOFromFile() treats relative paths as APK assets on Android, so use
// an absolute path to open files relative to the current game directory.
char path[PATH_MAX];
if (!realpath(name, path))
return NULL;
return SDL_RWFromFP(fp, SDL_TRUE);
return sdl::IOFromFile(path, "rb");
#else
return SDL_RWFromFile(name, "rb");
return sdl::IOFromFile(name, "rb");
#endif
}
+518
View File
@@ -0,0 +1,518 @@
#ifndef SYSTEM3_SDL_COMPAT_H_
#define SYSTEM3_SDL_COMPAT_H_
#include <stddef.h>
#include <stdint.h>
// SDL's public headers have different include paths in SDL2 and SDL3. Keep
// that choice in one place so the rest of the engine does not depend on the
// installed major version's directory layout.
#ifndef SYSTEM3_SDL_VERSION
#define SYSTEM3_SDL_VERSION 2
#endif
#if SYSTEM3_SDL_VERSION == 2
#include <SDL.h>
#elif SYSTEM3_SDL_VERSION == 3
#include <SDL3/SDL.h>
#else
#error "Unsupported SYSTEM3_SDL_VERSION"
#endif
static_assert(SDL_MAJOR_VERSION == SYSTEM3_SDL_VERSION,
"The selected SDL headers do not match SYSTEM3_SDL_VERSION");
// windows.h defines CreateWindow as a macro.
#ifdef CreateWindow
#undef CreateWindow
#endif
namespace sdl {
#if SYSTEM3_SDL_VERSION == 2
using Gamepad = SDL_GameController;
using PixelFormat = SDL_PixelFormatEnum;
using Mutex = SDL_mutex;
using AtomicInt = SDL_atomic_t;
using IOStream = SDL_RWops;
using GamepadButton = SDL_GameControllerButton;
using GamepadAxis = SDL_GameControllerAxis;
inline constexpr Uint32 INIT_GAMEPAD = SDL_INIT_GAMECONTROLLER;
inline constexpr Uint32 EVENT_QUIT = SDL_QUIT;
inline constexpr Uint32 EVENT_KEY_UP = SDL_KEYUP;
inline constexpr Uint32 EVENT_MOUSE_MOTION = SDL_MOUSEMOTION;
inline constexpr Uint32 EVENT_MOUSE_WHEEL = SDL_MOUSEWHEEL;
inline constexpr Uint32 EVENT_FINGER_DOWN = SDL_FINGERDOWN;
inline constexpr Uint32 EVENT_FINGER_UP = SDL_FINGERUP;
inline constexpr Uint32 EVENT_FINGER_MOTION = SDL_FINGERMOTION;
inline constexpr SDL_AudioFormat AudioS16 = AUDIO_S16SYS;
inline constexpr GamepadButton GAMEPAD_BUTTON_DPAD_UP = SDL_CONTROLLER_BUTTON_DPAD_UP;
inline constexpr GamepadButton GAMEPAD_BUTTON_DPAD_DOWN = SDL_CONTROLLER_BUTTON_DPAD_DOWN;
inline constexpr GamepadButton GAMEPAD_BUTTON_DPAD_LEFT = SDL_CONTROLLER_BUTTON_DPAD_LEFT;
inline constexpr GamepadButton GAMEPAD_BUTTON_DPAD_RIGHT = SDL_CONTROLLER_BUTTON_DPAD_RIGHT;
inline constexpr GamepadButton GAMEPAD_BUTTON_SOUTH = SDL_CONTROLLER_BUTTON_A;
inline constexpr GamepadButton GAMEPAD_BUTTON_EAST = SDL_CONTROLLER_BUTTON_B;
inline constexpr GamepadButton GAMEPAD_BUTTON_WEST = SDL_CONTROLLER_BUTTON_X;
inline constexpr GamepadButton GAMEPAD_BUTTON_NORTH = SDL_CONTROLLER_BUTTON_Y;
inline constexpr GamepadAxis GAMEPAD_AXIS_LEFTX = SDL_CONTROLLER_AXIS_LEFTX;
inline constexpr GamepadAxis GAMEPAD_AXIS_LEFTY = SDL_CONTROLLER_AXIS_LEFTY;
#else
using Gamepad = SDL_Gamepad;
using PixelFormat = SDL_PixelFormat;
using Mutex = SDL_Mutex;
using AtomicInt = SDL_AtomicInt;
using IOStream = SDL_IOStream;
using GamepadButton = SDL_GamepadButton;
using GamepadAxis = SDL_GamepadAxis;
inline constexpr Uint32 INIT_GAMEPAD = SDL_INIT_GAMEPAD;
inline constexpr Uint32 EVENT_QUIT = SDL_EVENT_QUIT;
inline constexpr Uint32 EVENT_KEY_UP = SDL_EVENT_KEY_UP;
inline constexpr Uint32 EVENT_MOUSE_MOTION = SDL_EVENT_MOUSE_MOTION;
inline constexpr Uint32 EVENT_MOUSE_WHEEL = SDL_EVENT_MOUSE_WHEEL;
inline constexpr Uint32 EVENT_FINGER_DOWN = SDL_EVENT_FINGER_DOWN;
inline constexpr Uint32 EVENT_FINGER_UP = SDL_EVENT_FINGER_UP;
inline constexpr Uint32 EVENT_FINGER_MOTION = SDL_EVENT_FINGER_MOTION;
inline constexpr SDL_AudioFormat AudioS16 = SDL_AUDIO_S16;
inline constexpr GamepadButton GAMEPAD_BUTTON_DPAD_UP = SDL_GAMEPAD_BUTTON_DPAD_UP;
inline constexpr GamepadButton GAMEPAD_BUTTON_DPAD_DOWN = SDL_GAMEPAD_BUTTON_DPAD_DOWN;
inline constexpr GamepadButton GAMEPAD_BUTTON_DPAD_LEFT = SDL_GAMEPAD_BUTTON_DPAD_LEFT;
inline constexpr GamepadButton GAMEPAD_BUTTON_DPAD_RIGHT = SDL_GAMEPAD_BUTTON_DPAD_RIGHT;
inline constexpr GamepadButton GAMEPAD_BUTTON_SOUTH = SDL_GAMEPAD_BUTTON_SOUTH;
inline constexpr GamepadButton GAMEPAD_BUTTON_EAST = SDL_GAMEPAD_BUTTON_EAST;
inline constexpr GamepadButton GAMEPAD_BUTTON_WEST = SDL_GAMEPAD_BUTTON_WEST;
inline constexpr GamepadButton GAMEPAD_BUTTON_NORTH = SDL_GAMEPAD_BUTTON_NORTH;
inline constexpr GamepadAxis GAMEPAD_AXIS_LEFTX = SDL_GAMEPAD_AXIS_LEFTX;
inline constexpr GamepadAxis GAMEPAD_AXIS_LEFTY = SDL_GAMEPAD_AXIS_LEFTY;
#endif
inline IOStream* IOFromFile(const char* path, const char* mode)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_RWFromFile(path, mode);
#else
return SDL_IOFromFile(path, mode);
#endif
}
inline IOStream* IOFromConstMem(const void* data, size_t size)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_RWFromConstMem(data, static_cast<int>(size));
#else
return SDL_IOFromConstMem(data, size);
#endif
}
inline size_t ReadIO(IOStream* stream, void* destination, size_t bytes)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_RWread(stream, destination, 1, bytes);
#else
return SDL_ReadIO(stream, destination, bytes);
#endif
}
inline int64_t SeekIO(IOStream* stream, int64_t offset, int whence)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_RWseek(stream, offset, whence);
#else
return SDL_SeekIO(stream, offset, static_cast<SDL_IOWhence>(whence));
#endif
}
inline bool CloseIO(IOStream* stream)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_RWclose(stream) == 0;
#else
return SDL_CloseIO(stream);
#endif
}
inline int SetAtomicInt(AtomicInt* value, int desired)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_AtomicSet(value, desired);
#else
return SDL_SetAtomicInt(value, desired);
#endif
}
inline int GetAtomicInt(AtomicInt* value)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_AtomicGet(value);
#else
return SDL_GetAtomicInt(value);
#endif
}
inline bool CompareAndSwapAtomicInt(AtomicInt* value, int expected, int desired)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_AtomicCAS(value, expected, desired) == SDL_TRUE;
#else
return SDL_CompareAndSwapAtomicInt(value, expected, desired);
#endif
}
inline bool SetCurrentThreadPriority(SDL_ThreadPriority priority)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_SetThreadPriority(priority) == 0;
#else
return SDL_SetCurrentThreadPriority(priority);
#endif
}
inline int GetNumTouchFingers(SDL_TouchID touch_id)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_GetNumTouchFingers(touch_id);
#else
int count = 0;
SDL_Finger** fingers = SDL_GetTouchFingers(touch_id, &count);
SDL_free(fingers);
return count;
#endif
}
inline SDL_TouchID GetTouchID(const SDL_TouchFingerEvent& event)
{
#if SYSTEM3_SDL_VERSION == 2
return event.touchId;
#else
return event.touchID;
#endif
}
inline SDL_Scancode GetKeyScancode(const SDL_KeyboardEvent& event)
{
#if SYSTEM3_SDL_VERSION == 2
return event.keysym.scancode;
#else
return event.scancode;
#endif
}
inline bool RenderCoordinatesFromWindow(SDL_Renderer* renderer, float window_x,
float window_y, float* x, float* y)
{
#if SYSTEM3_SDL_VERSION == 2
*x = window_x;
*y = window_y;
return true;
#else
return SDL_RenderCoordinatesFromWindow(renderer, window_x, window_y, x, y);
#endif
}
inline bool GetGamepadButton(Gamepad* gamepad, GamepadButton button)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_GameControllerGetButton(gamepad, button) != 0;
#else
return SDL_GetGamepadButton(gamepad, button);
#endif
}
inline Sint16 GetGamepadAxis(Gamepad* gamepad, GamepadAxis axis)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_GameControllerGetAxis(gamepad, axis);
#else
return SDL_GetGamepadAxis(gamepad, axis);
#endif
}
inline bool ShowMessageBox(const SDL_MessageBoxData* data, int* button_id)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_ShowMessageBox(data, button_id) == 0;
#else
return SDL_ShowMessageBox(data, button_id);
#endif
}
inline void HideCursor()
{
#if SYSTEM3_SDL_VERSION == 2
SDL_ShowCursor(SDL_DISABLE);
#else
SDL_HideCursor();
#endif
}
inline void ShowCursor()
{
#if SYSTEM3_SDL_VERSION == 2
SDL_ShowCursor(SDL_ENABLE);
#else
SDL_ShowCursor();
#endif
}
inline bool SaveBMP(SDL_Surface* surface, const char* path)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_SaveBMP(surface, path) == 0;
#else
return SDL_SaveBMP(surface, path);
#endif
}
inline void FreeWAV(Uint8* data)
{
#if SYSTEM3_SDL_VERSION == 2
SDL_FreeWAV(data);
#else
SDL_free(data);
#endif
}
inline Uint32 Swap32LE(Uint32 value)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_SwapLE32(value);
#else
return SDL_Swap32LE(value);
#endif
}
inline SDL_Surface* CreateSurface(int width, int height, PixelFormat format)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_CreateRGBSurfaceWithFormat(0, width, height,
SDL_BITSPERPIXEL(format), format);
#else
SDL_Surface* surface = SDL_CreateSurface(width, height, format);
if (surface && format == SDL_PIXELFORMAT_INDEX8)
SDL_CreateSurfacePalette(surface);
return surface;
#endif
}
inline SDL_Surface* CreateSurfaceWithMasks(int width, int height, int depth,
Uint32 red, Uint32 green, Uint32 blue, Uint32 alpha)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_CreateRGBSurface(0, width, height, depth, red, green, blue, alpha);
#else
return SDL_CreateSurface(width, height,
SDL_GetPixelFormatForMasks(depth, red, green, blue, alpha));
#endif
}
inline void DestroySurface(SDL_Surface* surface)
{
#if SYSTEM3_SDL_VERSION == 2
SDL_FreeSurface(surface);
#else
SDL_DestroySurface(surface);
#endif
}
inline SDL_Palette* GetSurfacePalette(SDL_Surface* surface)
{
#if SYSTEM3_SDL_VERSION == 2
return surface->format->palette;
#else
return SDL_GetSurfacePalette(surface);
#endif
}
inline SDL_Window* CreateWindow(const char* title, int width, int height,
SDL_WindowFlags flags)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_CreateWindow(title, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
width, height, flags);
#else
// SDL3 uses physical pixels for window coordinates on platforms such as
// Windows. Scale the requested content size so that it has the same
// apparent size on a high-DPI display. On platforms whose window
// coordinates are already logical (macOS and Wayland), the content scale is
// 1 and HIGH_PIXEL_DENSITY provides the correspondingly larger backbuffer.
float scale = SDL_GetDisplayContentScale(SDL_GetPrimaryDisplay());
if (scale <= 0.0f)
scale = 1.0f;
width = static_cast<int>(width * scale + 0.5f);
height = static_cast<int>(height * scale + 0.5f);
return SDL_CreateWindow(title, width, height,
flags | SDL_WINDOW_HIGH_PIXEL_DENSITY);
#endif
}
inline void SetWindowContentSize(SDL_Window* window, int width, int height)
{
#if SYSTEM3_SDL_VERSION == 2
SDL_SetWindowSize(window, width, height);
#else
// Convert content units to window coordinates. Dividing the window's
// display scale by its pixel density gives the platform content scale.
float pixel_density = SDL_GetWindowPixelDensity(window);
float scale = SDL_GetWindowDisplayScale(window);
if (pixel_density > 0.0f && scale > 0.0f)
scale /= pixel_density;
else
scale = 1.0f;
SDL_SetWindowSize(window,
static_cast<int>(width * scale + 0.5f),
static_cast<int>(height * scale + 0.5f));
#endif
}
inline SDL_Renderer* CreateRenderer(SDL_Window* window)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_CreateRenderer(window, -1, 0);
#else
return SDL_CreateRenderer(window, nullptr);
#endif
}
inline bool SetRenderLogicalPresentation(SDL_Renderer* renderer, int width, int height)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_RenderSetLogicalSize(renderer, width, height) == 0;
#else
return SDL_SetRenderLogicalPresentation(renderer, width, height,
SDL_LOGICAL_PRESENTATION_LETTERBOX);
#endif
}
inline SDL_Palette* CreatePalette(int colors)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_AllocPalette(colors);
#else
return SDL_CreatePalette(colors);
#endif
}
inline void DestroyPalette(SDL_Palette* palette)
{
#if SYSTEM3_SDL_VERSION == 2
SDL_FreePalette(palette);
#else
SDL_DestroyPalette(palette);
#endif
}
inline void DestroyCursor(SDL_Cursor* cursor)
{
#if SYSTEM3_SDL_VERSION == 2
SDL_FreeCursor(cursor);
#else
SDL_DestroyCursor(cursor);
#endif
}
inline bool GetRectIntersection(const SDL_Rect* a, const SDL_Rect* b, SDL_Rect* result)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_IntersectRect(a, b, result) == SDL_TRUE;
#else
return SDL_GetRectIntersection(a, b, result);
#endif
}
inline void GetRectUnion(const SDL_Rect* a, const SDL_Rect* b, SDL_Rect* result)
{
#if SYSTEM3_SDL_VERSION == 2
SDL_UnionRect(a, b, result);
#else
SDL_GetRectUnion(a, b, result);
#endif
}
inline bool RenderTexture(SDL_Renderer* renderer, SDL_Texture* texture,
const SDL_Rect* source, const SDL_Rect* destination)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_RenderCopy(renderer, texture, source, destination) == 0;
#else
SDL_FRect src;
SDL_FRect dst;
const SDL_FRect* src_ptr = nullptr;
const SDL_FRect* dst_ptr = nullptr;
if (source) {
src = { static_cast<float>(source->x), static_cast<float>(source->y),
static_cast<float>(source->w), static_cast<float>(source->h) };
src_ptr = &src;
}
if (destination) {
dst = { static_cast<float>(destination->x), static_cast<float>(destination->y),
static_cast<float>(destination->w), static_cast<float>(destination->h) };
dst_ptr = &dst;
}
return SDL_RenderTexture(renderer, texture, src_ptr, dst_ptr);
#endif
}
inline bool SetSurfaceColorKey(SDL_Surface* surface, bool enabled, Uint32 key)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_SetColorKey(surface, enabled ? SDL_TRUE : SDL_FALSE, key) == 0;
#else
return SDL_SetSurfaceColorKey(surface, enabled, key);
#endif
}
inline bool SurfaceHasColorKey(SDL_Surface* surface)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_HasColorKey(surface) == SDL_TRUE;
#else
return SDL_SurfaceHasColorKey(surface);
#endif
}
inline bool GetSurfaceColorKey(SDL_Surface* surface, Uint32* key)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_GetColorKey(surface, key) == 0;
#else
return SDL_GetSurfaceColorKey(surface, key);
#endif
}
inline bool StretchSurface(SDL_Surface* source, const SDL_Rect* source_rect,
SDL_Surface* destination, SDL_Rect* destination_rect)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_SoftStretch(source, source_rect, destination, destination_rect) == 0;
#else
return SDL_StretchSurface(source, source_rect, destination, destination_rect,
SDL_SCALEMODE_NEAREST);
#endif
}
inline bool FillSurfaceRect(SDL_Surface* surface, const SDL_Rect* rect, Uint32 color)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_FillRect(surface, rect, color) == 0;
#else
return SDL_FillSurfaceRect(surface, rect, color);
#endif
}
inline bool FillSurfaceRects(SDL_Surface* surface, const SDL_Rect* rects,
int count, Uint32 color)
{
#if SYSTEM3_SDL_VERSION == 2
return SDL_FillRects(surface, rects, count, color) == 0;
#else
return SDL_FillSurfaceRects(surface, rects, count, color);
#endif
}
} // namespace sdl
sdl::IOStream* open_resource(const char* name, const char* type);
sdl::IOStream* open_file(const char* name);
#endif // SYSTEM3_SDL_COMPAT_H_
+12 -3
View File
@@ -1,3 +1,6 @@
#if SYSTEM3_SDL_VERSION == 3
#include <SDL3/SDL_main.h>
#endif
#include <string>
#include "common.h"
#include "config.h"
@@ -35,7 +38,9 @@ SDL_Window* create_window(const Config& config, const GameId& game_id)
SDL_Init(SDL_INIT_VIDEO);
SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0");
#if SYSTEM3_SDL_VERSION == 2
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
#endif
#ifdef __EMSCRIPTEN__
// Stop SDL from calling emscripten_sleep() in functions that are called
// indirectly, which does not work with ASYNCIFY_IGNORE_INDIRECT=1. For
@@ -48,7 +53,11 @@ SDL_Window* create_window(const Config& config, const GameId& game_id)
SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight");
Uint32 flags = SDL_WINDOW_FULLSCREEN;
#else
Uint32 flags = config.fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : SDL_WINDOW_RESIZABLE;
SDL_WindowFlags flags = config.fullscreen ? SDL_WINDOW_FULLSCREEN : SDL_WINDOW_RESIZABLE;
#if SYSTEM3_SDL_VERSION == 2
if (config.fullscreen)
flags = SDL_WINDOW_FULLSCREEN_DESKTOP;
#endif
#endif
#ifdef __EMSCRIPTEN__
@@ -57,7 +66,7 @@ SDL_Window* create_window(const Config& config, const GameId& game_id)
#else
const char *window_title = title.c_str();
#endif
return SDL_CreateWindow(window_title, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, 640, 400, flags);
return sdl::CreateWindow(window_title, 640, 400, flags);
}
} // namespace
@@ -77,7 +86,7 @@ int main(int argc, char *argv[])
GameId game_id(config);
g_window = create_window(config, game_id);
g_renderer = SDL_CreateRenderer(g_window, -1, 0);
g_renderer = sdl::CreateRenderer(g_window);
sdl_custom_event_type = SDL_RegisterEvents(1);
// system3 初期化
+29 -29
View File
@@ -20,7 +20,7 @@ const uint32 SCANLINE_ALPHA = 0x38; // 0-255
SDL_Texture* create_scanline_texture(SDL_Renderer* renderer, int width, int height)
{
SDL_Surface* sf = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_ARGB8888);
SDL_Surface* sf = sdl::CreateSurface(width, height, SDL_PIXELFORMAT_ARGB8888);
for (int y = 0; y < height; y++) {
uint32* p = reinterpret_cast<uint32*>(surface_line(sf, y));
uint32 v = y % 2 ? (SCANLINE_ALPHA << 24) : 0;
@@ -30,7 +30,7 @@ SDL_Texture* create_scanline_texture(SDL_Renderer* renderer, int width, int heig
}
SDL_Texture* tx = SDL_CreateTextureFromSurface(renderer, sf);
SDL_SetTextureBlendMode(tx, SDL_BLENDMODE_BLEND);
SDL_FreeSurface(sf);
sdl::DestroySurface(sf);
return tx;
}
@@ -48,19 +48,19 @@ AGS::AGS(const Config& config, const GameId& game_id) : game_id(game_id)
window_height = screen_height = 400;
}
SDL_SetWindowSize(g_window, window_width, window_height);
SDL_RenderSetLogicalSize(g_renderer, window_width, window_height);
sdl::SetWindowContentSize(g_window, window_width, window_height);
sdl::SetRenderLogicalPresentation(g_renderer, window_width, window_height);
sdlTexture = SDL_CreateTexture(g_renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING, screen_width, screen_height);
scanline_texture = NULL;
// DIBSection 8bpp
for(int i = 0; i < NR_SCREENS; i++) {
hBmpScreen[i] = SDL_CreateRGBSurfaceWithFormat(0, 640, 480, 8, SDL_PIXELFORMAT_INDEX8);
hBmpScreen[i] = sdl::CreateSurface(640, 480, SDL_PIXELFORMAT_INDEX8);
vram[i] = reinterpret_cast<uint8_t(*)[640]>(hBmpScreen[i]->pixels);
}
// All surfaces share the same palette.
screen_palette = hBmpScreen[SCREEN_FRONT]->format->palette;
screen_palette = sdl::GetSurfacePalette(hBmpScreen[SCREEN_FRONT]);
SDL_SetSurfacePalette(hBmpScreen[SCREEN_BACK], screen_palette);
if (!config.censor_list.empty())
@@ -77,17 +77,17 @@ AGS::AGS(const Config& config, const GameId& game_id) : game_id(game_id)
if (!rw_font)
sys_error("Cannot open default font");
}
hFont16 = TTF_OpenFontRW(rw_font, 0, 16);
SDL_RWseek(rw_font, 0, SEEK_SET);
hFont24 = TTF_OpenFontRW(rw_font, 0, 24);
SDL_RWseek(rw_font, 0, SEEK_SET);
hFont32 = TTF_OpenFontRW(rw_font, 0, 32);
SDL_RWseek(rw_font, 0, SEEK_SET);
hFont48 = TTF_OpenFontRW(rw_font, 0, 48);
SDL_RWseek(rw_font, 0, SEEK_SET);
hFont64 = TTF_OpenFontRW(rw_font, 0, 64);
hFont16 = ttf::OpenFontIO(rw_font, 16);
sdl::SeekIO(rw_font, 0, SEEK_SET);
hFont24 = ttf::OpenFontIO(rw_font, 24);
sdl::SeekIO(rw_font, 0, SEEK_SET);
hFont32 = ttf::OpenFontIO(rw_font, 32);
sdl::SeekIO(rw_font, 0, SEEK_SET);
hFont48 = ttf::OpenFontIO(rw_font, 48);
sdl::SeekIO(rw_font, 0, SEEK_SET);
hFont64 = ttf::OpenFontIO(rw_font, 64);
if (!hFont16 || !hFont24 || !hFont32 || !hFont48 || !hFont64) {
sys_error("TTF_OpenFontRW failed: %s", TTF_GetError());
sys_error("Cannot open font: %s", SDL_GetError());
}
if (config.no_antialias)
ags_setAntialiasedStringMode(0);
@@ -120,7 +120,7 @@ AGS::AGS(const Config& config, const GameId& game_id) : game_id(game_id)
acg.open("ACG.DAT");
// パレット
program_palette = SDL_AllocPalette(256);
program_palette = sdl::CreatePalette(256);
program_palette->colors[0x00] = {0x00, 0x00, 0x00, 0xff};
program_palette->colors[0x01] = {0x00, 0x00, 0xaa, 0xff};
program_palette->colors[0x02] = {0xaa, 0x00, 0x00, 0xff};
@@ -199,7 +199,7 @@ AGS::~AGS()
// カーソル開放
for(int i = 0; i < 10; i++) {
if(hCursor[i]) {
SDL_FreeCursor(hCursor[i]);
sdl::DestroyCursor(hCursor[i]);
}
}
@@ -210,13 +210,13 @@ AGS::~AGS()
TTF_CloseFont(hFont32);
TTF_CloseFont(hFont48);
TTF_CloseFont(hFont64);
SDL_RWclose(rw_font);
sdl::CloseIO(rw_font);
}
SDL_FreePalette(program_palette);
sdl::DestroyPalette(program_palette);
for(int i = 0; i < NR_SCREENS; i++) {
SDL_FreeSurface(hBmpScreen[i]);
sdl::DestroySurface(hBmpScreen[i]);
}
SDL_DestroyTexture(sdlTexture);
@@ -264,8 +264,8 @@ void AGS::invalidate_screen(int sx, int sy, int width, int height)
{
SDL_Rect rect = {sx, sy, width, height};
SDL_Rect screen_rect = {0, 0, screen_width, screen_height};
SDL_IntersectRect(&rect, &screen_rect, &rect);
SDL_UnionRect(&dirty_rect, &rect, &dirty_rect);
sdl::GetRectIntersection(&rect, &screen_rect, &rect);
sdl::GetRectUnion(&dirty_rect, &rect, &dirty_rect);
}
void AGS::update_screen()
@@ -288,7 +288,7 @@ void AGS::update_screen()
dest.y = -scroll;
src.h = dest.h = screen_height + scroll;
}
SDL_RenderCopy(g_renderer, sdlTexture, &src, &dest);
sdl::RenderTexture(g_renderer, sdlTexture, &src, &dest);
if (fade_level) {
SDL_SetRenderDrawBlendMode(g_renderer, SDL_BLENDMODE_BLEND);
@@ -298,7 +298,7 @@ void AGS::update_screen()
SDL_SetRenderDrawBlendMode(g_renderer, SDL_BLENDMODE_NONE);
}
if (scanline_texture)
SDL_RenderCopy(g_renderer, scanline_texture, NULL, NULL);
sdl::RenderTexture(g_renderer, scanline_texture, NULL, NULL);
SDL_RenderPresent(g_renderer);
}
@@ -314,25 +314,25 @@ void AGS::set_scanline_mode(bool enable)
bool AGS::save_screenshot(const char* path)
{
SDL_Surface* sf = SDL_CreateRGBSurface(0, screen_width, screen_height, 32, 0, 0, 0, 0);
SDL_Surface* sf = sdl::CreateSurfaceWithMasks(screen_width, screen_height, 32, 0, 0, 0, 0);
SDL_BlitSurface(hBmpScreen[SCREEN_FRONT], NULL, sf, NULL);
if (scanline_texture) {
SDL_Renderer* renderer = SDL_CreateSoftwareRenderer(sf);
SDL_Texture *tx = create_scanline_texture(renderer, screen_width, screen_height);
SDL_RenderCopy(renderer, tx, NULL, NULL);
sdl::RenderTexture(renderer, tx, NULL, NULL);
SDL_DestroyTexture(tx);
SDL_DestroyRenderer(renderer);
}
bool ok = SDL_SaveBMP(sf, path) == 0;
bool ok = sdl::SaveBMP(sf, path);
if (!ok) {
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, "system3",
SDL_GetError(), g_window);
SDL_ClearError();
}
SDL_FreeSurface(sf);
sdl::DestroySurface(sf);
return ok;
}
+2 -2
View File
@@ -16,7 +16,7 @@
#include "game_id.h"
#include "cg.h"
#include "dri.h"
#include <SDL_ttf.h>
#include "ttf_compat.h"
struct Config;
@@ -137,7 +137,7 @@ private:
int fade_color = 0; // 0: black, 255: white
// font
SDL_RWops* rw_font;
sdl::IOStream* rw_font;
TTF_Font* hFont16;
TTF_Font* hFont24;
TTF_Font* hFont32;
+8 -1
View File
@@ -72,7 +72,7 @@ void AGS::load_cursor(int page, uint8_t flags)
}
}
if(hCursor[i]) {
SDL_FreeCursor(hCursor[i]);
sdl::DestroyCursor(hCursor[i]);
}
// TODO: fix amask/xmask values
hCursor[i] = SDL_CreateCursor(amask, xmask, 32, 32, 2, 2);
@@ -90,6 +90,12 @@ void AGS::select_cursor()
void AGS::translate_mouse_coords(int* x, int* y)
{
#if SYSTEM3_SDL_VERSION == 3
float window_x, window_y;
SDL_RenderCoordinatesToWindow(g_renderer, *x, *y, &window_x, &window_y);
*x = window_x;
*y = window_y;
#else
// scale mouse x and y
float scalex, scaley;
SDL_RenderGetScale(g_renderer, &scalex, &scaley);
@@ -113,4 +119,5 @@ void AGS::translate_mouse_coords(int* x, int* y)
// offset x and y by window borders
*x += border_left;
*y += border_top;
#endif
}
+16 -16
View File
@@ -19,14 +19,14 @@ namespace {
const int MOSAIC_SIZE = 16;
void mosaic(SDL_Surface* sf) {
SDL_Surface *tmp = SDL_CreateRGBSurfaceWithFormat(
0, (sf->w + MOSAIC_SIZE - 1) / MOSAIC_SIZE, (sf->h + MOSAIC_SIZE - 1) / MOSAIC_SIZE, 8, SDL_PIXELFORMAT_INDEX8);
if (sf->format->palette)
SDL_SetSurfacePalette(tmp, sf->format->palette);
SDL_Surface *tmp = sdl::CreateSurface(
(sf->w + MOSAIC_SIZE - 1) / MOSAIC_SIZE, (sf->h + MOSAIC_SIZE - 1) / MOSAIC_SIZE, SDL_PIXELFORMAT_INDEX8);
if (sdl::GetSurfacePalette(sf))
SDL_SetSurfacePalette(tmp, sdl::GetSurfacePalette(sf));
// NOTE: SDL_BlitScaled() does not support 8-bit surfaces.
SDL_SoftStretch(sf, NULL, tmp, NULL);
SDL_SoftStretch(tmp, NULL, sf, NULL);
SDL_FreeSurface(tmp);
sdl::StretchSurface(sf, NULL, tmp, NULL);
sdl::StretchSurface(tmp, NULL, sf, NULL);
sdl::DestroySurface(tmp);
}
} // namespace
@@ -160,10 +160,10 @@ void AGS::copy_screen(ScreenId src, ScreenId dest, int sx, int sy, int ex, int e
SDL_Surface* src_surface = hBmpScreen[src];
if (transparent_color >= 0)
SDL_SetColorKey(src_surface, SDL_TRUE, transparent_color);
sdl::SetSurfaceColorKey(src_surface, true, transparent_color);
SDL_BlitSurface(src_surface, &srcrect, hBmpScreen[dest], &destrect);
if (transparent_color >= 0)
SDL_SetColorKey(src_surface, SDL_FALSE, 0);
sdl::SetSurfaceColorKey(src_surface, false, 0);
if (dest == SCREEN_FRONT)
invalidate_screen(dx, dy, width, height);
@@ -248,7 +248,7 @@ void AGS::draw_mesh(int sx, int sy, int width, int height)
void AGS::box_fill(ScreenId dest, int sx, int sy, int ex, int ey, uint8 color)
{
SDL_Rect rect = {sx, sy, ex - sx + 1, ey - sy + 1};
SDL_FillRect(hBmpScreen[dest], &rect, color);
sdl::FillSurfaceRect(hBmpScreen[dest], &rect, color);
if(dest == SCREEN_FRONT) {
invalidate_screen(sx, sy, ex - sx + 1, ey - sy + 1);
}
@@ -261,10 +261,10 @@ void AGS::box_line(ScreenId dest, int sx, int sy, int ex, int ey, uint8 color)
SDL_Rect left = {sx, sy, 1, ey - sy + 1};
SDL_Rect right = {ex, sy, 1, ey - sy + 1};
SDL_FillRect(hBmpScreen[dest], &top, color);
SDL_FillRect(hBmpScreen[dest], &bottom, color);
SDL_FillRect(hBmpScreen[dest], &left, color);
SDL_FillRect(hBmpScreen[dest], &right, color);
sdl::FillSurfaceRect(hBmpScreen[dest], &top, color);
sdl::FillSurfaceRect(hBmpScreen[dest], &bottom, color);
sdl::FillSurfaceRect(hBmpScreen[dest], &left, color);
sdl::FillSurfaceRect(hBmpScreen[dest], &right, color);
if(dest == SCREEN_FRONT) {
invalidate_screen(sx, sy, ex - sx + 1, ey - sy + 1);
}
@@ -273,7 +273,7 @@ void AGS::box_line(ScreenId dest, int sx, int sy, int ex, int ey, uint8 color)
void AGS::draw_window(int sx, int sy, int ex, int ey, bool frame, uint8 frame_color, uint8 back_color)
{
SDL_Rect rect = {sx, sy, ex - sx + 1, ey - sy + 1};
SDL_FillRect(hBmpScreen[SCREEN_FRONT], &rect, back_color);
sdl::FillSurfaceRect(hBmpScreen[SCREEN_FRONT], &rect, back_color);
if (frame) {
SDL_Rect rects[] = {
@@ -282,7 +282,7 @@ void AGS::draw_window(int sx, int sy, int ex, int ey, bool frame, uint8 frame_co
{sx + 1, sy + 1, 2, ey - sy - 1},
{ex - 2, sy + 1, 2, ey - sy - 1},
};
SDL_FillRects(hBmpScreen[SCREEN_FRONT], rects, 4, frame_color);
sdl::FillSurfaceRects(hBmpScreen[SCREEN_FRONT], rects, 4, frame_color);
box_line(SCREEN_FRONT, sx + 4, sy + 4, ex - 4, ey - 4, frame_color);
}
invalidate_screen(sx, sy, ex - sx + 1, ey - sy + 1);
+1 -2
View File
@@ -42,7 +42,7 @@ CG AGS::load_gl3(const std::vector<uint8_t>& data, bool set_palette, int transpa
// GL3展開
CG cg(sx * 8, sy, width * 8, height);
if (transparent >= 0) {
SDL_SetColorKey(cg.surface(), SDL_TRUE, transparent | base);
sdl::SetSurfaceColorKey(cg.surface(), true, transparent | base);
}
uint8 cgdata[4][80][3];
int p = 0x36;
@@ -128,4 +128,3 @@ CG AGS::load_gl3(const std::vector<uint8_t>& data, bool set_palette, int transpa
return cg;
}
+1 -2
View File
@@ -25,7 +25,7 @@ CG AGS::load_gm3(const std::vector<uint8_t>& data, int transparent, uint8_t flag
// GM3展開
CG cg(sx * 8, sy * 2, width * 8, height * 2);
if (transparent >= 0) {
SDL_SetColorKey(cg.surface(), SDL_TRUE, transparent | base);
sdl::SetSurfaceColorKey(cg.surface(), true, transparent | base);
}
uint8 cgdata[3][80][3];
int p = 0x36;
@@ -115,4 +115,3 @@ CG AGS::load_gm3(const std::vector<uint8_t>& data, int transparent, uint8_t flag
return cg;
}
+1 -1
View File
@@ -81,7 +81,7 @@ CG AGS::load_pms(int page, const std::vector<uint8_t>& data, bool set_palette, i
// Extract pixel data
CG cg(sx, sy, width, height);
if (transparent >= 0) {
SDL_SetColorKey(cg.surface(), SDL_TRUE, transparent);
sdl::SetSurfaceColorKey(cg.surface(), true, transparent);
}
std::vector<uint8_t> buf[3];
buf[0].resize(width);
+5 -5
View File
@@ -30,8 +30,8 @@ int AGS::draw_text(ScreenId dest, int x, int y, std::u16string_view codes, int f
case 48: font = hFont48; break;
case 64: font = hFont64; break;
}
int ascent = TTF_FontAscent(font);
int descent = TTF_FontDescent(font);
int ascent = ttf::GetFontAscent(font);
int descent = ttf::GetFontDescent(font);
// Adjust dest_y if the font height is larger than the specified size.
int dest_y = y - (ascent - descent - font_size) / 2;
@@ -49,7 +49,7 @@ int AGS::draw_text(ScreenId dest, int x, int y, std::u16string_view codes, int f
draw_char(dest, dest_x, dest_y, code, font, color);
int miny, maxy, advance;
TTF_GlyphMetrics(font, code, NULL, NULL, &miny, &maxy, &advance);
ttf::GetGlyphMetrics(font, code, NULL, NULL, &miny, &maxy, &advance);
// Some fonts report incorrect Ascent/Descent value so we need to fix them.
if (miny < descent) descent = miny;
if (maxy > ascent) ascent = maxy;
@@ -79,7 +79,7 @@ void AGS::draw_char(ScreenId dest, int dest_x, int dest_y, uint16 code, TTF_Font
}
}
SDL_FreeSurface(fs);
sdl::DestroySurface(fs);
}
int AGS::nearest_color(int r, int g, int b) {
@@ -128,7 +128,7 @@ void AGS::draw_char_antialias(ScreenId dest, int dest_x, int dest_y, uint16 code
}
}
SDL_FreeSurface(fs);
sdl::DestroySurface(fs);
}
void AGS::draw_gaiji(ScreenId dest, int dest_x, int dest_y, const uint8_t bitmap[32], int size, uint8 color)
+5 -6
View File
@@ -12,12 +12,12 @@ namespace {
void trim(CG& cg, int w, int h)
{
SDL_Surface *sf = SDL_CreateRGBSurfaceWithFormat(0, w, h, 8, SDL_PIXELFORMAT_INDEX8);
SDL_Surface *sf = sdl::CreateSurface(w, h, SDL_PIXELFORMAT_INDEX8);
SDL_SetSurfacePalette(sf, cg.palette());
if (SDL_HasColorKey(cg.surface())) {
if (sdl::SurfaceHasColorKey(cg.surface())) {
Uint32 key;
SDL_GetColorKey(cg.surface(), &key);
SDL_SetColorKey(sf, SDL_TRUE, key);
sdl::GetSurfaceColorKey(cg.surface(), &key);
sdl::SetSurfaceColorKey(sf, true, key);
}
SDL_Rect srcrect = { 0, 0, w, h };
SDL_BlitSurface(cg.surface(), &srcrect, sf, NULL);
@@ -74,7 +74,7 @@ CG AGS::load_vsp(const std::vector<uint8_t>& data, bool set_palette, int transpa
// Gakuen Senki uses exact sx values rather than 8x.
CG cg(game_id.is(GameId::GAKUEN_SENKI) ? sx : sx * 8, sy, width * 8, height);
if (transparent >= 0) {
SDL_SetColorKey(cg.surface(), SDL_TRUE, transparent | base);
sdl::SetSurfaceColorKey(cg.surface(), true, transparent | base);
}
uint8 cgdata[4][2][480], mask = 0;
int p = 0x3a;
@@ -166,4 +166,3 @@ CG AGS::load_vsp(const std::vector<uint8_t>& data, bool set_palette, int transpa
}
return cg;
}
+1 -1
View File
@@ -39,7 +39,7 @@ CG AGS::load_vsp2l(const std::vector<uint8_t>& data, int transparent, uint8_t fl
// VSP2L展開
CG cg(sx * 8, sy * 2, width * 8, height * 2);
if (transparent >= 0) {
SDL_SetColorKey(cg.surface(), SDL_TRUE, transparent | base);
sdl::SetSurfaceColorKey(cg.surface(), true, transparent | base);
}
uint8 cgdata[3][2][200], mask = 0;
int p = 0x1a;
+4 -4
View File
@@ -8,7 +8,7 @@
#define _CG_H_
#include <memory>
#include <SDL.h>
#include "sdl_compat.h"
#include "common.h"
inline uint8_t* surface_line(SDL_Surface* surface, int y)
@@ -18,7 +18,7 @@ inline uint8_t* surface_line(SDL_Surface* surface, int y)
struct SurfaceDeleter {
void operator()(SDL_Surface* s) const noexcept {
if (s) SDL_FreeSurface(s);
if (s) sdl::DestroySurface(s);
}
};
@@ -29,14 +29,14 @@ struct CG {
CG() = default;
CG(int x, int y, int width, int height)
: surface_(SDL_CreateRGBSurfaceWithFormat(0, width, height, 8, SDL_PIXELFORMAT_INDEX8)),
: surface_(sdl::CreateSurface(width, height, SDL_PIXELFORMAT_INDEX8)),
x(x), y(y) {}
explicit operator bool() const noexcept { return static_cast<bool>(surface_); }
SDL_Surface* surface() const { return surface_.get(); }
int width() const { return surface_->w; }
int height() const { return surface_->h; }
SDL_Palette* palette() const { return surface_->format->palette; }
SDL_Palette* palette() const { return sdl::GetSurfacePalette(surface_.get()); }
};
#endif // _CG_H_
+16 -16
View File
@@ -7,9 +7,9 @@
#include "dri.h"
#include <memory>
#include <string.h>
#include <SDL.h>
#include "game_id.h"
#include "fileio.h"
#include "sdl_compat.h"
void Dri::open(const char* file_name)
{
@@ -185,37 +185,37 @@ std::vector<uint8> Dri::load_mda(const GameId& game_id, int page)
return {};
}
SDL_RWops* rw = open_resource(name, "mda");
sdl::IOStream* rw = open_resource(name, "mda");
if (!rw)
return {};
uint8 buf[4];
// ページの位置を取得
SDL_RWread(rw, buf, 4, 1);
sdl::ReadIO(rw, buf, 4);
int link_sector = buf[0] | (buf[1] << 8);
int data_sector = buf[2] | (buf[3] << 8);
if(page > (data_sector - link_sector) * 128 - 1) {
// ページ番号不正
SDL_RWclose(rw);
sdl::CloseIO(rw);
return {};
}
SDL_RWseek(rw, (link_sector - 1) * 256 + (page - 1) * 2, RW_SEEK_SET);
SDL_RWread(rw, buf, 2, 1);
sdl::SeekIO(rw, (link_sector - 1) * 256 + (page - 1) * 2, SEEK_SET);
sdl::ReadIO(rw, buf, 2);
int disk_index = buf[0];
int link_index = buf[1];
if(disk_index == 0 || disk_index == 0x1a) {
// 欠番
SDL_RWclose(rw);
sdl::CloseIO(rw);
return {};
}
// AMUS.MDA以外にリンクされている場合はリソースを開き直す
if(disk_index == 2) {
SDL_RWclose(rw);
sdl::CloseIO(rw);
switch (game_id.game) {
case GameId::DPS_SG_FAHREN:
name = "BMUS_FAH.MDA";
@@ -264,7 +264,7 @@ std::vector<uint8> Dri::load_mda(const GameId& game_id, int page)
return {};
}
} else if(disk_index == 3) {
SDL_RWclose(rw);
sdl::CloseIO(rw);
switch (game_id.game) {
case GameId::TOUSHIN_HINT:
name = "CMUS_T1.MDA";
@@ -281,27 +281,27 @@ std::vector<uint8> Dri::load_mda(const GameId& game_id, int page)
}
} else if(disk_index != 1) {
// AMUS.MDA以外にリンクされている場合は失敗
SDL_RWclose(rw);
sdl::CloseIO(rw);
return {};
}
// データ取得
SDL_RWseek(rw, link_index * 2, RW_SEEK_SET);
SDL_RWread(rw, buf, 4, 1);
sdl::SeekIO(rw, link_index * 2, SEEK_SET);
sdl::ReadIO(rw, buf, 4);
int start_sector = buf[0] | (buf[1] << 8);
int end_sector = buf[2] | (buf[3] << 8);
int size = (end_sector - start_sector) * 256;
if (size == 0) {
// サイズ不正
SDL_RWclose(rw);
sdl::CloseIO(rw);
return {};
}
std::vector<uint8_t> buffer(size);
SDL_RWseek(rw, (start_sector - 1) * 256, RW_SEEK_SET);
SDL_RWread(rw, buffer.data(), size, 1);
sdl::SeekIO(rw, (start_sector - 1) * 256, SEEK_SET);
sdl::ReadIO(rw, buffer.data(), size);
SDL_RWclose(rw);
sdl::CloseIO(rw);
return buffer;
}
+1 -1
View File
@@ -1,5 +1,5 @@
#include <string.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "game_id.h"
#include "fileio.h"
#include "config.h"
+18 -18
View File
@@ -6,7 +6,7 @@
#include <memory>
#include <vector>
#include <SDL.h>
#include "sdl_compat.h"
#include <RtMidi.h>
#include "mako_midi.h"
@@ -98,7 +98,7 @@ public:
bool load_mml(const std::vector<uint8_t>& data);
void load_mda(const std::vector<uint8_t>& data);
void start_midi();
bool play_midi(SDL_atomic_t* current_loop, SDL_atomic_t* current_mark);
bool play_midi(sdl::AtomicInt* current_loop, sdl::AtomicInt* current_mark);
int seq() const { return seq_; }
private:
@@ -231,7 +231,7 @@ void Playback::start_midi()
play_time = 0;
}
bool Playback::play_midi(SDL_atomic_t* current_loop, SDL_atomic_t* current_mark)
bool Playback::play_midi(sdl::AtomicInt* current_loop, sdl::AtomicInt* current_mark)
{
// 経過時間の取得
Uint32 current_time = SDL_GetTicks();
@@ -272,7 +272,7 @@ bool Playback::play_midi(SDL_atomic_t* current_loop, SDL_atomic_t* current_mark)
!play[6].loop_flag && !play[7].loop_flag && !play[8].loop_flag) {
// 全チャンネルが再生停止 (ループしない曲)
stop_midi();
SDL_AtomicSet(current_loop, 1);
sdl::SetAtomicInt(current_loop, 1);
return false;
}
play[i].wait_time = 1;
@@ -284,7 +284,7 @@ bool Playback::play_midi(SDL_atomic_t* current_loop, SDL_atomic_t* current_mark)
loop = play[j].loop_cnt;
}
}
SDL_AtomicSet(current_loop, loop);
sdl::SetAtomicInt(current_loop, loop);
if(loop_ && loop >= loop_) {
// 指定回数だけ再生完了
stop_midi();
@@ -311,7 +311,7 @@ bool Playback::play_midi(SDL_atomic_t* current_loop, SDL_atomic_t* current_mark)
}
play[i].note = 128;
} else if(d0 == 0xe0) {
SDL_AtomicSet(current_mark, mml[i].next());
sdl::SetAtomicInt(current_mark, mml[i].next());
} else if(d0 == 0xe1) {
d1 = mml[i].next();
play[i].velocity += (d1 > 127) ? (d1 - 256) : d1;
@@ -638,9 +638,9 @@ MAKOMidi::MAKOMidi(int device)
thread = SDL_CreateThread(thread_main, "MAKOMidi", this);
queue_mutex = SDL_CreateMutex();
}
SDL_AtomicSet(&current_seq, 0);
SDL_AtomicSet(&current_loop, 0);
SDL_AtomicSet(&current_mark, 0);
sdl::SetAtomicInt(&current_seq, 0);
sdl::SetAtomicInt(&current_loop, 0);
sdl::SetAtomicInt(&current_mark, 0);
}
MAKOMidi::~MAKOMidi()
@@ -666,7 +666,7 @@ bool MAKOMidi::play(const GameId& game_id, Dri& amus, Dri& mda, int page, int lo
auto playback = Playback::create(game_id, amus, mda, page, loop, seq);
if (!playback)
return false;
SDL_AtomicSet(&current_seq, seq);
sdl::SetAtomicInt(&current_seq, seq);
SDL_LockMutex(queue_mutex);
queue.push(std::make_unique<MAKOMidi::Command>(Command::PLAY, std::move(playback)));
SDL_UnlockMutex(queue_mutex);
@@ -675,7 +675,7 @@ bool MAKOMidi::play(const GameId& game_id, Dri& amus, Dri& mda, int page, int lo
void MAKOMidi::stop()
{
SDL_AtomicSet(&current_seq, 0);
sdl::SetAtomicInt(&current_seq, 0);
SDL_LockMutex(queue_mutex);
queue.push(std::make_unique<MAKOMidi::Command>(Command::STOP));
SDL_UnlockMutex(queue_mutex);
@@ -683,13 +683,13 @@ void MAKOMidi::stop()
bool MAKOMidi::is_playing()
{
return SDL_AtomicGet(&current_seq) != 0;
return sdl::GetAtomicInt(&current_seq) != 0;
}
void MAKOMidi::get_mark(int* mark, int* loop)
{
*mark = SDL_AtomicGet(&current_mark);
*loop = SDL_AtomicGet(&current_loop);
*mark = sdl::GetAtomicInt(&current_mark);
*loop = sdl::GetAtomicInt(&current_loop);
}
void MAKOMidi::thread_loop()
@@ -708,8 +708,8 @@ void MAKOMidi::thread_loop()
stop_midi();
current = std::move(cmd->playback);
current->start_midi();
SDL_AtomicSet(&current_loop, 0);
SDL_AtomicSet(&current_mark, 0);
sdl::SetAtomicInt(&current_loop, 0);
sdl::SetAtomicInt(&current_mark, 0);
SDL_Delay(100); // ?
break;
case Command::STOP:
@@ -726,7 +726,7 @@ void MAKOMidi::thread_loop()
SDL_UnlockMutex(queue_mutex);
if (current) {
if (!current->play_midi(&current_loop, &current_mark)) {
SDL_AtomicCAS(&current_seq, current->seq(), 0);
sdl::CompareAndSwapAtomicInt(&current_seq, current->seq(), 0);
current.reset();
}
}
@@ -737,7 +737,7 @@ void MAKOMidi::thread_loop()
// static
int MAKOMidi::thread_main(void* data)
{
SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
sdl::SetCurrentThreadPriority(SDL_THREAD_PRIORITY_HIGH);
MAKOMidi* mm = reinterpret_cast<MAKOMidi*>(data);
mm->thread_loop();
+5 -5
View File
@@ -3,7 +3,7 @@
#include <memory>
#include <queue>
#include <SDL.h>
#include "sdl_compat.h"
#include "common.h"
struct GameId;
@@ -24,11 +24,11 @@ public:
private:
struct Command;
std::queue<std::unique_ptr<Command>> queue;
SDL_mutex* queue_mutex = nullptr;
sdl::Mutex* queue_mutex = nullptr;
SDL_Thread* thread = nullptr;
SDL_atomic_t current_seq;
SDL_atomic_t current_loop;
SDL_atomic_t current_mark;
sdl::AtomicInt current_seq;
sdl::AtomicInt current_loop;
sdl::AtomicInt current_mark;
int next_seq = 0;
void thread_loop();
-35
View File
@@ -1,35 +0,0 @@
#ifndef MAKO_MUSIC_H_
#define MAKO_MUSIC_H_
#include <memory>
#include <string>
#include <SDL.h>
class MakoMusicDecoder;
// Plays a single BGM file (MP3, OGG, or WAV, chosen by file extension),
// decoding on demand and mixing into the audio callback's output buffer.
class MakoMusic {
public:
// loops: number of times to play; 0 means loop forever.
MakoMusic(const std::string& path, int loops, const SDL_AudioSpec& device_spec);
~MakoMusic();
bool is_open() const { return stream != nullptr; }
bool is_playing() const { return playing; }
// Called from the audio callback.
void mix(Uint8* out, int len);
private:
// Decodes and queues one chunk, handling EOF and decoder errors.
void decode();
std::unique_ptr<MakoMusicDecoder> decoder;
SDL_AudioStream* stream = nullptr;
int loops_; // number of times to play, 0 for infinite loop
bool playing = false;
bool input_finished = false;
};
#endif // MAKO_MUSIC_H_
@@ -1,31 +1,16 @@
#include "mako_music.h"
#include "music_decoder.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <memory>
#include "common.h"
#define DR_MP3_IMPLEMENTATION
#include "dr_mp3.h"
#include "stb_vorbis.h"
struct DecodedChunk {
const Uint8* data;
int frames;
};
class MakoMusicDecoder {
public:
virtual ~MakoMusicDecoder() = default;
virtual bool is_open() const = 0;
virtual const SDL_AudioSpec& spec() const = 0;
virtual DecodedChunk decode(int max_frames) = 0;
virtual void seek_start() = 0;
};
namespace {
class Mp3Decoder final : public MakoMusicDecoder {
class Mp3Decoder final : public MusicDecoder {
public:
explicit Mp3Decoder(const std::string& path)
{
@@ -35,7 +20,7 @@ public:
}
initialized = true;
spec_.freq = static_cast<int>(mp3.sampleRate);
spec_.format = AUDIO_S16SYS;
spec_.format = sdl::AudioS16;
spec_.channels = static_cast<Uint8>(mp3.channels);
}
@@ -63,7 +48,7 @@ private:
bool initialized = false;
};
class OggDecoder final : public MakoMusicDecoder {
class OggDecoder final : public MusicDecoder {
public:
explicit OggDecoder(const std::string& path)
{
@@ -75,7 +60,7 @@ public:
}
stb_vorbis_info info = stb_vorbis_get_info(vorbis);
spec_.freq = static_cast<int>(info.sample_rate);
spec_.format = AUDIO_S16SYS;
spec_.format = sdl::AudioS16;
spec_.channels = static_cast<Uint8>(info.channels);
}
@@ -103,7 +88,7 @@ private:
std::array<int16_t, 1024 * 2> pcm;
};
class WavDecoder final : public MakoMusicDecoder {
class WavDecoder final : public MusicDecoder {
public:
explicit WavDecoder(const std::string& path)
{
@@ -119,7 +104,7 @@ public:
~WavDecoder() override
{
if (data)
SDL_FreeWAV(data);
sdl::FreeWAV(data);
}
bool is_open() const override { return data_size > 0 && frame_size > 0; }
@@ -155,9 +140,11 @@ bool has_extension(const std::string& path, const char* ext)
return tail == ext;
}
std::unique_ptr<MakoMusicDecoder> create_decoder(const std::string& path)
} // namespace
std::unique_ptr<MusicDecoder> create_music_decoder(const std::string& path)
{
std::unique_ptr<MakoMusicDecoder> decoder;
std::unique_ptr<MusicDecoder> decoder;
if (has_extension(path, ".ogg") || has_extension(path, ".oga"))
decoder = std::make_unique<OggDecoder>(path);
else if (has_extension(path, ".wav"))
@@ -168,67 +155,3 @@ std::unique_ptr<MakoMusicDecoder> create_decoder(const std::string& path)
return nullptr;
return decoder;
}
} // namespace
MakoMusic::MakoMusic(const std::string& path, int loops, const SDL_AudioSpec& device_spec)
: decoder(create_decoder(path)), loops_(loops)
{
if (!decoder)
return;
const SDL_AudioSpec& src_spec = decoder->spec();
stream = SDL_NewAudioStream(src_spec.format, src_spec.channels, src_spec.freq,
device_spec.format, device_spec.channels, device_spec.freq);
if (!stream) {
WARNING("SDL_NewAudioStream failed: %s", SDL_GetError());
return;
}
playing = true;
}
MakoMusic::~MakoMusic()
{
if (stream)
SDL_FreeAudioStream(stream);
}
void MakoMusic::decode()
{
constexpr int CHUNK_FRAMES = 1024;
DecodedChunk chunk = decoder->decode(CHUNK_FRAMES);
if (chunk.frames == 0) {
if (loops_ && --loops_ == 0) {
SDL_AudioStreamFlush(stream);
input_finished = true;
} else {
decoder->seek_start();
}
return;
}
const SDL_AudioSpec& spec = decoder->spec();
int bytes = chunk.frames * SDL_AUDIO_BITSIZE(spec.format) / 8 * spec.channels;
if (SDL_AudioStreamPut(stream, chunk.data, bytes) < 0) {
WARNING("SDL_AudioStreamPut failed: %s", SDL_GetError());
input_finished = true;
}
}
void MakoMusic::mix(Uint8* out, int len)
{
if (!stream || !playing)
return;
while (SDL_AudioStreamAvailable(stream) < len && !input_finished)
decode();
Uint8* tmp = SDL_stack_alloc(Uint8, len);
int got = SDL_AudioStreamGet(stream, tmp, len);
if (got > 0)
SDL_MixAudioFormat(out, tmp, AUDIO_S16SYS, got, SDL_MIX_MAXVOLUME);
SDL_stack_free(tmp);
if (input_finished && SDL_AudioStreamAvailable(stream) <= 0)
playing = false;
}
+24
View File
@@ -0,0 +1,24 @@
#ifndef MAKO_MUSIC_DECODER_H_
#define MAKO_MUSIC_DECODER_H_
#include <memory>
#include <string>
#include "sdl_compat.h"
struct DecodedChunk {
const Uint8* data;
int frames;
};
class MusicDecoder {
public:
virtual ~MusicDecoder() = default;
virtual bool is_open() const = 0;
virtual const SDL_AudioSpec& spec() const = 0;
virtual DecodedChunk decode(int max_frames) = 0;
virtual void seek_start() = 0;
};
std::unique_ptr<MusicDecoder> create_music_decoder(const std::string& path);
#endif // MAKO_MUSIC_DECODER_H_
+15 -1
View File
@@ -79,7 +79,8 @@ NACT::NACT(const Config& config, const GameId& game_id)
init_windows();
init_text();
SDL_Init(SDL_INIT_GAMECONTROLLER);
SDL_Init(sdl::INIT_GAMEPAD);
#if SYSTEM3_SDL_VERSION == 2
for (int i = 0; i < SDL_NumJoysticks(); ++i) {
if (SDL_IsGameController(i)) {
sdl_gamecontroller = SDL_GameControllerOpen(i);
@@ -90,6 +91,19 @@ NACT::NACT(const Config& config, const GameId& game_id)
}
}
}
#else
int count = 0;
SDL_JoystickID* ids = SDL_GetJoysticks(&count);
for (int i = 0; i < count; ++i) {
if (SDL_IsGamepad(ids[i])) {
sdl_gamecontroller = SDL_OpenGamepad(ids[i]);
if (sdl_gamecontroller)
break;
WARNING("Could not open gamepad %i: %s\n", i, SDL_GetError());
}
}
SDL_free(ids);
#endif
}
NACT::~NACT()
+11 -2
View File
@@ -14,7 +14,7 @@
#include <string_view>
#include <vector>
#include <stdio.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "common.h"
#include "config.h"
#include "cg.h"
@@ -22,6 +22,11 @@
#include "game_id.h"
#include "scenario.h"
#ifdef _WIN32
struct tagMSG;
typedef tagMSG MSG;
#endif
#define RND var[ 0]
#define MAX_VERB 128
@@ -58,6 +63,10 @@ public:
void set_skip_menu_state(bool enabled, bool checked);
#ifdef _WIN32
void handle_windows_event(MSG* msg);
#endif
virtual uint16 cali() = 0;
Scenario sco;
@@ -270,7 +279,7 @@ protected:
// input
bool mouse_move_enabled = true;
bool wait_keydown = true; // ウェイト時のキー受付
SDL_GameController *sdl_gamecontroller = NULL;
sdl::Gamepad *sdl_gamecontroller = NULL;
uint8 get_key(bool notify_texthook = true);
void wait_key_release(uint8_t mask = 0xff);
+4 -5
View File
@@ -568,7 +568,7 @@ private:
base = CELL_EMPTY;
if (base == CELL_EMPTY) {
SDL_Rect rect = { px, py, TILE_SIZE, TILE_SIZE };
SDL_FillRect(composed_map.surface(), &rect, 0);
sdl::FillSurfaceRect(composed_map.surface(), &rect, 0);
} else {
draw_map_tile(base, px, py, false);
}
@@ -666,8 +666,7 @@ private:
sheet_x, sheet_y, width, height, cg.width(), cg.height());
return std::nullopt;
}
SDL_SetColorKey(cg.surface(), transparent ? SDL_TRUE : SDL_FALSE,
TRANSPARENT_COLOR);
sdl::SetSurfaceColorKey(cg.surface(), transparent, TRANSPARENT_COLOR);
return SDL_Rect{ sheet_x, sheet_y, width, height };
}
@@ -1472,7 +1471,7 @@ private:
get_cursor(&seen_x, &seen_y);
// Hides the system pointer and draws the cursor at (cx, cy) instead.
auto show_cursor = [&] {
SDL_ShowCursor(SDL_DISABLE);
sdl::HideCursor();
draw_mouse_cursor(shape, cx, cy);
cursor_drawn = true;
};
@@ -1481,7 +1480,7 @@ private:
auto hide_cursor = [&] {
if (!cursor_drawn)
return;
SDL_ShowCursor(SDL_ENABLE);
sdl::ShowCursor();
ags->copy_screen(SCREEN_BACK, SCREEN_FRONT, cx, cy, cx + MOUSE_CURSOR_SIZE - 1,
cy + MOUSE_CURSOR_SIZE - 1, cx, cy);
cursor_drawn = false;
+26 -22
View File
@@ -8,7 +8,6 @@
#include <windows.h>
#undef ERROR
#endif
#include <SDL_syswm.h>
#include "nact.h"
#include "ags.h"
#include "texthook.h"
@@ -21,6 +20,7 @@ enum TouchState {
};
extern SDL_Window* g_window;
extern SDL_Renderer* g_renderer;
static int mousex, mousey, wheel;
static TouchState touch_state = TOUCH_NONE;
@@ -30,33 +30,37 @@ void NACT::handle_event(SDL_Event e)
return;
switch (e.type) {
case SDL_QUIT:
case sdl::EVENT_QUIT:
show_quit_dialog();
break;
#ifdef __ANDROID__
case SDL_KEYUP:
if (e.key.keysym.scancode == SDL_SCANCODE_AC_BACK) {
case sdl::EVENT_KEY_UP:
if (sdl::GetKeyScancode(e.key) == SDL_SCANCODE_AC_BACK) {
show_quit_dialog();
}
break;
#endif
case SDL_MOUSEMOTION:
mousex = e.motion.x * ags->screen_width / ags->window_width;
mousey = e.motion.y * ags->screen_height / ags->window_height;
case sdl::EVENT_MOUSE_MOTION: {
float render_x, render_y;
sdl::RenderCoordinatesFromWindow(g_renderer, e.motion.x, e.motion.y,
&render_x, &render_y);
mousex = render_x * ags->screen_width / ags->window_width;
mousey = render_y * ags->screen_height / ags->window_height;
break;
}
case SDL_MOUSEWHEEL:
case sdl::EVENT_MOUSE_WHEEL:
wheel += e.wheel.y * (e.wheel.direction == SDL_MOUSEWHEEL_FLIPPED ? -1 : 1);
break;
case SDL_FINGERDOWN:
case SDL_FINGERUP:
case SDL_FINGERMOTION:
case sdl::EVENT_FINGER_DOWN:
case sdl::EVENT_FINGER_UP:
case sdl::EVENT_FINGER_MOTION:
mousex = e.tfinger.x * ags->screen_width;
mousey = e.tfinger.y * ags->screen_height;
switch (SDL_GetNumTouchFingers(e.tfinger.touchId)) {
switch (sdl::GetNumTouchFingers(sdl::GetTouchID(e.tfinger))) {
case 0:
touch_state = TOUCH_NONE;
break;
@@ -116,7 +120,7 @@ uint8 NACT::get_key(bool notify_texthook)
pump_events();
// キーボード&マウス
const Uint8* key = SDL_GetKeyboardState(NULL);
const auto* key = SDL_GetKeyboardState(NULL);
Uint32 mouse = SDL_GetMouseState(NULL, NULL);
if(key[SDL_SCANCODE_UP ] || key[SDL_SCANCODE_KP_8 ] ) val |= 0x01;
@@ -131,14 +135,14 @@ uint8 NACT::get_key(bool notify_texthook)
// マウス移動で方向入力はサポートしない
if(sdl_gamecontroller) {
if(SDL_GameControllerGetButton(sdl_gamecontroller, SDL_CONTROLLER_BUTTON_DPAD_UP) || SDL_GameControllerGetAxis(sdl_gamecontroller, SDL_CONTROLLER_AXIS_LEFTY) <= -8000) val |= 0x01;
if(SDL_GameControllerGetButton(sdl_gamecontroller, SDL_CONTROLLER_BUTTON_DPAD_DOWN) || SDL_GameControllerGetAxis(sdl_gamecontroller, SDL_CONTROLLER_AXIS_LEFTY) >= 8000) val |= 0x02;
if(SDL_GameControllerGetButton(sdl_gamecontroller, SDL_CONTROLLER_BUTTON_DPAD_LEFT) || SDL_GameControllerGetAxis(sdl_gamecontroller, SDL_CONTROLLER_AXIS_LEFTX) <= -8000) val |= 0x04;
if(SDL_GameControllerGetButton(sdl_gamecontroller, SDL_CONTROLLER_BUTTON_DPAD_RIGHT) || SDL_GameControllerGetAxis(sdl_gamecontroller, SDL_CONTROLLER_AXIS_LEFTX) >= 8000) val |= 0x08;
if(SDL_GameControllerGetButton(sdl_gamecontroller, SDL_CONTROLLER_BUTTON_A)) val |= 0x10;
if(SDL_GameControllerGetButton(sdl_gamecontroller, SDL_CONTROLLER_BUTTON_B)) val |= 0x20;
if(SDL_GameControllerGetButton(sdl_gamecontroller, SDL_CONTROLLER_BUTTON_X)) val |= 0x40;
if(SDL_GameControllerGetButton(sdl_gamecontroller, SDL_CONTROLLER_BUTTON_Y)) val |= 0x80;
if(sdl::GetGamepadButton(sdl_gamecontroller, sdl::GAMEPAD_BUTTON_DPAD_UP) || sdl::GetGamepadAxis(sdl_gamecontroller, sdl::GAMEPAD_AXIS_LEFTY) <= -8000) val |= 0x01;
if(sdl::GetGamepadButton(sdl_gamecontroller, sdl::GAMEPAD_BUTTON_DPAD_DOWN) || sdl::GetGamepadAxis(sdl_gamecontroller, sdl::GAMEPAD_AXIS_LEFTY) >= 8000) val |= 0x02;
if(sdl::GetGamepadButton(sdl_gamecontroller, sdl::GAMEPAD_BUTTON_DPAD_LEFT) || sdl::GetGamepadAxis(sdl_gamecontroller, sdl::GAMEPAD_AXIS_LEFTX) <= -8000) val |= 0x04;
if(sdl::GetGamepadButton(sdl_gamecontroller, sdl::GAMEPAD_BUTTON_DPAD_RIGHT) || sdl::GetGamepadAxis(sdl_gamecontroller, sdl::GAMEPAD_AXIS_LEFTX) >= 8000) val |= 0x08;
if(sdl::GetGamepadButton(sdl_gamecontroller, sdl::GAMEPAD_BUTTON_SOUTH)) val |= 0x10;
if(sdl::GetGamepadButton(sdl_gamecontroller, sdl::GAMEPAD_BUTTON_EAST)) val |= 0x20;
if(sdl::GetGamepadButton(sdl_gamecontroller, sdl::GAMEPAD_BUTTON_WEST)) val |= 0x40;
if(sdl::GetGamepadButton(sdl_gamecontroller, sdl::GAMEPAD_BUTTON_NORTH)) val |= 0x80;
}
return val;
@@ -186,7 +190,7 @@ void NACT::show_quit_dialog()
buttons,
};
int buttonid = 0;
if (SDL_ShowMessageBox(&messagebox_data, &buttonid) < 0) {
if (!sdl::ShowMessageBox(&messagebox_data, &buttonid)) {
WARNING("error displaying message box");
buttonid = 1;
}
+1 -1
View File
@@ -3,7 +3,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <SDL.h>
#include "sdl_compat.h"
#include "common.h"
#include "texthook.h"
#ifdef __EMSCRIPTEN__
+58
View File
@@ -0,0 +1,58 @@
#ifndef SYSTEM3_TTF_COMPAT_H_
#define SYSTEM3_TTF_COMPAT_H_
#include "sdl_compat.h"
#if SYSTEM3_SDL_VERSION == 2
#include <SDL_ttf.h>
#elif SYSTEM3_SDL_VERSION == 3
#include <SDL3_ttf/SDL_ttf.h>
#else
#error "Unsupported SYSTEM3_SDL_VERSION"
#endif
static_assert(SDL_TTF_MAJOR_VERSION == SYSTEM3_SDL_VERSION,
"SDL_ttf and SDL must use the same major version");
namespace ttf {
inline TTF_Font* OpenFontIO(sdl::IOStream* stream, int point_size)
{
#if SYSTEM3_SDL_VERSION == 2
return TTF_OpenFontRW(stream, 0, point_size);
#else
return TTF_OpenFontIO(stream, false, point_size);
#endif
}
inline int GetFontAscent(TTF_Font* font)
{
#if SYSTEM3_SDL_VERSION == 2
return TTF_FontAscent(font);
#else
return TTF_GetFontAscent(font);
#endif
}
inline int GetFontDescent(TTF_Font* font)
{
#if SYSTEM3_SDL_VERSION == 2
return TTF_FontDescent(font);
#else
return TTF_GetFontDescent(font);
#endif
}
inline bool GetGlyphMetrics(TTF_Font* font, Uint32 code, int* minx, int* maxx,
int* miny, int* maxy, int* advance)
{
#if SYSTEM3_SDL_VERSION == 2
return TTF_GlyphMetrics(font, code, minx, maxx, miny, maxy, advance) == 0;
#else
return TTF_GetGlyphMetrics(font, code, minx, maxx, miny, maxy, advance);
#endif
}
} // namespace ttf
#endif // SYSTEM3_TTF_COMPAT_H_
+47 -6
View File
@@ -3,7 +3,9 @@
#undef ERROR
#include <time.h>
#include "nact.h"
#if SYSTEM3_SDL_VERSION == 2
#include "SDL_syswm.h"
#endif
#include "encoding.h"
#include "ags.h"
#include "mako.h"
@@ -19,12 +21,34 @@ namespace {
bool auto_copy_enabled = false;
HWND get_hwnd(SDL_Window* window) {
#if SYSTEM3_SDL_VERSION == 2
SDL_SysWMinfo info;
SDL_VERSION(&info.version);
SDL_GetWindowWMInfo(window, &info);
return info.info.win.window;
#else
return (HWND)SDL_GetPointerProperty(
SDL_GetWindowProperties(window), SDL_PROP_WINDOW_WIN32_HWND_POINTER, NULL);
#endif
}
void set_fullscreen(bool fullscreen)
{
#if SYSTEM3_SDL_VERSION == 2
SDL_SetWindowFullscreen(
g_window, fullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0);
#else
SDL_SetWindowFullscreen(g_window, fullscreen);
#endif
}
#if SYSTEM3_SDL_VERSION == 3
bool SDLCALL windows_message_hook(void* userdata, MSG* msg) {
static_cast<NACT*>(userdata)->handle_windows_event(msg);
return true;
}
#endif
void init_menu(bool mouse_move_enabled, const Config& config)
{
HINSTANCE hinst = (HINSTANCE)GetModuleHandle(NULL);
@@ -146,7 +170,11 @@ void NACT::platform_initialize()
{
init_menu(mouse_move_enabled, config);
init_console(config, game_id.sys_ver);
#if SYSTEM3_SDL_VERSION == 2
SDL_EventState(SDL_SYSWMEVENT, SDL_ENABLE);
#else
SDL_SetWindowsMessageHook(windows_message_hook, this);
#endif
}
void NACT::platform_finalize()
@@ -194,13 +222,27 @@ bool NACT::handle_platform_event(const SDL_Event& e)
return false;
}
#if SYSTEM3_SDL_VERSION == 2
if (e.type != SDL_SYSWMEVENT)
return false;
const SDL_SysWMmsg* msg = e.syswm.msg;
const SDL_SysWMmsg* syswm = e.syswm.msg;
MSG msg = {};
msg.hwnd = syswm->msg.win.hwnd;
msg.message = syswm->msg.win.msg;
msg.wParam = syswm->msg.win.wParam;
msg.lParam = syswm->msg.win.lParam;
handle_windows_event(&msg);
return true;
#else
return false;
#endif
}
switch (msg->msg.win.msg) {
void NACT::handle_windows_event(MSG* msg)
{
switch (msg->message) {
case WM_COMMAND:
switch (msg->msg.win.wParam) {
switch (msg->wParam) {
case ID_SCREENSHOT:
save_screenshot(ags);
break;
@@ -211,10 +253,10 @@ bool NACT::handle_platform_event(const SDL_Event& e)
quit(0);
break;
case ID_SCREEN_WINDOW:
SDL_SetWindowFullscreen(g_window, 0);
set_fullscreen(false);
break;
case ID_SCREEN_FULL:
SDL_SetWindowFullscreen(g_window, SDL_WINDOW_FULLSCREEN_DESKTOP);
set_fullscreen(true);
break;
case ID_SCANLINE:
ags->set_scanline_mode(!ags->get_scanline_mode());
@@ -252,5 +294,4 @@ bool NACT::handle_platform_event(const SDL_Event& e)
}
break;
}
return true;
}
+2 -1
View File
@@ -12,7 +12,8 @@ git submodule update --init
sudo (dkp-)pacman -S switch-dev switch-sdl2 switch-sdl2_ttf
mkdir -p out/debug
cd out/debug
/opt/devkitpro/portlibs/switch/bin/aarch64-none-elf-cmake -DCMAKE_BUILD_TYPE=Debug ../../
/opt/devkitpro/portlibs/switch/bin/aarch64-none-elf-cmake \
-DCMAKE_BUILD_TYPE=Debug -DSYSTEM3_SDL_VERSION=2 ../../
make
```