mirror of
https://github.com/kichikuou/xsystem35-sdl2.git
synced 2026-09-23 15:08:04 +03:00
Compare commits
114
Commits
virtual-pointer
...
sdl3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48458ac034 | ||
|
|
ffdd362f35 | ||
|
|
78519bb5ef | ||
|
|
5989fe72e2 | ||
|
|
66d0636197 | ||
|
|
6c8590f8d4 | ||
|
|
3cbe4c73d8 | ||
|
|
80da6cd782 | ||
|
|
2f73abd486 | ||
|
|
5844226b2f | ||
|
|
87596c58c5 | ||
|
|
212a7bf138 | ||
|
|
652724a9ab | ||
|
|
658debdcf3 | ||
|
|
ed4fbc1f19 | ||
|
|
c1910382f8 | ||
|
|
495d6146f3 | ||
|
|
6f01734215 | ||
|
|
b55574c162 | ||
|
|
9f92186905 | ||
|
|
d80d82912a | ||
|
|
eeb4953213 | ||
|
|
5abc9e8177 | ||
|
|
5d7ec418b1 | ||
|
|
e488123345 | ||
|
|
5eea90c9a8 | ||
|
|
677ecb5a0f | ||
|
|
1918350b19 | ||
|
|
9504ee7b65 | ||
|
|
f033a507d3 | ||
|
|
fbeb9a780e | ||
|
|
8c69f53073 | ||
|
|
cc96c85381 | ||
|
|
a2618319d9 | ||
|
|
30fd5768da | ||
|
|
15a769d83c | ||
|
|
253de6adc8 | ||
|
|
a0daa3261c | ||
|
|
98e9d8bcfb | ||
|
|
b7854aaec1 | ||
|
|
905eb34590 | ||
|
|
8f22566adb | ||
|
|
48ded43e2a | ||
|
|
571bec63a2 | ||
|
|
345c017c22 | ||
|
|
205004086b | ||
|
|
6820bcfcb8 | ||
|
|
5ce1494b59 | ||
|
|
dd45e566f6 | ||
|
|
1ccc7630c1 | ||
|
|
6456f59c37 | ||
|
|
f748e8f678 | ||
|
|
a9e23508b7 | ||
|
|
c3bd978f02 | ||
|
|
50a6b17c5e | ||
|
|
0d49619366 | ||
|
|
da3cc5d900 | ||
|
|
1785e93fcc | ||
|
|
a77e849f82 | ||
|
|
978dbdbc3b | ||
|
|
e3c168c5a6 | ||
|
|
9f8c2af842 | ||
|
|
5d8b1c5632 | ||
|
|
30188c3a4f | ||
|
|
a627a8ea3d | ||
|
|
0cf9c1fe8d | ||
|
|
0887c44292 | ||
|
|
27a0c89a73 | ||
|
|
5c827225ab | ||
|
|
bc37ba6256 | ||
|
|
ab4bc2d8d1 | ||
|
|
f26321c5e0 | ||
|
|
1d897c72d0 | ||
|
|
172c76b4e2 | ||
|
|
100584f896 | ||
|
|
975f618b53 | ||
|
|
0c8bab0c64 | ||
|
|
0192fd3d8e | ||
|
|
d69fc5c6a3 | ||
|
|
68a39843e2 | ||
|
|
e3d7b60785 | ||
|
|
2bb891ada8 | ||
|
|
f109fe11f5 | ||
|
|
73b3377b77 | ||
|
|
9400572502 | ||
|
|
a1e063c4b2 | ||
|
|
602c55a5e5 | ||
|
|
9624192a41 | ||
|
|
5c7436e2b1 | ||
|
|
6c9844697f | ||
|
|
210c0a13e4 | ||
|
|
3ebb1faece | ||
|
|
286f150510 | ||
|
|
a194cf10a6 | ||
|
|
9587d350ec | ||
|
|
02f62d51bb | ||
|
|
54b98ec3e4 | ||
|
|
8cde827ee6 | ||
|
|
a5d67b067c | ||
|
|
5cb70110b9 | ||
|
|
e3dcadd5dd | ||
|
|
ff31d03153 | ||
|
|
63886d7d00 | ||
|
|
c119c2f969 | ||
|
|
fb63d42ec1 | ||
|
|
6f631d073d | ||
|
|
49dca57fbd | ||
|
|
c8987da470 | ||
|
|
0dfb3eb378 | ||
|
|
269132fede | ||
|
|
651fc21a19 | ||
|
|
03f86bc4d7 | ||
|
|
4ffab8e4e9 | ||
|
|
1593452146 |
@@ -24,5 +24,6 @@ jobs:
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: xsystem35-android-sdl3
|
||||
path: android/app/build/outputs/apk/release/app-release.apk
|
||||
archive: false
|
||||
|
||||
@@ -2,12 +2,17 @@ name: Emscripten Build
|
||||
on: [push, pull_request]
|
||||
|
||||
env:
|
||||
EM_VERSION: latest
|
||||
EM_VERSION: 6.0.10
|
||||
EM_CACHE_FOLDER: 'emsdk-cache'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
sdl-version: [2, 3]
|
||||
name: Emscripten SDL${{ matrix.sdl-version }}
|
||||
steps:
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
@@ -17,7 +22,7 @@ jobs:
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: ${{env.EM_CACHE_FOLDER}}
|
||||
key: ${{env.EM_VERSION}}-${{ runner.os }}
|
||||
key: ${{env.EM_VERSION}}-${{ runner.os }}-sdl${{ matrix.sdl-version }}
|
||||
|
||||
- name: Setup Emscripten toolchain
|
||||
uses: emscripten-core/setup-emsdk@v16
|
||||
@@ -30,15 +35,16 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
mkdir -p out/wasm
|
||||
cd out/wasm
|
||||
emcmake cmake -DCMAKE_BUILD_TYPE=MinSizeRel -DCMAKE_COMPILE_WARNING_AS_ERROR=YES ../../
|
||||
make -j4
|
||||
mkdir xsystem35
|
||||
mv src/xsystem35.* xsystem35/
|
||||
emcmake cmake -S . -B out/wasm \
|
||||
-DCMAKE_BUILD_TYPE=MinSizeRel \
|
||||
-DCMAKE_COMPILE_WARNING_AS_ERROR=YES \
|
||||
-DXSYSTEM35_SDL_VERSION=${{ matrix.sdl-version }}
|
||||
cmake --build out/wasm -j4
|
||||
mkdir out/wasm/xsystem35
|
||||
mv out/wasm/src/xsystem35.* out/wasm/xsystem35/
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: xsystem35-wasm
|
||||
name: xsystem35-wasm-sdl${{ matrix.sdl-version }}
|
||||
path: out/wasm/xsystem35
|
||||
|
||||
@@ -3,27 +3,45 @@ on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-26.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
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
|
||||
|
||||
- name: Install Deps
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install libgtk-3-dev libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev libwebp-dev libportmidi-dev libcjson-dev asciidoctor
|
||||
sudo apt install libfreetype-dev libwebp-dev libportmidi-dev libcjson-dev asciidoctor gettext
|
||||
if [ "${{ matrix.sdl-version }}" = 2 ]; then
|
||||
sudo apt install libsdl2-dev libsdl2-mixer-dev
|
||||
else
|
||||
sudo apt install libsdl3-dev
|
||||
fi
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
mkdir -p out/${{ matrix.build-type }}
|
||||
cd out/${{ matrix.build-type }}
|
||||
cmake -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} ../../
|
||||
make -j4
|
||||
cmake -S . -B out/build -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \
|
||||
-DXSYSTEM35_SDL_VERSION=${{ matrix.sdl-version }}
|
||||
cmake --build out/build -j4
|
||||
|
||||
- name: Test
|
||||
run: ctest --output-on-failure
|
||||
working-directory: out/${{ matrix.build-type }}
|
||||
run: ctest --test-dir out/build --output-on-failure
|
||||
|
||||
- name: Verify xsystem35.pot is up to date
|
||||
if: matrix.build-type == 'Debug' && matrix.sdl-version == 2
|
||||
run: |
|
||||
xgettext --default-domain=xsystem35 --directory=. \
|
||||
--keyword=_ --keyword=N_ \
|
||||
--files-from=po/POTFILES.in \
|
||||
--output=/tmp/fresh.pot
|
||||
if ! diff <(grep '^msgid' po/xsystem35.pot | sort) \
|
||||
<(grep '^msgid' /tmp/fresh.pot | sort); then
|
||||
echo "::error::po/xsystem35.pot is out of date. Build the 'pot' target and commit the changes under po/."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -9,11 +9,23 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- sys: mingw32
|
||||
package: "xsystem35-32bit"
|
||||
package: "xsystem35-sdl2-32bit"
|
||||
sdl-version: 2
|
||||
sdl-deps: "SDL2:p SDL2_mixer:p"
|
||||
cmake_args: "-DENABLE_DEBUGGER=no"
|
||||
deps: ""
|
||||
- sys: ucrt64
|
||||
package: "xsystem35-64bit"
|
||||
package: "xsystem35-sdl2-64bit"
|
||||
sdl-version: 2
|
||||
sdl-deps: "SDL2:p SDL2_mixer:p"
|
||||
cmake_args: ""
|
||||
deps: >-
|
||||
cjson:p
|
||||
portmidi:p
|
||||
- sys: ucrt64
|
||||
package: "xsystem35-sdl3-64bit"
|
||||
sdl-version: 3
|
||||
sdl-deps: "sdl3:p sdl3-mixer:p"
|
||||
cmake_args: ""
|
||||
deps: >-
|
||||
cjson:p
|
||||
@@ -21,7 +33,7 @@ jobs:
|
||||
defaults:
|
||||
run:
|
||||
shell: msys2 {0}
|
||||
name: MSYS2 ${{ matrix.sys }}
|
||||
name: MSYS2 SDL${{ matrix.sdl-version }} ${{ matrix.sys }}
|
||||
steps:
|
||||
|
||||
- name: Set up MSYS2
|
||||
@@ -29,9 +41,8 @@ jobs:
|
||||
with:
|
||||
msystem: ${{ matrix.sys }}
|
||||
pacboy: >-
|
||||
SDL2:p
|
||||
SDL2_ttf:p
|
||||
SDL2_mixer:p
|
||||
${{ matrix.sdl-deps }}
|
||||
freetype:p
|
||||
libwebp:p
|
||||
${{ matrix.deps }}
|
||||
|
||||
@@ -50,10 +61,11 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
mkdir -p out/release
|
||||
cd out/release
|
||||
cmake -GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_COMPILE_WARNING_AS_ERROR=YES ${{ matrix.cmake_args }} ../../
|
||||
ninja
|
||||
cmake -S . -B out/release -GNinja -DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_COMPILE_WARNING_AS_ERROR=YES \
|
||||
-DXSYSTEM35_SDL_VERSION=${{ matrix.sdl-version }} \
|
||||
${{ matrix.cmake_args }}
|
||||
cmake --build out/release
|
||||
|
||||
- name: Test
|
||||
run: ctest --output-on-failure
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
out
|
||||
android/.gradle
|
||||
android/.idea
|
||||
android/app/aars
|
||||
android/app/.cxx
|
||||
android/app/.externalNativeBuild
|
||||
android/app/build
|
||||
|
||||
@@ -1,5 +1,29 @@
|
||||
# Changelog
|
||||
|
||||
## 2.20.0 - 2026-09-11
|
||||
- Improved font rendering to more closely match the original engine. On
|
||||
Windows, MS Gothic and MS Mincho are now used by default when available.
|
||||
- The experimental `enable_zb` option has been removed. (#44)
|
||||
- SACT: Message skipping now also skips text animation, effects, waits, and
|
||||
related input waits.
|
||||
- SACT: BGM now honors the loop count specified by the game.
|
||||
- SACT: Fixed timer handling in 楽園行.
|
||||
- The popup menu and dialogs can now be operated with the keyboard.
|
||||
- Fixed mouse coordinates changing unexpectedly when automatic mouse movement
|
||||
is disabled in System 3.8/3.9 games.
|
||||
- Android: The status bar now stays hidden after returning to the game. (#81)
|
||||
- Debugger: Added an `info cache` command for viewing cache usage and hit rates.
|
||||
|
||||
## 2.19.1 - 2026-08-02
|
||||
- Fixed an issue that prevented moving upward during battles in Rance 4.
|
||||
|
||||
## 2.19.0 - 2026-07-19
|
||||
- Replaced the GTK-based popup menu and dialogs with a pure-SDL implementation.
|
||||
- On Android, the popup menu opens with a three-finger tap.
|
||||
- Android: Added installation directly from CD-ROM images.
|
||||
- Implemented volume control panel.
|
||||
- Added `mute_on_unfocus` option to mute audio while the window is unfocused.
|
||||
|
||||
## 2.18.0 - 2026-06-23
|
||||
- Added support for the intro demo of Daiakuji.
|
||||
- Replaced the bundled mincho font with IPA Mincho.
|
||||
|
||||
+88
-31
@@ -5,7 +5,24 @@ set(CMAKE_C_STANDARD 99)
|
||||
enable_testing()
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)
|
||||
|
||||
set(XSYSTEM35_VERSION "2.18.0")
|
||||
set(XSYSTEM35_VERSION "2.20.0")
|
||||
|
||||
if (ANDROID)
|
||||
set(XSYSTEM35_DEFAULT_SDL_VERSION 3)
|
||||
else()
|
||||
set(XSYSTEM35_DEFAULT_SDL_VERSION 2)
|
||||
endif()
|
||||
set(XSYSTEM35_SDL_VERSION "${XSYSTEM35_DEFAULT_SDL_VERSION}" CACHE STRING
|
||||
"SDL major version (2 or 3)")
|
||||
set_property(CACHE XSYSTEM35_SDL_VERSION PROPERTY STRINGS 2 3)
|
||||
if (NOT XSYSTEM35_SDL_VERSION STREQUAL "2" AND
|
||||
NOT XSYSTEM35_SDL_VERSION STREQUAL "3")
|
||||
message(FATAL_ERROR "XSYSTEM35_SDL_VERSION must be 2 or 3")
|
||||
endif()
|
||||
add_library(xsystem35_sdl INTERFACE)
|
||||
target_compile_definitions(xsystem35_sdl INTERFACE
|
||||
XSYSTEM35_SDL_VERSION=${XSYSTEM35_SDL_VERSION})
|
||||
add_library(xsystem35_sdl_mixer INTERFACE)
|
||||
|
||||
include(CheckSymbolExists)
|
||||
include(FetchContent)
|
||||
@@ -55,6 +72,8 @@ check_symbol_exists(mmap "sys/mman.h" HAVE_MMAP)
|
||||
check_symbol_exists(sigaction "signal.h" HAVE_SIGACTION)
|
||||
|
||||
if (EMSCRIPTEN)
|
||||
option(JSPI "Use JavaScript Promise Integration instead of Asyncify" OFF)
|
||||
|
||||
function(add_emscripten_library name option)
|
||||
add_library(${name} INTERFACE)
|
||||
set_target_properties(${name} PROPERTIES
|
||||
@@ -62,16 +81,23 @@ if (EMSCRIPTEN)
|
||||
INTERFACE_LINK_OPTIONS ${option})
|
||||
endfunction()
|
||||
add_emscripten_library(zlib -sUSE_ZLIB=1)
|
||||
add_emscripten_library(sdl2 -sUSE_SDL=2)
|
||||
add_emscripten_library(sdl2_ttf -sUSE_SDL_TTF=2)
|
||||
target_compile_options(xsystem35_sdl INTERFACE
|
||||
-sUSE_SDL=${XSYSTEM35_SDL_VERSION})
|
||||
target_link_options(xsystem35_sdl INTERFACE
|
||||
-sUSE_SDL=${XSYSTEM35_SDL_VERSION})
|
||||
add_emscripten_library(freetype2 -sUSE_FREETYPE=1)
|
||||
set(DEFAULT_FONT_PATH /fonts/)
|
||||
fetch_webp()
|
||||
set(HAVE_WEBP 1)
|
||||
|
||||
elseif (ANDROID)
|
||||
add_library(sdl2 ALIAS SDL2)
|
||||
add_library(sdl2_ttf ALIAS SDL2_ttf)
|
||||
add_library(sdl2_mixer ALIAS SDL2_mixer)
|
||||
if (NOT XSYSTEM35_SDL_VERSION STREQUAL "3")
|
||||
message(FATAL_ERROR "Android supports SDL3 only")
|
||||
endif()
|
||||
target_link_libraries(xsystem35_sdl INTERFACE SDL3::SDL3)
|
||||
target_link_libraries(xsystem35_sdl_mixer INTERFACE
|
||||
SDL3_mixer::SDL3_mixer)
|
||||
add_library(freetype2 ALIAS freetype)
|
||||
find_library(ndk_log log)
|
||||
find_library(ndk_zlib z)
|
||||
add_library(zlib INTERFACE)
|
||||
@@ -87,25 +113,48 @@ else() # non-emscripten, non-android
|
||||
add_library(zlib ALIAS ZLIB::ZLIB)
|
||||
|
||||
include(FindPkgConfig)
|
||||
optional_pkg_check_modules(GTK3 IMPORTED_TARGET gtk+-3.0)
|
||||
if (GTK3_FOUND)
|
||||
set(ENABLE_GTK 1)
|
||||
endif()
|
||||
|
||||
pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2)
|
||||
pkg_check_modules(SDL2TTF REQUIRED IMPORTED_TARGET SDL2_ttf)
|
||||
pkg_check_modules(SDL2MIXER REQUIRED IMPORTED_TARGET SDL2_mixer)
|
||||
if (WIN32)
|
||||
add_static_library(sdl2 SDL2)
|
||||
add_static_library(sdl2_ttf SDL2TTF)
|
||||
# Workaround for linking error
|
||||
set_property(TARGET sdl2_ttf PROPERTY INTERFACE_LINK_LIBRARIES
|
||||
$<LINK_GROUP:RESCAN,${SDL2TTF_STATIC_LIBRARIES} -lstdc++>)
|
||||
add_static_library(sdl2_mixer SDL2MIXER)
|
||||
pkg_check_modules(FREETYPE REQUIRED IMPORTED_TARGET freetype2)
|
||||
if (XSYSTEM35_SDL_VERSION STREQUAL "2")
|
||||
pkg_check_modules(SDL2 REQUIRED IMPORTED_TARGET sdl2>=2.18.0)
|
||||
pkg_check_modules(SDL2MIXER REQUIRED IMPORTED_TARGET SDL2_mixer)
|
||||
if (WIN32)
|
||||
add_static_library(sdl2 SDL2)
|
||||
add_static_library(sdl2_mixer SDL2MIXER)
|
||||
target_link_libraries(xsystem35_sdl INTERFACE sdl2)
|
||||
target_link_libraries(xsystem35_sdl_mixer INTERFACE sdl2_mixer)
|
||||
else()
|
||||
target_link_libraries(xsystem35_sdl INTERFACE PkgConfig::SDL2)
|
||||
target_link_libraries(xsystem35_sdl_mixer INTERFACE
|
||||
PkgConfig::SDL2MIXER)
|
||||
endif()
|
||||
else()
|
||||
add_library(sdl2 ALIAS PkgConfig::SDL2)
|
||||
add_library(sdl2_ttf ALIAS PkgConfig::SDL2TTF)
|
||||
add_library(sdl2_mixer ALIAS PkgConfig::SDL2MIXER)
|
||||
find_package(SDL3 3.4 REQUIRED CONFIG)
|
||||
find_package(SDL3_mixer 3.2 QUIET CONFIG)
|
||||
if (NOT SDL3_mixer_FOUND)
|
||||
message(STATUS "SDL3_mixer not found; building SDL3_mixer 3.2.4 from source")
|
||||
FetchContent_Declare(
|
||||
SDL3_mixer
|
||||
URL https://github.com/libsdl-org/SDL_mixer/archive/refs/tags/release-3.2.4.tar.gz
|
||||
URL_HASH SHA256=f2ea848ccdf2f394cd4973ee0f6c482e04511044695cccfd46bab6dcd7f780aa
|
||||
)
|
||||
FetchContent_MakeAvailable(SDL3_mixer)
|
||||
endif()
|
||||
if (WIN32)
|
||||
target_link_libraries(xsystem35_sdl INTERFACE SDL3::SDL3-static)
|
||||
target_link_libraries(xsystem35_sdl_mixer INTERFACE
|
||||
SDL3_mixer::SDL3_mixer-static)
|
||||
else()
|
||||
target_link_libraries(xsystem35_sdl INTERFACE SDL3::SDL3)
|
||||
target_link_libraries(xsystem35_sdl_mixer INTERFACE
|
||||
SDL3_mixer::SDL3_mixer)
|
||||
endif()
|
||||
endif()
|
||||
if (WIN32)
|
||||
add_static_library(freetype2 FREETYPE)
|
||||
# harfbuzz and graphite2, which the static freetype depends on, are C++ libraries.
|
||||
target_link_libraries(freetype2 INTERFACE -lstdc++)
|
||||
else()
|
||||
add_library(freetype2 ALIAS PkgConfig::FREETYPE)
|
||||
set(DEFAULT_FONT_PATH ${CMAKE_INSTALL_PREFIX}/share/xsystem35/fonts/)
|
||||
endif()
|
||||
|
||||
@@ -153,14 +202,19 @@ else() # non-emscripten, non-android
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Menu
|
||||
# i18n support
|
||||
#
|
||||
# Translate via libintl if available; otherwise fall back to a built-in catalog
|
||||
# compiled from the .po files.
|
||||
#
|
||||
# To add a language, create po/<lang>.po and list <lang> here.
|
||||
set(NLS_LANGUAGES ja)
|
||||
|
||||
if (ENABLE_GTK)
|
||||
# i18n support (currently only menus are translated)
|
||||
if (NOT EMSCRIPTEN AND NOT ANDROID AND NOT WIN32)
|
||||
include(FindIntl)
|
||||
include(FindGettext)
|
||||
if (Intl_FOUND AND GETTEXT_FOUND)
|
||||
set(ENABLE_NLS 1)
|
||||
set(HAVE_LIBINTL 1)
|
||||
add_compile_definitions(LOCALEDIR="${CMAKE_INSTALL_PREFIX}/share/locale")
|
||||
include_directories(${Intl_INCLUDE_DIRS})
|
||||
link_libraries(${Intl_LIBRARIES})
|
||||
@@ -211,6 +265,7 @@ endfunction()
|
||||
|
||||
message("xsystem35 summary:")
|
||||
message("------------------")
|
||||
print_summary(SDL ${XSYSTEM35_SDL_VERSION})
|
||||
print_summary(audio ${SUMMARY_AUDIO})
|
||||
print_summary(cdrom ${SUMMARY_CDROM})
|
||||
print_summary(midi ${SUMMARY_MIDI})
|
||||
@@ -227,9 +282,11 @@ add_subdirectory(fonts)
|
||||
if (NOT ANDROID AND NOT EMSCRIPTEN)
|
||||
add_subdirectory(doc)
|
||||
endif()
|
||||
if (ENABLE_NLS)
|
||||
add_subdirectory(po)
|
||||
endif()
|
||||
add_subdirectory(po)
|
||||
|
||||
add_subdirectory(modules)
|
||||
target_link_libraries(xsystem35 PRIVATE modules)
|
||||
|
||||
if (NOT ANDROID AND NOT EMSCRIPTEN)
|
||||
add_subdirectory(test)
|
||||
endif()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# xsytem35-sdl2
|
||||
# xsystem35 for SDL2/SDL3
|
||||
|
||||
This is a multi-platform port of `xsystem35`, a free implementation of
|
||||
AliceSoft's System 3.x game engine.
|
||||
@@ -102,53 +102,97 @@ $ xsystem35
|
||||
See [xsystem35 command manual](doc/xsystem35.6.adoc) for detailed usage.
|
||||
|
||||
## Building
|
||||
|
||||
SDL2 is used by default on desktop platforms and Emscripten. Pass
|
||||
`-DXSYSTEM35_SDL_VERSION=3` to CMake to select SDL3. Use a separate build
|
||||
directory for each SDL version.
|
||||
|
||||
The supported configurations are:
|
||||
|
||||
| Platform | SDL2 | SDL3 |
|
||||
| --- | --- | --- |
|
||||
| Linux and macOS | Yes | Yes |
|
||||
| Windows 64-bit | Yes | Yes |
|
||||
| Windows 32-bit | Yes | No |
|
||||
| Emscripten | Yes | Yes |
|
||||
| Android | No | Yes |
|
||||
|
||||
SDL2 builds require SDL 2.18 or later and SDL2_mixer. SDL3 builds require
|
||||
SDL 3.4 or later and SDL3_mixer 3.2 or later. The remaining common
|
||||
dependencies are FreeType, zlib, and optionally libwebp, PortMidi, and cJSON.
|
||||
|
||||
### Linux (Debian / Ubuntu)
|
||||
|
||||
```bash
|
||||
$ sudo apt install build-essential cmake libgtk-3-dev libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-dev libwebp-dev libportmidi-dev libcjson-dev asciidoctor
|
||||
$ mkdir -p out/debug
|
||||
$ cd out/debug
|
||||
$ cmake -DCMAKE_BUILD_TYPE=Debug ../../
|
||||
$ make && make install
|
||||
$ sudo apt install build-essential cmake libsdl2-dev libsdl2-mixer-dev \
|
||||
libfreetype-dev libwebp-dev libportmidi-dev libcjson-dev asciidoctor
|
||||
$ cmake -S . -B out/sdl2 -DCMAKE_BUILD_TYPE=Debug \
|
||||
-DXSYSTEM35_SDL_VERSION=2
|
||||
$ cmake --build out/sdl2
|
||||
$ sudo cmake --install out/sdl2
|
||||
```
|
||||
|
||||
For SDL3 on a distribution that provides SDL 3.4 or later (for example,
|
||||
Ubuntu 26.04), install `libsdl3-dev` instead of the SDL2 packages and
|
||||
configure `out/sdl3` with `-DXSYSTEM35_SDL_VERSION=3`. If SDL3_mixer 3.2 or
|
||||
later is installed, CMake uses it; otherwise, CMake downloads and builds
|
||||
SDL3_mixer 3.2.4 during configuration.
|
||||
|
||||
### MacOS
|
||||
|
||||
[Homebrew](https://brew.sh/) is required.
|
||||
|
||||
```bash
|
||||
$ brew install cmake pkg-config sdl2 sdl2_mixer sdl2_ttf webp portmidi cjson asciidoctor
|
||||
$ mkdir -p out/debug
|
||||
$ cd out/debug
|
||||
$ cmake -DCMAKE_BUILD_TYPE=Debug ../../
|
||||
$ make && make install
|
||||
$ brew install cmake pkg-config sdl2 sdl2_mixer freetype webp portmidi \
|
||||
cjson asciidoctor
|
||||
$ cmake -S . -B out/sdl2 -DCMAKE_BUILD_TYPE=Debug \
|
||||
-DXSYSTEM35_SDL_VERSION=2
|
||||
$ cmake --build out/sdl2
|
||||
$ sudo cmake --install out/sdl2
|
||||
```
|
||||
|
||||
For SDL3, install `sdl3 sdl3_mixer` instead of `sdl2 sdl2_mixer` and
|
||||
configure `out/sdl3` with `-DXSYSTEM35_SDL_VERSION=3`.
|
||||
|
||||
### Windows
|
||||
|
||||
[MSYS2](https://www.msys2.org) is required.
|
||||
|
||||
```bash
|
||||
$ pacman -S cmake mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-SDL2 mingw-w64-ucrt-x86_64-SDL2_ttf mingw-w64-ucrt-x86_64-SDL2_mixer mingw-w64-ucrt-x86_64-libwebp mingw-w64-ucrt-x86_64-portmidi mingw-w64-ucrt-x86_64-cjson
|
||||
$ mkdir -p out/debug
|
||||
$ cd out/debug
|
||||
$ cmake -G"MSYS Makefiles" -DCMAKE_BUILD_TYPE=Debug ../../
|
||||
$ make
|
||||
$ pacman -S mingw-w64-ucrt-x86_64-gcc \
|
||||
mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-SDL2 \
|
||||
mingw-w64-ucrt-x86_64-SDL2_mixer mingw-w64-ucrt-x86_64-freetype \
|
||||
mingw-w64-ucrt-x86_64-libwebp mingw-w64-ucrt-x86_64-portmidi \
|
||||
mingw-w64-ucrt-x86_64-cjson
|
||||
$ cmake -S . -B out/sdl2 -G"MSYS Makefiles" \
|
||||
-DCMAKE_BUILD_TYPE=Debug -DXSYSTEM35_SDL_VERSION=2
|
||||
$ cmake --build out/sdl2
|
||||
```
|
||||
|
||||
For a 64-bit SDL3 build, install `mingw-w64-ucrt-x86_64-sdl3` and
|
||||
`mingw-w64-ucrt-x86_64-sdl3-mixer` instead of the SDL2 packages, then
|
||||
configure `out/sdl3` with `-DXSYSTEM35_SDL_VERSION=3`. The MSYS2 mingw32
|
||||
repository does not provide SDL3, so 32-bit Windows builds remain on SDL2.
|
||||
|
||||
### Emscripten
|
||||
|
||||
```bash
|
||||
$ mkdir -p out/wasm
|
||||
$ cd out/wasm
|
||||
$ emcmake cmake -DCMAKE_BUILD_TYPE=MinSizeRel ../../
|
||||
$ make
|
||||
$ emcmake cmake -S . -B out/wasm-sdl2 -DCMAKE_BUILD_TYPE=MinSizeRel \
|
||||
-DXSYSTEM35_SDL_VERSION=2
|
||||
$ cmake --build out/wasm-sdl2
|
||||
```
|
||||
|
||||
For SDL3, use a different build directory and
|
||||
`-DXSYSTEM35_SDL_VERSION=3`. Both versions use Emscripten's bundled SDL port;
|
||||
SDL_mixer is not required.
|
||||
|
||||
To use the generated binary, check out
|
||||
[Kichikuou on Web](https://github.com/kichikuou/web) and copy `out/xsystem35.*`
|
||||
into its `docs` directory.
|
||||
[Kichikuou on Web](https://github.com/kichikuou/web) and copy
|
||||
`out/wasm-sdl2/src/xsystem35.*` into its `docs` directory.
|
||||
|
||||
### Android
|
||||
|
||||
See [android/README.md](android/).
|
||||
|
||||
Android uses SDL3 exclusively. The Gradle build downloads fixed official SDL3
|
||||
3.4.16 and SDL3_mixer 3.2.4 AARs and exposes them to CMake through Prefab.
|
||||
|
||||
+27
-2
@@ -16,13 +16,31 @@ Alternatively, you can install `xsystem35-sdl2` using [Obtainium](https://github
|
||||
|
||||
## Usage
|
||||
### Basic Usage
|
||||
You can install games either from a prepared ZIP file or directly from a CD-ROM
|
||||
image.
|
||||
|
||||
#### Installing from a ZIP
|
||||
1. Create a ZIP file containing all the game files and BGM files (see
|
||||
[below](#preparing-a-zip) for details), and transfer it to your device.
|
||||
2. Open the app. Tap the option menu (three dots in the top right corner) and
|
||||
select "Install from ZIP".
|
||||
3. Select the ZIP file you created in step 1.
|
||||
4. The game will start. To simulate a right-click, tap the black bars on either
|
||||
the left or right, or top or bottom of the screen.
|
||||
4. The game will start.
|
||||
|
||||
#### Installing from a CD image
|
||||
1. Transfer the CD image file(s) to your device.
|
||||
2. Open the app. Tap the option menu and select "Install from CD image".
|
||||
3. Select the image file and, when needed, the matching metadata file at the
|
||||
same time. The supported combinations are:
|
||||
- `.iso`
|
||||
- `.bin` or `.img` with matching `.cue`
|
||||
- `.bin` or `.img` with matching `.ccd`
|
||||
- `.mdf` with matching `.mds`
|
||||
|
||||
In many Android file pickers, tapping a file immediately selects only that
|
||||
one file. Long-press a file first to enter multi-select mode, then select
|
||||
both the image file and its metadata file.
|
||||
4. The game will start.
|
||||
|
||||
### Preparing a ZIP
|
||||
- Include all files from the `GAMEDATA` folder (such as `.ALD` files and
|
||||
@@ -41,6 +59,8 @@ Note: This ZIP format is also compatible with
|
||||
[Kichikuou on Web](http://kichikuou.github.io/web/).
|
||||
|
||||
### Miscellaneous
|
||||
- To simulate a right-click, tap the black bars on either the left or right,
|
||||
or top or bottom of the screen.
|
||||
- You can export or import save files via the game list's option menu.
|
||||
- To uninstall a game, long-tap its title in the game list.
|
||||
|
||||
@@ -53,6 +73,11 @@ Note: This ZIP format is also compatible with
|
||||
|
||||
## Building from Source
|
||||
|
||||
Android builds use SDL3 exclusively. Gradle downloads the fixed official SDL3
|
||||
3.4.16 and SDL3_mixer 3.2.4 AARs, verifies their checksums, and supplies their
|
||||
native libraries to CMake through Prefab. No separate SDL installation is
|
||||
required.
|
||||
|
||||
### Using Android Studio
|
||||
Open this directory as an Android Studio project.
|
||||
|
||||
|
||||
@@ -8,19 +8,89 @@ else {
|
||||
}
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
def aarsDir = file('aars')
|
||||
def assetsLicensesDir = file('src/main/assets/licenses')
|
||||
|
||||
def sha256 = { File file ->
|
||||
def digest = java.security.MessageDigest.getInstance('SHA-256')
|
||||
file.withInputStream { input ->
|
||||
byte[] buffer = new byte[8192]
|
||||
for (int n = input.read(buffer); n >= 0; n = input.read(buffer)) {
|
||||
digest.update(buffer, 0, n)
|
||||
}
|
||||
}
|
||||
digest.digest().encodeHex().toString()
|
||||
}
|
||||
|
||||
def installAar = { String url, String archiveSha256, String aarName,
|
||||
String aarSha256, String licenseName ->
|
||||
def aarFile = new File(aarsDir, aarName)
|
||||
def licenseFile = new File(assetsLicensesDir, licenseName)
|
||||
if (!aarFile.exists() || sha256(aarFile) != aarSha256 || !licenseFile.exists()) {
|
||||
logger.lifecycle("Downloading ${aarName} ...")
|
||||
def archive = File.createTempFile('sdl-android', '.zip')
|
||||
try {
|
||||
new URL(url).withInputStream { input ->
|
||||
archive.withOutputStream { it << input }
|
||||
}
|
||||
if (sha256(archive) != archiveSha256) {
|
||||
throw new GradleException("Checksum mismatch for ${url}")
|
||||
}
|
||||
aarsDir.mkdirs()
|
||||
assetsLicensesDir.mkdirs()
|
||||
copy {
|
||||
from zipTree(archive)
|
||||
include aarName
|
||||
into aarsDir
|
||||
}
|
||||
copy {
|
||||
from zipTree(archive)
|
||||
include 'LICENSE.txt'
|
||||
into assetsLicensesDir
|
||||
rename 'LICENSE.txt', licenseName
|
||||
}
|
||||
} finally {
|
||||
archive.delete()
|
||||
}
|
||||
}
|
||||
if (!aarFile.exists() || sha256(aarFile) != aarSha256) {
|
||||
throw new GradleException("Checksum mismatch for ${aarFile}")
|
||||
}
|
||||
aarFile
|
||||
}
|
||||
|
||||
def sdlVersion = '3.4.16'
|
||||
def sdlAarFile = installAar(
|
||||
"https://github.com/libsdl-org/SDL/releases/download/release-${sdlVersion}/SDL3-devel-${sdlVersion}-android.zip",
|
||||
'e1da8d9298471ea4ab8c6d48a804c0ac5415f35f30fcf7eba61aa7357e165765',
|
||||
"SDL3-${sdlVersion}.aar",
|
||||
'03710fc7b49cc070551446841a843840fabf2aaaaa3043cd5739292a55e4e61c',
|
||||
'SDL')
|
||||
|
||||
def sdlMixerVersion = '3.2.4'
|
||||
def sdlMixerAarFile = installAar(
|
||||
"https://github.com/libsdl-org/SDL_mixer/releases/download/release-${sdlMixerVersion}/SDL3_mixer-devel-${sdlMixerVersion}-android.zip",
|
||||
'b7f9f1ee5162a0492e71941f2cbdc50ec256ef15c0e995ca72c7e519eebd810d',
|
||||
"SDL3_mixer-${sdlMixerVersion}.aar",
|
||||
'd13dc45d4a4d6a2db4132400e77629c466ed533641fc9e211dfe85fab025b792',
|
||||
'SDL_mixer')
|
||||
|
||||
android {
|
||||
if (buildAsApplication) {
|
||||
namespace "io.github.kichikuou.xsystem35"
|
||||
}
|
||||
buildFeatures {
|
||||
prefab true
|
||||
}
|
||||
compileSdkVersion 34
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 34
|
||||
versionCode 36
|
||||
versionName "2.18.0" + gitRevision()
|
||||
versionName "2.20.0" + gitRevision()
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DANDROID_APP_PLATFORM=android-19", "-DANDROID_STL=c++_static"
|
||||
arguments "-DANDROID_STL=c++_static", "-DXSYSTEM35_SDL_VERSION=3"
|
||||
// abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
|
||||
abiFilters 'armeabi-v7a', 'arm64-v8a'
|
||||
}
|
||||
@@ -83,8 +153,10 @@ android {
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||
implementation files(sdlAarFile, sdlMixerAarFile)
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
|
||||
testImplementation 'junit:junit:4.13.2'
|
||||
}
|
||||
|
||||
static String gitRevision() {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
include $(call all-subdir-makefiles)
|
||||
@@ -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
|
||||
@@ -3,37 +3,25 @@ cmake_minimum_required(VERSION 3.14)
|
||||
project(GAME)
|
||||
set(PROJECT_ROOT_DIR ../../..)
|
||||
|
||||
# Compilation of SDL and companion libraries
|
||||
# SDL3 and SDL3_mixer are provided by their official Android AARs through Prefab.
|
||||
find_package(SDL3 3.4 REQUIRED CONFIG)
|
||||
find_package(SDL3_mixer 3.2.4 REQUIRED CONFIG)
|
||||
|
||||
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
|
||||
)
|
||||
FetchContent_Declare(
|
||||
SDL_mixer
|
||||
URL https://github.com/libsdl-org/SDL_mixer/releases/download/release-2.8.0/SDL2_mixer-2.8.0.tar.gz
|
||||
URL_HASH SHA1=a58c69f9d00e44833b9e00e1adb58d85759ca499
|
||||
freetype
|
||||
URL https://downloads.sourceforge.net/project/freetype/freetype2/2.14.3/freetype-2.14.3.tar.gz
|
||||
https://download.savannah.gnu.org/releases/freetype/freetype-2.14.3.tar.gz
|
||||
URL_HASH SHA1=a911f65c6355ddcf7aaba46e250dbc00f67e1678
|
||||
)
|
||||
# Only FreeType's own rasterizer is needed, none of its optional dependencies.
|
||||
set(FT_DISABLE_ZLIB ON CACHE BOOL "Disable use of system zlib" FORCE)
|
||||
set(FT_DISABLE_BZIP2 ON CACHE BOOL "Disable use of system libbz2" FORCE)
|
||||
set(FT_DISABLE_PNG ON CACHE BOOL "Disable use of system libpng" FORCE)
|
||||
set(FT_DISABLE_HARFBUZZ ON CACHE BOOL "Disable use of harfbuzz" FORCE)
|
||||
set(FT_DISABLE_BROTLI ON CACHE BOOL "Disable use of brotli" FORCE)
|
||||
|
||||
set(SDL2TTF_SAMPLES OFF CACHE BOOL "Build the SDL2_ttf sample program(s)" FORCE)
|
||||
set(SDL2TTF_INSTALL OFF CACHE BOOL "Enable SDL2_ttf install target" FORCE)
|
||||
set(SDL2TTF_VENDORED ON CACHE BOOL "Use vendored third-party libraries" FORCE)
|
||||
|
||||
set(SDL2MIXER_OPUS OFF CACHE BOOL "Enable Opus music" FORCE)
|
||||
set(SDL2MIXER_FLAC OFF CACHE BOOL "Enable FLAC music" FORCE)
|
||||
set(SDL2MIXER_MOD OFF CACHE BOOL "Support loading MOD music" FORCE)
|
||||
set(SDL2MIXER_MIDI OFF CACHE BOOL "Enable MIDI music" FORCE)
|
||||
set(SDL2MIXER_WAVPACK OFF CACHE BOOL "Enable WavPack music" FORCE)
|
||||
set(SDL2MIXER_SAMPLES OFF CACHE BOOL "Build the SDL2_mixer sample program(s)" FORCE)
|
||||
set(SDL2MIXER_INSTALL OFF CACHE BOOL "Enable SDL2_mixer install target" FORCE)
|
||||
|
||||
FetchContent_MakeAvailable(SDL SDL_ttf SDL_mixer)
|
||||
FetchContent_MakeAvailable(freetype)
|
||||
|
||||
# The main CMakeLists.txt of xsystem35
|
||||
add_subdirectory(${PROJECT_ROOT_DIR} xsystem35)
|
||||
@@ -47,8 +35,5 @@ file(COPY_FILE ${PROJECT_ROOT_DIR}/COPYING ${ASSETS_DIR}/licenses/xsystem35)
|
||||
file(COPY_FILE ${PROJECT_ROOT_DIR}/licenses/MTLc3m.txt ${ASSETS_DIR}/licenses/MTLc3m)
|
||||
file(COPY_FILE ${PROJECT_ROOT_DIR}/licenses/mincho.txt ${ASSETS_DIR}/licenses/mincho)
|
||||
file(COPY_FILE ${PROJECT_ROOT_DIR}/licenses/nanojpeg.txt ${ASSETS_DIR}/licenses/nanojpeg)
|
||||
file(COPY_FILE ${sdl_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL)
|
||||
file(COPY_FILE ${sdl_ttf_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL_ttf)
|
||||
file(COPY_FILE ${sdl_ttf_SOURCE_DIR}/external/freetype/docs/GPLv2.TXT ${ASSETS_DIR}/licenses/freetype)
|
||||
file(COPY_FILE ${sdl_ttf_SOURCE_DIR}/external/harfbuzz/COPYING ${ASSETS_DIR}/licenses/harfbuzz)
|
||||
file(COPY_FILE ${sdl_mixer_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL_mixer)
|
||||
file(COPY_FILE ${PROJECT_ROOT_DIR}/licenses/microui.txt ${ASSETS_DIR}/licenses/microui)
|
||||
file(COPY_FILE ${freetype_SOURCE_DIR}/docs/GPLv2.TXT ${ASSETS_DIR}/licenses/freetype)
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
<!-- Example of setting SDL hints from AndroidManifest.xml:
|
||||
<meta-data android:name="SDL_ENV.SDL_ACCELEROMETER_AS_JOYSTICK" android:value="0"/>
|
||||
-->
|
||||
<meta-data android:name="SDL_ENV.SDL_IOS_ORIENTATIONS" android:value="LandscapeLeft LandscapeRight"/>
|
||||
|
||||
<activity android:name=".LauncherActivity"
|
||||
android:exported="true">
|
||||
@@ -78,6 +79,7 @@
|
||||
|
||||
<activity android:name=".GameActivity"
|
||||
android:label="@string/app_name"
|
||||
android:process=":game"
|
||||
android:alwaysRetainTaskState="true"
|
||||
android:configChanges="layoutDirection|locale|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
|
||||
android:theme="@style/AppTheme"
|
||||
|
||||
@@ -0,0 +1,566 @@
|
||||
/* Copyright (C) 2026 <KichikuouChrome@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
*/
|
||||
package io.github.kichikuou.xsystem35
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.os.ParcelFileDescriptor
|
||||
import java.io.Closeable
|
||||
import java.io.EOFException
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.channels.FileChannel
|
||||
import java.util.Locale
|
||||
|
||||
internal class CdImageReader private constructor(
|
||||
private val image: RandomAccessImage,
|
||||
private val tracks: List<TrackInfo?>,
|
||||
) : Closeable {
|
||||
fun readSector(sector: Int, buffer: ByteArray) {
|
||||
readDataFully(sector.toLong() * ISO_SECTOR_SIZE, buffer, 0, ISO_SECTOR_SIZE)
|
||||
}
|
||||
|
||||
fun readDataFully(offset: Long, buffer: ByteArray, bufferOffset: Int, length: Int) {
|
||||
// ISO9660 always sees logical 2048-byte data sectors even when the
|
||||
// image stores each sector as a raw 2352-byte CD-ROM block.
|
||||
var logicalOffset = offset
|
||||
var outOffset = bufferOffset
|
||||
var remaining = length
|
||||
val sectorBuffer = ByteArray(ISO_SECTOR_SIZE)
|
||||
while (remaining > 0) {
|
||||
val sector = (logicalOffset / ISO_SECTOR_SIZE).toInt()
|
||||
val offsetInSector = (logicalOffset % ISO_SECTOR_SIZE).toInt()
|
||||
val count = minOf(remaining, ISO_SECTOR_SIZE - offsetInSector)
|
||||
readSectorPayload(sector, sectorBuffer)
|
||||
sectorBuffer.copyInto(buffer, outOffset, offsetInSector, offsetInSector + count)
|
||||
logicalOffset += count
|
||||
outOffset += count
|
||||
remaining -= count
|
||||
}
|
||||
}
|
||||
|
||||
fun audioTracks(): List<Int> {
|
||||
return tracks.mapIndexedNotNull { trackNumber, track ->
|
||||
if (track?.isAudio == true) trackNumber else null
|
||||
}
|
||||
}
|
||||
|
||||
fun extractAudioTrack(trackNumber: Int, output: OutputStream) {
|
||||
val track = tracks.getOrNull(trackNumber)
|
||||
if (track?.isAudio != true) {
|
||||
throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
val dataSize = track.numSectors.toLong() * RAW_AUDIO_SECTOR_SIZE
|
||||
// CD-DA sectors are already 44.1kHz stereo 16-bit PCM; wrapping the
|
||||
// raw sector stream in a WAV header makes it playable by SDL_mixer.
|
||||
writeWaveHeader(output, dataSize)
|
||||
val buffer = ByteArray(track.blockSize * 16)
|
||||
var sector = 0
|
||||
while (sector < track.numSectors) {
|
||||
val sectorsToRead = minOf(16, track.numSectors - sector)
|
||||
val bytesToRead = sectorsToRead * track.blockSize
|
||||
image.readFully(track.offset + sector.toLong() * track.blockSize, buffer, 0, bytesToRead)
|
||||
output.write(buffer, 0, sectorsToRead * RAW_AUDIO_SECTOR_SIZE)
|
||||
sector += sectorsToRead
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
image.close()
|
||||
}
|
||||
|
||||
private fun readSectorPayload(sector: Int, buffer: ByteArray) {
|
||||
val track = findDataTrack(sector)
|
||||
val offset = track.offset + (sector - track.startSector).toLong() * track.blockSize + track.blockOffset
|
||||
image.readFully(offset, buffer, 0, ISO_SECTOR_SIZE)
|
||||
}
|
||||
|
||||
private fun findDataTrack(sector: Int): TrackInfo {
|
||||
for (track in tracks) {
|
||||
if (track == null || track.isAudio) {
|
||||
continue
|
||||
}
|
||||
if (sector >= track.startSector && sector < track.startSector + track.numSectors) {
|
||||
return track
|
||||
}
|
||||
}
|
||||
throw InstallFailureException(R.string.invalid_iso9660_image)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ISO_SECTOR_SIZE = 2048
|
||||
private const val RAW_AUDIO_SECTOR_SIZE = 2352
|
||||
private const val WAVE_HEADER_SIZE = 44
|
||||
private const val MDS_MODE_AUDIO = 0xa9
|
||||
private const val MDS_MODE_MODE1 = 0xaa
|
||||
|
||||
fun open(
|
||||
contentResolver: ContentResolver,
|
||||
files: List<SelectedInstallFile>,
|
||||
tempDir: File,
|
||||
): CdImageReader {
|
||||
val selected = selectImageFiles(files)
|
||||
val image = RandomAccessImage.open(contentResolver, selected.image, tempDir)
|
||||
try {
|
||||
if (selected.metadata == null) {
|
||||
return CdImageReader(image, listOf(null, TrackInfo.iso(image.size)))
|
||||
}
|
||||
val metadataName = selected.metadata.displayName.lowercase(Locale.US)
|
||||
val tracks = when {
|
||||
metadataName.endsWith(".cue") -> {
|
||||
val cueText = readMetadataText(contentResolver, selected.metadata)
|
||||
parseCue(cueText, image.size)
|
||||
}
|
||||
metadataName.endsWith(".ccd") -> {
|
||||
val ccdText = readMetadataText(contentResolver, selected.metadata)
|
||||
parseCcd(ccdText, image.size)
|
||||
}
|
||||
metadataName.endsWith(".mds") -> {
|
||||
val mdsBytes = readMetadataBytes(contentResolver, selected.metadata)
|
||||
parseMds(mdsBytes)
|
||||
}
|
||||
else -> throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
return CdImageReader(image, tracks)
|
||||
} catch (e: Exception) {
|
||||
image.close()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
internal fun openForFile(
|
||||
imageFile: File,
|
||||
metadataName: String? = null,
|
||||
metadataBytes: ByteArray? = null,
|
||||
): CdImageReader {
|
||||
val image = RandomAccessImage.open(imageFile)
|
||||
try {
|
||||
if (metadataName == null) {
|
||||
return CdImageReader(image, listOf(null, TrackInfo.iso(image.size)))
|
||||
}
|
||||
val tracks = when {
|
||||
metadataName.lowercase(Locale.US).endsWith(".cue") -> {
|
||||
parseCue(metadataBytes?.toString(Charsets.UTF_8) ?: "", image.size)
|
||||
}
|
||||
metadataName.lowercase(Locale.US).endsWith(".ccd") -> {
|
||||
parseCcd(metadataBytes?.toString(Charsets.UTF_8) ?: "", image.size)
|
||||
}
|
||||
metadataName.lowercase(Locale.US).endsWith(".mds") -> {
|
||||
parseMds(metadataBytes ?: ByteArray(0))
|
||||
}
|
||||
else -> throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
return CdImageReader(image, tracks)
|
||||
} catch (e: Exception) {
|
||||
image.close()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectImageFiles(files: List<SelectedInstallFile>): SelectedImageFiles {
|
||||
if (files.isEmpty()) {
|
||||
throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
val isoFiles = files.filter { it.displayName.lowercase(Locale.US).endsWith(".iso") }
|
||||
if (isoFiles.size == 1 && files.size == 1) {
|
||||
return SelectedImageFiles(isoFiles.single(), null)
|
||||
}
|
||||
|
||||
val imageFiles = files.filter {
|
||||
val name = it.displayName.lowercase(Locale.US)
|
||||
name.endsWith(".bin") || name.endsWith(".img") || name.endsWith(".mdf")
|
||||
}
|
||||
val metadataFiles = files.filter {
|
||||
val name = it.displayName.lowercase(Locale.US)
|
||||
name.endsWith(".cue") || name.endsWith(".ccd") || name.endsWith(".mds")
|
||||
}
|
||||
if (imageFiles.size == 1 && metadataFiles.isEmpty() && files.size == 1 ||
|
||||
imageFiles.isEmpty() && metadataFiles.size == 1 && files.size == 1
|
||||
) {
|
||||
throw InstallFailureException(R.string.missing_cd_image_metadata)
|
||||
}
|
||||
if (imageFiles.size != 1 || metadataFiles.size != 1 || files.size != 2) {
|
||||
throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
val imageBase = imageFiles.single().baseName()
|
||||
val metadataBase = metadataFiles.single().baseName()
|
||||
if (imageBase != metadataBase) {
|
||||
throw InstallFailureException(R.string.missing_cd_image_metadata)
|
||||
}
|
||||
if (!isSupportedMetadataForImage(imageFiles.single(), metadataFiles.single())) {
|
||||
throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
return SelectedImageFiles(imageFiles.single(), metadataFiles.single())
|
||||
}
|
||||
|
||||
private fun isSupportedMetadataForImage(
|
||||
image: SelectedInstallFile,
|
||||
metadata: SelectedInstallFile,
|
||||
): Boolean {
|
||||
val imageName = image.displayName.lowercase(Locale.US)
|
||||
val metadataName = metadata.displayName.lowercase(Locale.US)
|
||||
return if (imageName.endsWith(".mdf")) {
|
||||
metadataName.endsWith(".mds")
|
||||
} else {
|
||||
metadataName.endsWith(".cue") || metadataName.endsWith(".ccd")
|
||||
}
|
||||
}
|
||||
|
||||
private fun readMetadataText(
|
||||
contentResolver: ContentResolver,
|
||||
file: SelectedInstallFile,
|
||||
): String {
|
||||
return contentResolver.openInputStream(file.uri)?.bufferedReader()?.use {
|
||||
it.readText()
|
||||
} ?: throw InstallFailureException(R.string.cd_image_read_error)
|
||||
}
|
||||
|
||||
private fun readMetadataBytes(
|
||||
contentResolver: ContentResolver,
|
||||
file: SelectedInstallFile,
|
||||
): ByteArray {
|
||||
return contentResolver.openInputStream(file.uri)?.use {
|
||||
it.readBytes()
|
||||
} ?: throw InstallFailureException(R.string.cd_image_read_error)
|
||||
}
|
||||
|
||||
private fun parseCue(cueText: String, imageSize: Long): List<TrackInfo?> {
|
||||
val cueTracks = mutableListOf<CueTrack?>()
|
||||
var currentTrack: Int? = null
|
||||
for (line in cueText.lines()) {
|
||||
val fields = line.trim().split(Regex("\\s+"))
|
||||
if (fields.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
when (fields[0].uppercase(Locale.US)) {
|
||||
"TRACK" -> {
|
||||
if (fields.size < 3) {
|
||||
throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
currentTrack = fields[1].toIntOrNull()
|
||||
?: throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
while (cueTracks.size <= currentTrack) {
|
||||
cueTracks.add(null)
|
||||
}
|
||||
cueTracks[currentTrack] = when (fields[2].uppercase(Locale.US)) {
|
||||
"MODE1/2048" -> CueTrack(false, 2048, 0)
|
||||
"MODE1/2352" -> CueTrack(false, 2352, 16)
|
||||
"AUDIO" -> CueTrack(true, 2352, 0)
|
||||
else -> throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
}
|
||||
"INDEX" -> {
|
||||
val trackNumber = currentTrack ?: continue
|
||||
if (fields.size < 3) {
|
||||
throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
val indexNumber = fields[1].toIntOrNull()
|
||||
?: throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
cueTracks[trackNumber]?.index?.put(indexNumber, indexToSector(fields[2]))
|
||||
}
|
||||
}
|
||||
}
|
||||
return makeTrackInfo(cueTracks, imageSize)
|
||||
}
|
||||
|
||||
private fun parseCcd(ccdText: String, imageSize: Long): List<TrackInfo?> {
|
||||
val cueTracks = mutableListOf<CueTrack?>()
|
||||
var currentTrack: Int? = null
|
||||
for (line in ccdText.lines()) {
|
||||
val trimmed = line.trim()
|
||||
val trackMatch = Regex("""\[TRACK ([0-9]+)]""").matchEntire(trimmed)
|
||||
if (trackMatch != null) {
|
||||
currentTrack = trackMatch.groupValues[1].toInt()
|
||||
while (cueTracks.size <= currentTrack) {
|
||||
cueTracks.add(null)
|
||||
}
|
||||
cueTracks[currentTrack] = CueTrack(false, 2352, 16)
|
||||
continue
|
||||
}
|
||||
val trackNumber = currentTrack ?: continue
|
||||
val keyValue = trimmed.split("=", limit = 2)
|
||||
if (keyValue.size != 2) {
|
||||
continue
|
||||
}
|
||||
when (keyValue[0].uppercase(Locale.US)) {
|
||||
"MODE" -> if (keyValue[1] == "0") {
|
||||
cueTracks[trackNumber] = CueTrack(true, 2352, 0, cueTracks[trackNumber]?.index ?: mutableMapOf())
|
||||
}
|
||||
"INDEX 0" -> cueTracks[trackNumber]?.index?.put(
|
||||
0,
|
||||
keyValue[1].toIntOrNull() ?: throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
)
|
||||
"INDEX 1" -> cueTracks[trackNumber]?.index?.put(
|
||||
1,
|
||||
keyValue[1].toIntOrNull() ?: throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
)
|
||||
}
|
||||
}
|
||||
return makeTrackInfo(cueTracks, imageSize)
|
||||
}
|
||||
|
||||
private fun parseMds(mdsBytes: ByteArray): List<TrackInfo?> {
|
||||
if (mdsBytes.size < 0x70 ||
|
||||
String(mdsBytes, 0, 16, Charsets.US_ASCII).trimEnd('\u0000') != "MEDIA DESCRIPTOR"
|
||||
) {
|
||||
throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
val entries = mdsBytes[0x62].toInt() and 0xff
|
||||
if (0x70 + entries * 0x58 > mdsBytes.size) {
|
||||
throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
|
||||
val tracks = MutableList<TrackInfo?>(100) { null }
|
||||
for (i in 0 until entries) {
|
||||
val trackOffset = 0x70 + i * 0x50
|
||||
val extraOffset = 0x70 + entries * 0x50 + i * 8
|
||||
val mode = mdsBytes[trackOffset].toInt() and 0xff
|
||||
val trackNumber = mdsBytes[trackOffset + 0x04].toInt() and 0xff
|
||||
val sectorSize = readLittleEndianShort(mdsBytes, trackOffset + 0x10)
|
||||
// MDS offsets are treated as 32-bit file offsets, so images
|
||||
// beyond 4GB are unsupported.
|
||||
val imageOffset = readLittleEndianInt(mdsBytes, trackOffset + 0x28)
|
||||
val sectors = readLittleEndianInt(mdsBytes, extraOffset + 0x04).toInt()
|
||||
if (trackNumber >= tracks.size) {
|
||||
continue
|
||||
}
|
||||
tracks[trackNumber] = when (mode) {
|
||||
MDS_MODE_AUDIO -> TrackInfo(
|
||||
isAudio = true,
|
||||
offset = imageOffset,
|
||||
blockSize = sectorSize,
|
||||
blockOffset = 0,
|
||||
startSector = 0,
|
||||
numSectors = sectors,
|
||||
)
|
||||
MDS_MODE_MODE1 -> TrackInfo(
|
||||
isAudio = false,
|
||||
offset = imageOffset,
|
||||
blockSize = sectorSize,
|
||||
blockOffset = 16,
|
||||
startSector = 0,
|
||||
numSectors = sectors,
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
if (tracks.getOrNull(1)?.isAudio != false) {
|
||||
throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
return tracks
|
||||
}
|
||||
|
||||
private fun makeTrackInfo(cueTracks: List<CueTrack?>, imageSize: Long): List<TrackInfo?> {
|
||||
val tracks = MutableList<TrackInfo?>(cueTracks.size) { null }
|
||||
var offset = 0L
|
||||
var startSector = 0
|
||||
for (trackNumber in 1 until cueTracks.size) {
|
||||
val cueTrack = cueTracks[trackNumber] ?: continue
|
||||
val index1 = cueTrack.index[1]
|
||||
?: throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
// INDEX 0 describes a pregap, but a zero INDEX 0 is treated as absent.
|
||||
cueTrack.index[0]?.takeIf { it != 0 }?.let { index0 ->
|
||||
val gap = index1 - index0
|
||||
if (gap > 0) {
|
||||
startSector += gap
|
||||
offset += gap.toLong() * cueTrack.blockSize
|
||||
}
|
||||
}
|
||||
val nextTrack = cueTracks.drop(trackNumber + 1).firstOrNull { it != null }
|
||||
val nextStart = nextTrack?.let { it.index[0]?.takeIf { index -> index != 0 } ?: it.index[1] }
|
||||
val numSectors = if (nextStart != null) {
|
||||
nextStart - index1
|
||||
} else {
|
||||
((imageSize - offset) / cueTrack.blockSize).toInt()
|
||||
}
|
||||
if (numSectors <= 0) {
|
||||
throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
tracks[trackNumber] = TrackInfo(
|
||||
isAudio = cueTrack.isAudio,
|
||||
offset = offset,
|
||||
blockSize = cueTrack.blockSize,
|
||||
blockOffset = cueTrack.blockOffset,
|
||||
startSector = startSector,
|
||||
numSectors = numSectors,
|
||||
)
|
||||
startSector += numSectors
|
||||
offset += numSectors.toLong() * cueTrack.blockSize
|
||||
}
|
||||
return tracks
|
||||
}
|
||||
|
||||
private fun indexToSector(index: String): Int {
|
||||
val parts = index.split(":").map {
|
||||
it.toIntOrNull() ?: throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
if (parts.size != 3) {
|
||||
throw InstallFailureException(R.string.unsupported_cd_image)
|
||||
}
|
||||
return parts[0] * 60 * 75 + parts[1] * 75 + parts[2]
|
||||
}
|
||||
|
||||
private fun writeWaveHeader(output: OutputStream, dataSize: Long) {
|
||||
val header = ByteArray(WAVE_HEADER_SIZE)
|
||||
writeAscii(header, 0, "RIFF")
|
||||
writeLittleEndianInt(header, 4, dataSize + 36)
|
||||
writeAscii(header, 8, "WAVE")
|
||||
writeAscii(header, 12, "fmt ")
|
||||
writeLittleEndianInt(header, 16, 16)
|
||||
writeLittleEndianShort(header, 20, 1)
|
||||
writeLittleEndianShort(header, 22, 2)
|
||||
writeLittleEndianInt(header, 24, 44100)
|
||||
writeLittleEndianInt(header, 28, 44100 * 2 * 2)
|
||||
writeLittleEndianShort(header, 32, 2 * 2)
|
||||
writeLittleEndianShort(header, 34, 16)
|
||||
writeAscii(header, 36, "data")
|
||||
writeLittleEndianInt(header, 40, dataSize)
|
||||
output.write(header)
|
||||
}
|
||||
|
||||
private fun writeAscii(buffer: ByteArray, offset: Int, value: String) {
|
||||
value.toByteArray(Charsets.US_ASCII).copyInto(buffer, offset)
|
||||
}
|
||||
|
||||
private fun writeLittleEndianShort(buffer: ByteArray, offset: Int, value: Int) {
|
||||
buffer[offset] = value.toByte()
|
||||
buffer[offset + 1] = (value shr 8).toByte()
|
||||
}
|
||||
|
||||
private fun writeLittleEndianInt(buffer: ByteArray, offset: Int, value: Long) {
|
||||
buffer[offset] = value.toByte()
|
||||
buffer[offset + 1] = (value shr 8).toByte()
|
||||
buffer[offset + 2] = (value shr 16).toByte()
|
||||
buffer[offset + 3] = (value shr 24).toByte()
|
||||
}
|
||||
|
||||
private fun readLittleEndianShort(buffer: ByteArray, offset: Int): Int {
|
||||
return (buffer[offset].toInt() and 0xff) or
|
||||
((buffer[offset + 1].toInt() and 0xff) shl 8)
|
||||
}
|
||||
|
||||
private fun readLittleEndianInt(buffer: ByteArray, offset: Int): Long {
|
||||
return (buffer[offset].toLong() and 0xff) or
|
||||
((buffer[offset + 1].toLong() and 0xff) shl 8) or
|
||||
((buffer[offset + 2].toLong() and 0xff) shl 16) or
|
||||
((buffer[offset + 3].toLong() and 0xff) shl 24)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class SelectedImageFiles(
|
||||
val image: SelectedInstallFile,
|
||||
val metadata: SelectedInstallFile?,
|
||||
)
|
||||
|
||||
private data class CueTrack(
|
||||
val isAudio: Boolean,
|
||||
val blockSize: Int,
|
||||
val blockOffset: Int,
|
||||
val index: MutableMap<Int, Int> = mutableMapOf(),
|
||||
)
|
||||
|
||||
private data class TrackInfo(
|
||||
val isAudio: Boolean,
|
||||
val offset: Long,
|
||||
val blockSize: Int,
|
||||
val blockOffset: Int,
|
||||
val startSector: Int,
|
||||
val numSectors: Int,
|
||||
) {
|
||||
companion object {
|
||||
fun iso(imageSize: Long): TrackInfo {
|
||||
return TrackInfo(
|
||||
isAudio = false,
|
||||
offset = 0,
|
||||
blockSize = CdImageReader.ISO_SECTOR_SIZE,
|
||||
blockOffset = 0,
|
||||
startSector = 0,
|
||||
numSectors = (imageSize / CdImageReader.ISO_SECTOR_SIZE).toInt(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class RandomAccessImage private constructor(
|
||||
private val channel: FileChannel,
|
||||
private val descriptor: ParcelFileDescriptor?,
|
||||
private val tempFile: File?,
|
||||
) : Closeable {
|
||||
val size: Long = channel.size()
|
||||
|
||||
fun readFully(offset: Long, buffer: ByteArray, bufferOffset: Int, length: Int) {
|
||||
val byteBuffer = ByteBuffer.wrap(buffer, bufferOffset, length)
|
||||
var pos = offset
|
||||
while (byteBuffer.hasRemaining()) {
|
||||
val read = channel.read(byteBuffer, pos)
|
||||
if (read < 0) {
|
||||
throw EOFException("Unexpected end of CD image")
|
||||
}
|
||||
pos += read
|
||||
}
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
channel.close()
|
||||
descriptor?.close()
|
||||
tempFile?.delete()
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal fun open(file: File): RandomAccessImage {
|
||||
return RandomAccessImage(FileInputStream(file).channel, null, null)
|
||||
}
|
||||
|
||||
fun open(
|
||||
contentResolver: ContentResolver,
|
||||
file: SelectedInstallFile,
|
||||
tempDir: File,
|
||||
): RandomAccessImage {
|
||||
val descriptor = contentResolver.openFileDescriptor(file.uri, "r")
|
||||
?: throw InstallFailureException(R.string.cd_image_read_error)
|
||||
try {
|
||||
val channel = FileInputStream(descriptor.fileDescriptor).channel
|
||||
channel.size()
|
||||
return RandomAccessImage(channel, descriptor, null)
|
||||
} catch (e: Exception) {
|
||||
descriptor.close()
|
||||
}
|
||||
|
||||
// Some SAF providers expose the URI as a pipe. Copy those to an
|
||||
// app-private temp file so the CD reader can use positional reads.
|
||||
val tempFile = File.createTempFile("cdimage-", ".img", tempDir)
|
||||
try {
|
||||
contentResolver.openInputStream(file.uri)?.use { input ->
|
||||
tempFile.outputStream().buffered().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
} ?: throw InstallFailureException(R.string.cd_image_read_error)
|
||||
return RandomAccessImage(FileInputStream(tempFile).channel, null, tempFile)
|
||||
} catch (e: Exception) {
|
||||
tempFile.delete()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun SelectedInstallFile.baseName(): String {
|
||||
return displayName.substringBeforeLast('.').lowercase(Locale.US)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ package io.github.kichikuou.xsystem35
|
||||
import android.app.AlertDialog
|
||||
import android.media.MediaPlayer
|
||||
import android.os.Bundle
|
||||
import android.os.Process
|
||||
import android.text.InputType
|
||||
import android.util.Log
|
||||
import android.widget.EditText
|
||||
@@ -44,6 +45,8 @@ class GameActivity : SDLActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
gameRoot = File(intent.getStringExtra(EXTRA_GAME_ROOT)!!)
|
||||
// Workaround for https://github.com/libsdl-org/SDL/issues/8995
|
||||
SDLActivity.setWindowStyle(true)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
@@ -56,8 +59,20 @@ class GameActivity : SDLActivity() {
|
||||
midi.onActivityResume()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
try {
|
||||
super.onDestroy()
|
||||
} finally {
|
||||
try {
|
||||
midi.release()
|
||||
} finally {
|
||||
Process.killProcess(Process.myPid())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getLibraries(): Array<String> {
|
||||
return arrayOf("SDL2", "xsystem35")
|
||||
return arrayOf("SDL3", "xsystem35")
|
||||
}
|
||||
|
||||
override fun getArguments(): Array<String> {
|
||||
@@ -75,7 +90,6 @@ class GameActivity : SDLActivity() {
|
||||
return
|
||||
}
|
||||
File(gameRoot, Launcher.TITLE_FILE).writeText(str)
|
||||
Launcher.updateGameList()
|
||||
}
|
||||
|
||||
private fun textInputDialog(msg: String, oldVal: String, maxLen: Int, result: Array<String?>) {
|
||||
@@ -154,11 +168,21 @@ class GameActivity : SDLActivity() {
|
||||
}
|
||||
|
||||
private class MidiPlayer {
|
||||
private enum class State {
|
||||
STOPPED,
|
||||
PLAYING,
|
||||
PAUSED,
|
||||
RELEASED,
|
||||
}
|
||||
|
||||
private val player = MediaPlayer()
|
||||
private var playing = false
|
||||
private var playerPaused = false
|
||||
private var state = State.STOPPED
|
||||
|
||||
fun start(path: String, loop: Boolean) {
|
||||
if (state == State.RELEASED) {
|
||||
return
|
||||
}
|
||||
state = State.STOPPED
|
||||
try {
|
||||
player.apply {
|
||||
reset()
|
||||
@@ -167,7 +191,7 @@ private class MidiPlayer {
|
||||
prepare()
|
||||
start()
|
||||
}
|
||||
playing = true
|
||||
state = State.PLAYING
|
||||
} catch (e: IOException) {
|
||||
Log.e("midiStart", "Cannot play midi", e)
|
||||
player.reset()
|
||||
@@ -175,27 +199,41 @@ private class MidiPlayer {
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
if (playing && player.isPlaying) {
|
||||
player.stop()
|
||||
playing = false
|
||||
when (state) {
|
||||
State.PLAYING, State.PAUSED -> {
|
||||
player.stop()
|
||||
state = State.STOPPED
|
||||
}
|
||||
State.STOPPED, State.RELEASED -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
fun currentPosition(): Int {
|
||||
return if (playing) player.currentPosition else 0
|
||||
return when (state) {
|
||||
State.PLAYING, State.PAUSED -> player.currentPosition
|
||||
State.STOPPED, State.RELEASED -> 0
|
||||
}
|
||||
}
|
||||
|
||||
fun onActivityStop() {
|
||||
if (playing && player.isPlaying) {
|
||||
if (state == State.PLAYING && player.isPlaying) {
|
||||
player.pause()
|
||||
playerPaused = true
|
||||
state = State.PAUSED
|
||||
}
|
||||
}
|
||||
|
||||
fun onActivityResume() {
|
||||
if (playerPaused) {
|
||||
if (state == State.PAUSED) {
|
||||
player.start()
|
||||
playerPaused = false
|
||||
state = State.PLAYING
|
||||
}
|
||||
}
|
||||
|
||||
fun release() {
|
||||
if (state == State.RELEASED) {
|
||||
return
|
||||
}
|
||||
state = State.RELEASED
|
||||
player.release()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
/* Copyright (C) 2026 <KichikuouChrome@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
*/
|
||||
package io.github.kichikuou.xsystem35
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.UTFDataFormatException
|
||||
import java.nio.charset.Charset
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
data class SelectedInstallFile(
|
||||
val uri: Uri,
|
||||
val displayName: String,
|
||||
)
|
||||
|
||||
class GameInstaller(private val store: GameStore) {
|
||||
suspend fun installZip(input: InputStream, progressCallback: suspend (String) -> Unit): File {
|
||||
val dir = store.createDirForGame()
|
||||
var committed = false
|
||||
try {
|
||||
val gameDir = extractFiles(input, dir, progressCallback)
|
||||
committed = true
|
||||
return gameDir
|
||||
} finally {
|
||||
if (!committed && !dir.deleteRecursively()) {
|
||||
Log.w("launcher", "Failed to delete incomplete install directory: $dir")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun installCdImage(
|
||||
files: List<SelectedInstallFile>,
|
||||
contentResolver: ContentResolver,
|
||||
tempDir: File,
|
||||
progressCallback: suspend (String) -> Unit
|
||||
): File {
|
||||
val dir = store.createDirForGame()
|
||||
var committed = false
|
||||
try {
|
||||
val gameDir = extractCdImage(files, contentResolver, tempDir, dir, progressCallback)
|
||||
committed = true
|
||||
return gameDir
|
||||
} finally {
|
||||
if (!committed && !dir.deleteRecursively()) {
|
||||
Log.w("launcher", "Failed to delete incomplete install directory: $dir")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun installCdImageForTest(
|
||||
cdImage: CdImageReader,
|
||||
progressCallback: suspend (String) -> Unit
|
||||
): File {
|
||||
val dir = store.createDirForGame()
|
||||
var committed = false
|
||||
try {
|
||||
val gameDir = extractCdImage(cdImage, dir, progressCallback)
|
||||
committed = true
|
||||
return gameDir
|
||||
} finally {
|
||||
if (!committed && !dir.deleteRecursively()) {
|
||||
Log.w("launcher", "Failed to delete incomplete install directory: $dir")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun extractFiles(
|
||||
input: InputStream,
|
||||
outDir: File,
|
||||
progressCallback: suspend (String) -> Unit
|
||||
): File {
|
||||
val configWriter = GameConfigWriter()
|
||||
val hadDecodeError = forEachZipEntrySuspending(input) { zipEntry, zip ->
|
||||
Log.i("extractFiles", zipEntry.name)
|
||||
if (zipEntry.isDirectory)
|
||||
return@forEachZipEntrySuspending
|
||||
val resolvedPath = resolveOutputPath(outDir, zipEntry.name)
|
||||
val path = resolvedPath.file
|
||||
path.parentFile?.mkdirs()
|
||||
progressCallback(resolvedPath.relativePath)
|
||||
FileOutputStream(path).buffered().use {
|
||||
zip.copyTo(it)
|
||||
}
|
||||
configWriter.maybeAdd(resolvedPath.relativePath)
|
||||
}
|
||||
if (!configWriter.ready) {
|
||||
if (hadDecodeError)
|
||||
throw InstallFailureException(R.string.unsupported_zip)
|
||||
throw InstallFailureException(R.string.cannot_find_ald)
|
||||
}
|
||||
configWriter.write(outDir)
|
||||
return configWriter.gameDir?.let { File(outDir, it) } ?: outDir
|
||||
}
|
||||
|
||||
private suspend fun extractCdImage(
|
||||
files: List<SelectedInstallFile>,
|
||||
contentResolver: ContentResolver,
|
||||
tempDir: File,
|
||||
outDir: File,
|
||||
progressCallback: suspend (String) -> Unit
|
||||
): File {
|
||||
progressCallback("metadata parsing")
|
||||
CdImageReader.open(contentResolver, files, tempDir).use { cdImage ->
|
||||
return extractCdImage(cdImage, outDir, progressCallback)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun extractCdImage(
|
||||
cdImage: CdImageReader,
|
||||
outDir: File,
|
||||
progressCallback: suspend (String) -> Unit
|
||||
): File {
|
||||
val fs = Iso9660FileSystem(cdImage)
|
||||
val gameData = fs.findGameDataDirectory()
|
||||
?: throw InstallFailureException(R.string.cannot_find_game_data_directory)
|
||||
var foundAld = false
|
||||
fs.extractDirectory(gameData, "GAMEDATA") { path, input ->
|
||||
val resolvedPath = resolveOutputPath(outDir, path)
|
||||
resolvedPath.file.parentFile?.mkdirs()
|
||||
progressCallback(resolvedPath.relativePath)
|
||||
FileOutputStream(resolvedPath.file).buffered().use {
|
||||
input.copyTo(it)
|
||||
}
|
||||
if (File(path).name.matches(""".*?s[a-z]\.ald""".toRegex(RegexOption.IGNORE_CASE))) {
|
||||
foundAld = true
|
||||
}
|
||||
}
|
||||
if (!foundAld) {
|
||||
throw InstallFailureException(R.string.cannot_find_ald)
|
||||
}
|
||||
File(outDir, Launcher.GAMEDIR_FILE).writeText("GAMEDATA")
|
||||
extractAudioTracks(cdImage, File(outDir, "GAMEDATA"), progressCallback)
|
||||
return File(outDir, "GAMEDATA")
|
||||
}
|
||||
|
||||
private suspend fun extractAudioTracks(
|
||||
cdImage: CdImageReader,
|
||||
gameDir: File,
|
||||
progressCallback: suspend (String) -> Unit
|
||||
) {
|
||||
val audioTracks = cdImage.audioTracks()
|
||||
if (audioTracks.isEmpty()) {
|
||||
return
|
||||
}
|
||||
val cddaDir = File(gameDir, "cdda")
|
||||
cddaDir.mkdirs()
|
||||
val playlist = arrayOfNulls<String>(audioTracks.maxOrNull() ?: 0)
|
||||
for (track in audioTracks) {
|
||||
val relativePath = "cdda/track%02d.wav".format(track)
|
||||
val outputFile = File(gameDir, relativePath)
|
||||
progressCallback(relativePath)
|
||||
FileOutputStream(outputFile).buffered().use {
|
||||
cdImage.extractAudioTrack(track, it)
|
||||
}
|
||||
playlist[track - 1] = relativePath
|
||||
}
|
||||
File(gameDir, Launcher.PLAYLIST_FILE).writeText(playlist.joinToString("\n") { it ?: "" }.trimEnd('\n'))
|
||||
}
|
||||
}
|
||||
|
||||
internal class InstallFailureException(val msgId: Int) : Exception()
|
||||
|
||||
// A helper class which generates GAMEDIR_FILE and PLAYLIST_FILE.
|
||||
private class GameConfigWriter {
|
||||
var ready = false
|
||||
private set
|
||||
var gameDir: String? = null
|
||||
private set
|
||||
private val aldRegex = """.*?s[a-z]\.ald""".toRegex(RegexOption.IGNORE_CASE)
|
||||
private val audioRegex = """((\d+).*|.*?(\d+))\.(wav|mp3|ogg)""".toRegex(RegexOption.IGNORE_CASE)
|
||||
private val audioFiles: Array<String?> = arrayOfNulls(100)
|
||||
|
||||
fun maybeAdd(path: String) {
|
||||
val name = File(path).name
|
||||
aldRegex.matchEntire(name)?.let {
|
||||
gameDir = File(path).parent
|
||||
ready = true
|
||||
}
|
||||
audioRegex.matchEntire(name)?.let {
|
||||
val track = it.groupValues[2].toIntOrNull() ?: it.groupValues[3].toInt()
|
||||
if (0 < track && track <= audioFiles.size)
|
||||
audioFiles[track - 1] = path
|
||||
}
|
||||
}
|
||||
|
||||
fun write(outDir: File) {
|
||||
// Generate GAMEDIR_FILE
|
||||
gameDir?.let {
|
||||
File(outDir, Launcher.GAMEDIR_FILE).writeText(it)
|
||||
}
|
||||
// Generate PLAYLIST_FILE
|
||||
val absGameDir = gameDir?.let { File(outDir, it) } ?: outDir
|
||||
val playlistFile = File(absGameDir, Launcher.PLAYLIST_FILE)
|
||||
if (!playlistFile.exists()) {
|
||||
val prefixToRemove = gameDir?.let { "$it/" } ?: ""
|
||||
val playlist = audioFiles.joinToString("\n") {
|
||||
it?.removePrefix(prefixToRemove) ?: ""
|
||||
}.trimEnd('\n')
|
||||
playlistFile.writeText(playlist)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @property file Safe canonical output path.
|
||||
* @property relativePath Path relative to the install root after canonicalization.
|
||||
*/
|
||||
internal data class ResolvedOutputPath(val file: File, val relativePath: String)
|
||||
|
||||
internal fun resolveOutputPath(baseDir: File, relativePath: String): ResolvedOutputPath {
|
||||
if (File(relativePath).isAbsolute) {
|
||||
throw IOException("Output path is absolute: $relativePath")
|
||||
}
|
||||
val canonicalBase = baseDir.canonicalFile
|
||||
val file = File(canonicalBase, relativePath).canonicalFile
|
||||
if (!isFileInsideDirectory(file, canonicalBase)) {
|
||||
throw IOException("Output path is outside target directory: $relativePath")
|
||||
}
|
||||
val basePath = canonicalBase.path + File.separator
|
||||
val canonicalRelativePath = file.path.removePrefix(basePath)
|
||||
return ResolvedOutputPath(file, canonicalRelativePath)
|
||||
}
|
||||
|
||||
private fun isFileInsideDirectory(file: File, directory: File): Boolean {
|
||||
return file.path.startsWith(directory.path + File.separator)
|
||||
}
|
||||
|
||||
internal fun forEachZipEntry(input: InputStream, action: (ZipEntry, ZipInputStream) -> Unit): Boolean =
|
||||
runBlocking {
|
||||
forEachZipEntrySuspending(input) { zipEntry, zip ->
|
||||
action(zipEntry, zip)
|
||||
}
|
||||
}
|
||||
|
||||
internal suspend fun forEachZipEntrySuspending(
|
||||
input: InputStream,
|
||||
action: suspend (ZipEntry, ZipInputStream) -> Unit
|
||||
): Boolean {
|
||||
val zip = if (Build.VERSION.SDK_INT >= 24) {
|
||||
ZipInputStream(input.buffered(), Charset.forName("Shift_JIS"))
|
||||
} else {
|
||||
ZipInputStream(input.buffered())
|
||||
}
|
||||
var hadDecodeError = false
|
||||
zip.use {
|
||||
while (true) {
|
||||
try {
|
||||
val zipEntry = zip.nextEntry ?: break
|
||||
action(zipEntry, zip)
|
||||
} catch (e: UTFDataFormatException) {
|
||||
// Attempted to read Shift_JIS zip in Android < 7
|
||||
Log.w("forEachZipEntry", "UTFDataFormatException: skipping a zip entry")
|
||||
zip.closeEntry()
|
||||
hadDecodeError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return hadDecodeError
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/* Copyright (C) 2026 <KichikuouChrome@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
*/
|
||||
package io.github.kichikuou.xsystem35
|
||||
|
||||
import android.util.Log
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.io.UTFDataFormatException
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
class GameStore(private val rootDir: File) {
|
||||
data class Entry(val path: File, val title: String, val timestamp: Long)
|
||||
|
||||
private val gameList = arrayListOf<Entry>()
|
||||
val games: List<Entry>
|
||||
get() = gameList
|
||||
val titles: List<String>
|
||||
get() = gameList.map(Entry::title)
|
||||
|
||||
init {
|
||||
updateGameList()
|
||||
}
|
||||
|
||||
fun uninstall(id: Int) {
|
||||
gameList[id].path.deleteRecursively()
|
||||
gameList.removeAt(id)
|
||||
}
|
||||
|
||||
fun updateGameList() {
|
||||
var saveDirFound = false
|
||||
gameList.clear()
|
||||
for (path in rootDir.listFiles() ?: emptyArray()) {
|
||||
if (!path.isDirectory)
|
||||
continue
|
||||
if (path.name == Launcher.SAVE_DIR) {
|
||||
saveDirFound = true
|
||||
continue
|
||||
}
|
||||
try {
|
||||
val gameDirFile = File(path, Launcher.GAMEDIR_FILE)
|
||||
val gamePath = if (gameDirFile.exists()) File(path, gameDirFile.readText()) else path
|
||||
val titleFile = File(gamePath, Launcher.TITLE_FILE)
|
||||
val title = titleFile.readText()
|
||||
gameList.add(Entry(gamePath, title, titleFile.lastModified()))
|
||||
migratePlaylist(path)
|
||||
} catch (e: IOException) {
|
||||
// Incomplete game installation. Delete it.
|
||||
path.deleteRecursively()
|
||||
}
|
||||
}
|
||||
gameList.sortByDescending(Entry::timestamp)
|
||||
if (!saveDirFound) {
|
||||
File(rootDir, Launcher.SAVE_DIR).mkdir()
|
||||
}
|
||||
}
|
||||
|
||||
fun createDirForGame(): File {
|
||||
var i = 0
|
||||
while (true) {
|
||||
val f = File(rootDir, i++.toString())
|
||||
if (!f.exists() && f.mkdir()) {
|
||||
return f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Throws IOException
|
||||
fun exportSaveData(output: OutputStream) {
|
||||
ZipOutputStream(output.buffered()).use { zip ->
|
||||
for (path in File(rootDir, Launcher.SAVE_DIR).listFiles() ?: emptyArray()) {
|
||||
if (path.isDirectory || path.name.endsWith(".asd."))
|
||||
continue
|
||||
val pathInZip = "${Launcher.SAVE_DIR}/${path.name}"
|
||||
Log.i("exportSaveData", pathInZip)
|
||||
zip.putNextEntry(ZipEntry(pathInZip))
|
||||
path.inputStream().buffered().use {
|
||||
it.copyTo(zip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun importSaveData(input: InputStream): Int? {
|
||||
try {
|
||||
var imported = false
|
||||
forEachZipEntry(input) { zipEntry, zip ->
|
||||
// Process only files directly under save/
|
||||
if (zipEntry.isDirectory || !zipEntry.name.startsWith("save/") ||
|
||||
zipEntry.name.count{it == '/'} != 1)
|
||||
return@forEachZipEntry
|
||||
val path = resolveOutputPath(rootDir, zipEntry.name).file
|
||||
Log.i("importSaveData", zipEntry.name)
|
||||
FileOutputStream(path).buffered().use {
|
||||
zip.copyTo(it)
|
||||
}
|
||||
imported = true
|
||||
}
|
||||
return if (imported) null else R.string.no_data_to_import
|
||||
} catch (e: UTFDataFormatException) {
|
||||
// Attempted to read Shift_JIS zip in Android < 7
|
||||
return R.string.unsupported_zip
|
||||
} catch (e: IOException) {
|
||||
Log.e("launcher", "Failed to extract ZIP", e)
|
||||
return R.string.zip_extraction_error
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSaveData(): Boolean {
|
||||
var success = true
|
||||
File(rootDir, Launcher.SAVE_DIR).listFiles()?.forEach { file ->
|
||||
if (!file.deleteRecursively()) success = false
|
||||
}
|
||||
return success
|
||||
}
|
||||
|
||||
// Xsystem35-sdl2 2.3.0 - 2.11.1 used playlist2.txt. Rename it to playlist.txt.
|
||||
private fun migratePlaylist(dir: File) {
|
||||
val oldPlaylist = File(dir, Launcher.OLD_PLAYLIST_FILE)
|
||||
if (oldPlaylist.exists()) {
|
||||
oldPlaylist.renameTo(File(dir, Launcher.PLAYLIST_FILE))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
/* Copyright (C) 2026 <KichikuouChrome@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
*/
|
||||
package io.github.kichikuou.xsystem35
|
||||
|
||||
import java.io.InputStream
|
||||
import java.nio.charset.Charset
|
||||
import java.util.Locale
|
||||
|
||||
internal class Iso9660FileSystem(private val image: CdImageReader) {
|
||||
private val descriptor = readBestVolumeDescriptor()
|
||||
|
||||
data class Entry(
|
||||
val name: String,
|
||||
val extent: Int,
|
||||
val size: Int,
|
||||
val isDirectory: Boolean,
|
||||
)
|
||||
|
||||
fun findGameDataDirectory(): Entry? {
|
||||
return listDirectory(descriptor.rootDirectory).firstOrNull {
|
||||
it.isDirectory && it.name.uppercase(Locale.US) == "GAMEDATA"
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun extractDirectory(
|
||||
directory: Entry,
|
||||
outputPath: String,
|
||||
writeFile: suspend (String, InputStream) -> Unit,
|
||||
) {
|
||||
for (entry in listDirectory(directory)) {
|
||||
val childPath = "$outputPath/${entry.name}"
|
||||
if (entry.isDirectory) {
|
||||
extractDirectory(entry, childPath, writeFile)
|
||||
} else {
|
||||
writeFile(childPath, fileInputStream(entry))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun listDirectory(directory: Entry): List<Entry> {
|
||||
val bytes = ByteArray(directory.size)
|
||||
image.readDataFully(
|
||||
directory.extent.toLong() * CdImageReader.ISO_SECTOR_SIZE,
|
||||
bytes,
|
||||
0,
|
||||
bytes.size
|
||||
)
|
||||
val entries = mutableListOf<Entry>()
|
||||
var offset = 0
|
||||
while (offset < bytes.size) {
|
||||
val length = bytes[offset].toInt() and 0xff
|
||||
if (length == 0) {
|
||||
offset = ((offset / CdImageReader.ISO_SECTOR_SIZE) + 1) * CdImageReader.ISO_SECTOR_SIZE
|
||||
continue
|
||||
}
|
||||
parseDirectoryRecord(bytes, offset, length)?.let { entries.add(it) }
|
||||
offset += length
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
private fun parseDirectoryRecord(bytes: ByteArray, offset: Int, length: Int): Entry? {
|
||||
if (length < 34) {
|
||||
throw InstallFailureException(R.string.invalid_iso9660_image)
|
||||
}
|
||||
val nameLength = bytes[offset + 32].toInt() and 0xff
|
||||
if (33 + nameLength > length) {
|
||||
throw InstallFailureException(R.string.invalid_iso9660_image)
|
||||
}
|
||||
val rawName = bytes.copyOfRange(offset + 33, offset + 33 + nameLength)
|
||||
if (rawName.size == 1 && (rawName[0].toInt() == 0 || rawName[0].toInt() == 1)) {
|
||||
return null
|
||||
}
|
||||
val name = descriptor.decodeName(rawName)
|
||||
val extent = readLittleEndianInt(bytes, offset + 2)
|
||||
val size = readLittleEndianInt(bytes, offset + 10)
|
||||
val flags = bytes[offset + 25].toInt() and 0xff
|
||||
return Entry(name, extent, size, flags and 0x02 != 0)
|
||||
}
|
||||
|
||||
private fun fileInputStream(entry: Entry): InputStream {
|
||||
return object : InputStream() {
|
||||
private var pos = 0
|
||||
|
||||
override fun read(): Int {
|
||||
val buffer = ByteArray(1)
|
||||
val read = read(buffer, 0, 1)
|
||||
return if (read < 0) -1 else buffer[0].toInt() and 0xff
|
||||
}
|
||||
|
||||
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
|
||||
if (pos >= entry.size) {
|
||||
return -1
|
||||
}
|
||||
val count = minOf(length, entry.size - pos)
|
||||
image.readDataFully(
|
||||
entry.extent.toLong() * CdImageReader.ISO_SECTOR_SIZE + pos,
|
||||
buffer,
|
||||
offset,
|
||||
count
|
||||
)
|
||||
pos += count
|
||||
return count
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readBestVolumeDescriptor(): VolumeDescriptor {
|
||||
var primary: VolumeDescriptor? = null
|
||||
var joliet: VolumeDescriptor? = null
|
||||
val sector = ByteArray(CdImageReader.ISO_SECTOR_SIZE)
|
||||
var sectorNumber = 0x10
|
||||
while (true) {
|
||||
image.readSector(sectorNumber++, sector)
|
||||
if (String(sector, 1, 5, Charsets.US_ASCII) != "CD001") {
|
||||
throw InstallFailureException(R.string.invalid_iso9660_image)
|
||||
}
|
||||
when (sector[0].toInt() and 0xff) {
|
||||
1 -> primary = VolumeDescriptor(parseRootDirectory(sector), false)
|
||||
2 -> if (isJolietDescriptor(sector)) {
|
||||
joliet = VolumeDescriptor(parseRootDirectory(sector), true)
|
||||
}
|
||||
255 -> return joliet ?: primary
|
||||
?: throw InstallFailureException(R.string.invalid_iso9660_image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseRootDirectory(sector: ByteArray): Entry {
|
||||
return Entry(
|
||||
name = "",
|
||||
extent = readLittleEndianInt(sector, 156 + 2),
|
||||
size = readLittleEndianInt(sector, 156 + 10),
|
||||
isDirectory = true,
|
||||
)
|
||||
}
|
||||
|
||||
private fun isJolietDescriptor(sector: ByteArray): Boolean {
|
||||
return sector[88] == 0x25.toByte() &&
|
||||
sector[89] == 0x2f.toByte() &&
|
||||
(sector[90] == 0x40.toByte() || sector[90] == 0x43.toByte() || sector[90] == 0x45.toByte())
|
||||
}
|
||||
|
||||
private data class VolumeDescriptor(
|
||||
val rootDirectory: Entry,
|
||||
val joliet: Boolean,
|
||||
) {
|
||||
fun decodeName(rawName: ByteArray): String {
|
||||
val charset = if (joliet) Charsets.UTF_16BE else Charset.forName("Shift_JIS")
|
||||
return String(rawName, charset).substringBefore(";")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readLittleEndianInt(bytes: ByteArray, offset: Int): Int {
|
||||
return (bytes[offset].toInt() and 0xff) or
|
||||
((bytes[offset + 1].toInt() and 0xff) shl 8) or
|
||||
((bytes[offset + 2].toInt() and 0xff) shl 16) or
|
||||
((bytes[offset + 3].toInt() and 0xff) shl 24)
|
||||
}
|
||||
@@ -17,18 +17,15 @@
|
||||
*/
|
||||
package io.github.kichikuou.xsystem35
|
||||
|
||||
import android.os.Build
|
||||
import android.content.ContentResolver
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.*
|
||||
import java.nio.charset.Charset
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
|
||||
private var gLauncher: Launcher? = null
|
||||
|
||||
@@ -39,7 +36,16 @@ interface LauncherObserver {
|
||||
fun onInstallFailure(msgId: Int)
|
||||
}
|
||||
|
||||
class Launcher private constructor(private val rootDir: File) {
|
||||
sealed class InstallState {
|
||||
object Idle : InstallState()
|
||||
data class Installing(val progress: String?) : InstallState()
|
||||
data class Succeeded(val path: File, val archiveName: String?) : InstallState()
|
||||
data class Failed(val msgId: Int) : InstallState()
|
||||
}
|
||||
|
||||
data class InstallResult(val path: File, val archiveName: String?)
|
||||
|
||||
class Launcher private constructor(rootDir: File) {
|
||||
companion object {
|
||||
const val SAVE_DIR = "save"
|
||||
const val TITLE_FILE = "title.txt"
|
||||
@@ -54,234 +60,118 @@ class Launcher private constructor(private val rootDir: File) {
|
||||
return gLauncher!!
|
||||
}
|
||||
|
||||
fun updateGameList() {
|
||||
gLauncher?.updateGameList()
|
||||
}
|
||||
}
|
||||
|
||||
data class Entry(val path: File, val title: String, val timestamp: Long)
|
||||
val games = arrayListOf<Entry>()
|
||||
private val store = GameStore(rootDir)
|
||||
private val installer = GameInstaller(store)
|
||||
val games: List<GameStore.Entry>
|
||||
get() = store.games
|
||||
val titles: List<String>
|
||||
get() = games.map(Entry::title)
|
||||
get() = store.titles
|
||||
var observer: LauncherObserver? = null
|
||||
var isInstalling = false
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
private var installJob: Job? = null
|
||||
var installState: InstallState = InstallState.Idle
|
||||
private set
|
||||
|
||||
init {
|
||||
updateGameList()
|
||||
}
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
fun install(input: InputStream, archiveName: String?) {
|
||||
val dir = createDirForGame()
|
||||
isInstalling = true
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
try {
|
||||
val gameDir = withContext(Dispatchers.IO) {
|
||||
extractFiles(input, dir) { msg ->
|
||||
GlobalScope.launch(Dispatchers.Main) {
|
||||
observer?.onInstallProgress(msg)
|
||||
fun installZip(input: InputStream, archiveName: String?) {
|
||||
if (installJob?.isActive == true) {
|
||||
input.close()
|
||||
return
|
||||
}
|
||||
startInstallJob {
|
||||
val gameDir = withContext(Dispatchers.IO) {
|
||||
input.use {
|
||||
installer.installZip(it) { msg ->
|
||||
withContext(Dispatchers.Main) {
|
||||
setInstallProgress(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
observer?.onInstallSuccess(gameDir, archiveName)
|
||||
} catch (e: InstallFailureException) {
|
||||
observer?.onInstallFailure(e.msgId)
|
||||
} catch (e: Exception) {
|
||||
Log.e("launcher", "Failed to extract ZIP", e)
|
||||
observer?.onInstallFailure(R.string.zip_extraction_error)
|
||||
}
|
||||
isInstalling = false
|
||||
InstallResult(gameDir, archiveName)
|
||||
}
|
||||
}
|
||||
|
||||
fun installCdImage(
|
||||
files: List<SelectedInstallFile>,
|
||||
contentResolver: ContentResolver,
|
||||
tempDir: File,
|
||||
) {
|
||||
if (installJob?.isActive == true) {
|
||||
return
|
||||
}
|
||||
startInstallJob {
|
||||
val gameDir = withContext(Dispatchers.IO) {
|
||||
installer.installCdImage(files, contentResolver, tempDir) { msg ->
|
||||
withContext(Dispatchers.Main) {
|
||||
setInstallProgress(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
InstallResult(gameDir, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun startInstallJob(block: suspend () -> InstallResult) {
|
||||
if (installJob?.isActive == true) {
|
||||
return
|
||||
}
|
||||
installState = InstallState.Installing(null)
|
||||
installJob = scope.launch {
|
||||
try {
|
||||
val result = block()
|
||||
setInstallSucceeded(result)
|
||||
} catch (e: InstallFailureException) {
|
||||
setInstallFailed(e.msgId)
|
||||
} catch (e: Exception) {
|
||||
Log.e("launcher", "Failed to install game", e)
|
||||
setInstallFailed(R.string.install_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun consumeInstallResult() {
|
||||
if (installState is InstallState.Succeeded || installState is InstallState.Failed) {
|
||||
installState = InstallState.Idle
|
||||
}
|
||||
}
|
||||
|
||||
private fun setInstallProgress(progress: String) {
|
||||
installState = InstallState.Installing(progress)
|
||||
observer?.onInstallProgress(progress)
|
||||
}
|
||||
|
||||
private fun setInstallSucceeded(result: InstallResult) {
|
||||
installState = InstallState.Succeeded(result.path, result.archiveName)
|
||||
observer?.onInstallSuccess(result.path, result.archiveName)
|
||||
}
|
||||
|
||||
private fun setInstallFailed(msgId: Int) {
|
||||
installState = InstallState.Failed(msgId)
|
||||
observer?.onInstallFailure(msgId)
|
||||
}
|
||||
|
||||
fun uninstall(id: Int) {
|
||||
games[id].path.deleteRecursively()
|
||||
games.removeAt(id)
|
||||
store.uninstall(id)
|
||||
observer?.onGameListChange()
|
||||
}
|
||||
|
||||
private fun updateGameList() {
|
||||
var saveDirFound = false
|
||||
games.clear()
|
||||
for (path in rootDir.listFiles() ?: emptyArray()) {
|
||||
if (!path.isDirectory)
|
||||
continue
|
||||
if (path.name == SAVE_DIR) {
|
||||
saveDirFound = true
|
||||
continue
|
||||
}
|
||||
try {
|
||||
val gameDirFile = File(path, GAMEDIR_FILE)
|
||||
val gamePath = if (gameDirFile.exists()) File(path, gameDirFile.readText()) else path
|
||||
val titleFile = File(gamePath, TITLE_FILE)
|
||||
val title = titleFile.readText()
|
||||
games.add(Entry(gamePath, title, titleFile.lastModified()))
|
||||
migratePlaylist(path)
|
||||
} catch (e: IOException) {
|
||||
// Incomplete game installation. Delete it.
|
||||
path.deleteRecursively()
|
||||
}
|
||||
}
|
||||
games.sortByDescending(Entry::timestamp)
|
||||
if (!saveDirFound) {
|
||||
File(rootDir, SAVE_DIR).mkdir()
|
||||
}
|
||||
fun refreshGameList() {
|
||||
store.updateGameList()
|
||||
observer?.onGameListChange()
|
||||
}
|
||||
|
||||
private fun createDirForGame(): File {
|
||||
var i = 0
|
||||
while (true) {
|
||||
val f = File(rootDir, i++.toString())
|
||||
if (!f.exists() && f.mkdir()) {
|
||||
return f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Throws IOException
|
||||
fun exportSaveData(output: OutputStream) {
|
||||
ZipOutputStream(output.buffered()).use { zip ->
|
||||
for (path in File(rootDir, SAVE_DIR).listFiles() ?: emptyArray()) {
|
||||
if (path.isDirectory || path.name.endsWith(".asd."))
|
||||
continue
|
||||
val pathInZip = "${SAVE_DIR}/${path.name}"
|
||||
Log.i("exportSaveData", pathInZip)
|
||||
zip.putNextEntry(ZipEntry(pathInZip))
|
||||
path.inputStream().buffered().use {
|
||||
it.copyTo(zip)
|
||||
}
|
||||
}
|
||||
}
|
||||
store.exportSaveData(output)
|
||||
}
|
||||
|
||||
fun importSaveData(input: InputStream): Int? {
|
||||
try {
|
||||
var imported = false
|
||||
forEachZipEntry(input) { zipEntry, zip ->
|
||||
// Process only files directly under save/
|
||||
if (zipEntry.isDirectory || !zipEntry.name.startsWith("save/") ||
|
||||
zipEntry.name.count{it == '/'} != 1)
|
||||
return@forEachZipEntry
|
||||
Log.i("importSaveData", zipEntry.name)
|
||||
FileOutputStream(File(rootDir, zipEntry.name)).buffered().use {
|
||||
zip.copyTo(it)
|
||||
}
|
||||
imported = true
|
||||
}
|
||||
return if (imported) null else R.string.no_data_to_import
|
||||
} catch (e: UTFDataFormatException) {
|
||||
// Attempted to read Shift_JIS zip in Android < 7
|
||||
return R.string.unsupported_zip
|
||||
} catch (e: IOException) {
|
||||
Log.e("launcher", "Failed to extract ZIP", e)
|
||||
return R.string.zip_extraction_error
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractFiles(input: InputStream, outDir: File, progressCallback: (String) -> Unit): File {
|
||||
val configWriter = GameConfigWriter()
|
||||
val hadDecodeError = forEachZipEntry(input) { zipEntry, zip ->
|
||||
Log.i("extractFiles", zipEntry.name)
|
||||
val path = File(outDir, zipEntry.name)
|
||||
if (zipEntry.isDirectory)
|
||||
return@forEachZipEntry
|
||||
path.parentFile?.mkdirs()
|
||||
progressCallback(zipEntry.name)
|
||||
FileOutputStream(path).buffered().use {
|
||||
zip.copyTo(it)
|
||||
}
|
||||
configWriter.maybeAdd(zipEntry.name)
|
||||
}
|
||||
if (!configWriter.ready) {
|
||||
if (hadDecodeError)
|
||||
throw InstallFailureException(R.string.unsupported_zip)
|
||||
throw InstallFailureException(R.string.cannot_find_ald)
|
||||
}
|
||||
configWriter.write(outDir)
|
||||
return configWriter.gameDir?.let { File(outDir, it) } ?: outDir
|
||||
}
|
||||
|
||||
// Xsystem35-sdl2 2.3.0 - 2.11.1 used playlist2.txt. Rename it to playlist.txt.
|
||||
private fun migratePlaylist(dir: File) {
|
||||
val oldPlaylist = File(dir, OLD_PLAYLIST_FILE)
|
||||
if (oldPlaylist.exists()) {
|
||||
oldPlaylist.renameTo(File(dir, PLAYLIST_FILE))
|
||||
}
|
||||
}
|
||||
|
||||
class InstallFailureException(val msgId: Int) : Exception()
|
||||
|
||||
// A helper class which generates GAMEDIR_FILE and PLAYLIST_FILE.
|
||||
private class GameConfigWriter {
|
||||
var ready = false
|
||||
private set
|
||||
var gameDir: String? = null
|
||||
private set
|
||||
private val aldRegex = """.*?s[a-z]\.ald""".toRegex(RegexOption.IGNORE_CASE)
|
||||
private val audioRegex = """((\d+).*|.*?(\d+))\.(wav|mp3|ogg)""".toRegex(RegexOption.IGNORE_CASE)
|
||||
private val audioFiles: Array<String?> = arrayOfNulls(100)
|
||||
|
||||
fun maybeAdd(path: String) {
|
||||
val name = File(path).name
|
||||
aldRegex.matchEntire(name)?.let {
|
||||
gameDir = File(path).parent
|
||||
ready = true
|
||||
}
|
||||
audioRegex.matchEntire(name)?.let {
|
||||
val track = it.groupValues[2].toIntOrNull() ?: it.groupValues[3].toInt()
|
||||
if (0 < track && track <= audioFiles.size)
|
||||
audioFiles[track - 1] = path
|
||||
}
|
||||
}
|
||||
|
||||
fun write(outDir: File) {
|
||||
// Generate GAMEDIR_FILE
|
||||
gameDir?.let {
|
||||
File(outDir, GAMEDIR_FILE).writeText(it)
|
||||
}
|
||||
// Generate PLAYLIST_FILE
|
||||
val absGameDir = gameDir?.let { File(outDir, it) } ?: outDir
|
||||
val playlistFile = File(absGameDir, PLAYLIST_FILE)
|
||||
if (!playlistFile.exists()) {
|
||||
val prefixToRemove = gameDir?.let { "$it/" } ?: ""
|
||||
val playlist = audioFiles.joinToString("\n") {
|
||||
it?.removePrefix(prefixToRemove) ?: ""
|
||||
}.trimEnd('\n')
|
||||
playlistFile.writeText(playlist)
|
||||
}
|
||||
}
|
||||
return store.importSaveData(input)
|
||||
}
|
||||
|
||||
fun clearSaveData(): Boolean {
|
||||
var success = true
|
||||
File(rootDir, SAVE_DIR).listFiles()?.forEach { file ->
|
||||
if (!file.deleteRecursively()) success = false
|
||||
}
|
||||
return success
|
||||
return store.clearSaveData()
|
||||
}
|
||||
}
|
||||
|
||||
private fun forEachZipEntry(input: InputStream, action: (ZipEntry, ZipInputStream) -> Unit): Boolean {
|
||||
val zip = if (Build.VERSION.SDK_INT >= 24) {
|
||||
ZipInputStream(input.buffered(), Charset.forName("Shift_JIS"))
|
||||
} else {
|
||||
ZipInputStream(input.buffered())
|
||||
}
|
||||
var hadDecodeError = false
|
||||
zip.use {
|
||||
while (true) {
|
||||
try {
|
||||
val zipEntry = zip.nextEntry ?: break
|
||||
action(zipEntry, zip)
|
||||
} catch (e: UTFDataFormatException) {
|
||||
// Attempted to read Shift_JIS zip in Android < 7
|
||||
Log.w("forEachZipEntry", "UTFDataFormatException: skipping a zip entry")
|
||||
zip.closeEntry()
|
||||
hadDecodeError = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return hadDecodeError
|
||||
}
|
||||
|
||||
@@ -33,10 +33,10 @@ import android.widget.Toast
|
||||
import java.io.*
|
||||
|
||||
private const val CONTENT_TYPE_ZIP = "application/zip"
|
||||
private const val INSTALL_REQUEST = 1
|
||||
private const val ZIP_INSTALL_REQUEST = 1
|
||||
private const val SAVEDATA_EXPORT_REQUEST = 2
|
||||
private const val SAVEDATA_IMPORT_REQUEST = 3
|
||||
private const val STATE_PROGRESS_TEXT = "progressText"
|
||||
private const val CD_IMAGE_INSTALL_REQUEST = 4
|
||||
|
||||
class LauncherActivity : Activity(), LauncherObserver {
|
||||
private lateinit var launcher: Launcher
|
||||
@@ -48,9 +48,7 @@ class LauncherActivity : Activity(), LauncherObserver {
|
||||
|
||||
launcher = Launcher.getInstance(filesDir)
|
||||
launcher.observer = this
|
||||
if (launcher.isInstalling) {
|
||||
showProgressDialog(savedInstanceState)
|
||||
}
|
||||
renderInstallState(launcher.installState)
|
||||
|
||||
onGameListChange()
|
||||
val listView = findViewById<ListView>(R.id.list)
|
||||
@@ -70,11 +68,9 @@ class LauncherActivity : Activity(), LauncherObserver {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
progressDialog?.let {
|
||||
outState.putCharSequence(STATE_PROGRESS_TEXT, it.findViewById<TextView>(R.id.text).text)
|
||||
}
|
||||
super.onSaveInstanceState(outState)
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
launcher.refreshGameList()
|
||||
}
|
||||
|
||||
private fun onListItemClick(position: Int) {
|
||||
@@ -100,7 +96,15 @@ class LauncherActivity : Activity(), LauncherObserver {
|
||||
R.id.install_from_zip -> {
|
||||
val i = Intent(Intent.ACTION_GET_CONTENT)
|
||||
i.type = CONTENT_TYPE_ZIP
|
||||
startActivityForResult(Intent.createChooser(i, getString(R.string.choose_a_file)), INSTALL_REQUEST)
|
||||
startActivityForResult(Intent.createChooser(i, getString(R.string.choose_a_file)), ZIP_INSTALL_REQUEST)
|
||||
true
|
||||
}
|
||||
R.id.install_from_cd_image -> {
|
||||
val i = Intent(Intent.ACTION_GET_CONTENT)
|
||||
i.type = "*/*"
|
||||
i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
startActivityForResult(Intent.createChooser(i, getString(R.string.choose_cd_image_files)),
|
||||
CD_IMAGE_INSTALL_REQUEST)
|
||||
true
|
||||
}
|
||||
R.id.export_savedata -> {
|
||||
@@ -150,14 +154,20 @@ class LauncherActivity : Activity(), LauncherObserver {
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (resultCode != RESULT_OK)
|
||||
return
|
||||
val uri = data?.data ?: return
|
||||
when (requestCode) {
|
||||
INSTALL_REQUEST -> {
|
||||
ZIP_INSTALL_REQUEST -> {
|
||||
val uri = data?.data ?: return
|
||||
val input = contentResolver.openInputStream(uri) ?: return
|
||||
showProgressDialog()
|
||||
launcher.install(input, getArchiveName(uri))
|
||||
launcher.installZip(input, getArchiveName(uri))
|
||||
renderInstallState(launcher.installState)
|
||||
}
|
||||
CD_IMAGE_INSTALL_REQUEST -> {
|
||||
val files = getSelectedInstallFiles(data)
|
||||
launcher.installCdImage(files, contentResolver, cacheDir)
|
||||
renderInstallState(launcher.installState)
|
||||
}
|
||||
SAVEDATA_EXPORT_REQUEST -> try {
|
||||
val uri = data?.data ?: return
|
||||
launcher.exportSaveData(contentResolver.openOutputStream(uri)!!)
|
||||
Toast.makeText(this, R.string.save_data_export_success, Toast.LENGTH_SHORT).show()
|
||||
} catch (e: IOException) {
|
||||
@@ -165,6 +175,7 @@ class LauncherActivity : Activity(), LauncherObserver {
|
||||
errorDialog(R.string.save_data_export_error)
|
||||
}
|
||||
SAVEDATA_IMPORT_REQUEST -> {
|
||||
val uri = data?.data ?: return
|
||||
val input = contentResolver.openInputStream(uri) ?: return
|
||||
val errMsgId = launcher.importSaveData(input)
|
||||
if (errMsgId == null) {
|
||||
@@ -184,17 +195,15 @@ class LauncherActivity : Activity(), LauncherObserver {
|
||||
}
|
||||
|
||||
override fun onInstallProgress(path: String) {
|
||||
progressDialog?.findViewById<TextView>(R.id.text)?.text = getString(R.string.install_progress, path)
|
||||
renderInstallState(launcher.installState)
|
||||
}
|
||||
|
||||
override fun onInstallSuccess(path: File, archiveName: String?) {
|
||||
dismissProgressDialog()
|
||||
startGame(path, archiveName)
|
||||
renderInstallState(launcher.installState)
|
||||
}
|
||||
|
||||
override fun onInstallFailure(msgId: Int) {
|
||||
dismissProgressDialog()
|
||||
errorDialog(msgId)
|
||||
renderInstallState(launcher.installState)
|
||||
}
|
||||
|
||||
private fun startGame(path: File, archiveName: String?) {
|
||||
@@ -210,16 +219,35 @@ class LauncherActivity : Activity(), LauncherObserver {
|
||||
launcher.uninstall(id)
|
||||
}
|
||||
|
||||
private fun showProgressDialog(savedInstanceState: Bundle? = null) {
|
||||
progressDialog = Dialog(this)
|
||||
progressDialog!!.apply {
|
||||
setTitle(R.string.install_dialog_title)
|
||||
setCancelable(false)
|
||||
setContentView(R.layout.progress_dialog)
|
||||
savedInstanceState?.let {
|
||||
findViewById<TextView>(R.id.text)?.text = it.getCharSequence(STATE_PROGRESS_TEXT)
|
||||
private fun renderInstallState(state: InstallState) {
|
||||
when (state) {
|
||||
InstallState.Idle -> dismissProgressDialog()
|
||||
is InstallState.Installing -> showProgressDialog(state.progress)
|
||||
is InstallState.Succeeded -> {
|
||||
dismissProgressDialog()
|
||||
startGame(state.path, state.archiveName)
|
||||
launcher.consumeInstallResult()
|
||||
}
|
||||
show()
|
||||
is InstallState.Failed -> {
|
||||
dismissProgressDialog()
|
||||
errorDialog(state.msgId)
|
||||
launcher.consumeInstallResult()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showProgressDialog(progress: String? = null) {
|
||||
if (progressDialog == null) {
|
||||
progressDialog = Dialog(this)
|
||||
progressDialog!!.apply {
|
||||
setTitle(R.string.install_dialog_title)
|
||||
setCancelable(false)
|
||||
setContentView(R.layout.progress_dialog)
|
||||
show()
|
||||
}
|
||||
}
|
||||
progress?.let {
|
||||
progressDialog?.findViewById<TextView>(R.id.text)?.text = getString(R.string.install_progress, it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,13 +264,33 @@ class LauncherActivity : Activity(), LauncherObserver {
|
||||
}
|
||||
|
||||
private fun getArchiveName(uri: Uri): String? {
|
||||
val fname = getDisplayName(uri) ?: return null
|
||||
return if (fname.endsWith(".zip", true)) fname.dropLast(4) else fname
|
||||
}
|
||||
|
||||
private fun getSelectedInstallFiles(data: Intent?): List<SelectedInstallFile> {
|
||||
val files = mutableListOf<SelectedInstallFile>()
|
||||
val clipData = data?.clipData
|
||||
if (clipData != null) {
|
||||
for (i in 0 until clipData.itemCount) {
|
||||
val uri = clipData.getItemAt(i).uri
|
||||
files.add(SelectedInstallFile(uri, getDisplayName(uri) ?: uri.lastPathSegment ?: ""))
|
||||
}
|
||||
} else {
|
||||
data?.data?.let { uri ->
|
||||
files.add(SelectedInstallFile(uri, getDisplayName(uri) ?: uri.lastPathSegment ?: ""))
|
||||
}
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
private fun getDisplayName(uri: Uri): String? {
|
||||
val cursor = contentResolver.query(uri, null, null, null, null, null)
|
||||
cursor?.use {
|
||||
if (it.moveToFirst()) {
|
||||
val column = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (column >= 0) {
|
||||
val fname = it.getString(column)
|
||||
return if (fname.endsWith(".zip", true)) fname.dropLast(4) else fname
|
||||
return it.getString(column)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,10 +12,9 @@ class LicensesMenuActivity : Activity() {
|
||||
Entry("xsystem35-sdl2", "xsystem35", "https://github.com/kichikuou/xsystem35-sdl2"),
|
||||
Entry("SDL", "SDL", "https://www.libsdl.org/"),
|
||||
Entry("SDL_mixer", "SDL_mixer", "https://github.com/libsdl-org/SDL_mixer"),
|
||||
Entry("SDL_ttf", "SDL_ttf", "https://github.com/libsdl-org/SDL_ttf"),
|
||||
Entry("NanoJPEG", "nanojpeg", "https://keyj.emphy.de/nanojpeg/"),
|
||||
Entry("microui", "microui", "https://github.com/rxi/microui"),
|
||||
Entry("FreeType", "freetype", "https://freetype.org/"),
|
||||
Entry("HarfBuzz", "harfbuzz", "https://harfbuzz.github.io/"),
|
||||
Entry("MotoyaLCedar W3 mono", "MTLc3m", "https://github.com/aosp-mirror/platform_frameworks_base/tree/lollipop-release/data/fonts"),
|
||||
Entry("IPA明朝", "mincho", "https://moji.or.jp/ipafont/"),
|
||||
)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@
|
||||
<item
|
||||
android:id="@+id/install_from_zip"
|
||||
android:title="@string/action_install_from_zip" />
|
||||
<item
|
||||
android:id="@+id/install_from_cd_image"
|
||||
android:title="@string/action_install_from_cd_image" />
|
||||
<item
|
||||
android:id="@+id/export_savedata"
|
||||
android:title="@string/action_export_save_data" />
|
||||
@@ -18,4 +21,4 @@
|
||||
<item
|
||||
android:id="@+id/licenses"
|
||||
android:title="@string/action_licenses" />
|
||||
</menu>
|
||||
</menu>
|
||||
|
||||
@@ -4,17 +4,24 @@
|
||||
<string name="action_export_save_data">セーブデータをエクスポート</string>
|
||||
<string name="action_help">使い方</string>
|
||||
<string name="action_import_save_data">セーブデータをインポート</string>
|
||||
<string name="action_install_from_cd_image">CDイメージからインストール</string>
|
||||
<string name="action_install_from_zip">ZIPからインストール</string>
|
||||
<string name="action_licenses">オープンソースライセンス</string>
|
||||
<string name="action_clear_save_data">セーブデータをクリア</string>
|
||||
<string name="cancel">キャンセル</string>
|
||||
<string name="cannot_find_ald">System 3.x のファイル (*.ald) が見つかりません。</string>
|
||||
<string name="cannot_find_game_data_directory">CDイメージ内に GAMEDATA ディレクトリが見つかりません。</string>
|
||||
<string name="cd_image_read_error">選択された CD イメージファイルを読み取れません。</string>
|
||||
<string name="choose_a_file">ファイルを選択</string>
|
||||
<string name="choose_cd_image_files">CDイメージファイルを選択</string>
|
||||
<string name="clear_save_data_confirm">セーブデータをすべて削除します。元には戻せません。本当によろしいですか?</string>
|
||||
<string name="confirm">確認</string>
|
||||
<string name="error_dialog_title">エラー</string>
|
||||
<string name="install_dialog_title">インストール中…</string>
|
||||
<string name="install_error">ゲームのインストールに失敗しました。</string>
|
||||
<string name="install_progress">%s を展開中</string>
|
||||
<string name="invalid_iso9660_image">有効な ISO9660 イメージではありません。</string>
|
||||
<string name="missing_cd_image_metadata">CD イメージファイルと対応するメタデータファイルを一緒に選択してください。ファイル選択画面ではファイルを長押しすると複数選択モードに入れます。</string>
|
||||
<string name="no_data_to_import">ZIP アーカイブ中に save/ フォルダが見つかりません。</string>
|
||||
<string name="save_data_export_error">セーブデータのエクスポートに失敗しました。</string>
|
||||
<string name="save_data_export_success">エクスポートに成功しました。</string>
|
||||
@@ -22,6 +29,7 @@
|
||||
<string name="save_data_clear_success">セーブデータをクリアしました。</string>
|
||||
<string name="save_data_clear_error">セーブデータのクリアに失敗しました。</string>
|
||||
<string name="uninstall_dialog_message">「%s」をアンインストールしますか?</string>
|
||||
<string name="unsupported_cd_image">この CD イメージ形式にはまだ対応していません。現在は .iso、.bin/.img + .cue/.ccd、.mdf/.mds に対応しています。</string>
|
||||
<string name="unsupported_zip">この形式の ZIP はサポートしていません。</string>
|
||||
<string name="zip_extraction_error">ZIP の展開に失敗しました。</string>
|
||||
<string name="usage">
|
||||
@@ -41,4 +49,4 @@
|
||||
<p>3. この画面の右上のメニューボタンをタップして「ZIPからインストール」を選択し、転送したZIPを選びます。</p>
|
||||
]]>
|
||||
</string>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -4,16 +4,23 @@
|
||||
<string name="action_export_save_data">Export Save Files</string>
|
||||
<string name="action_help">Help</string>
|
||||
<string name="action_import_save_data">Import Save Files</string>
|
||||
<string name="action_install_from_cd_image">Install from CD image</string>
|
||||
<string name="action_install_from_zip">Install from ZIP</string>
|
||||
<string name="action_licenses">Open source licenses</string>
|
||||
<string name="cancel">Cancel</string>
|
||||
<string name="cannot_find_ald">Cannot find System 3.x game files (*.ald).</string>
|
||||
<string name="cannot_find_game_data_directory">Cannot find GAMEDATA directory in the CD image.</string>
|
||||
<string name="cd_image_read_error">Cannot read the selected CD image file.</string>
|
||||
<string name="choose_a_file">Choose a file</string>
|
||||
<string name="choose_cd_image_files">Choose CD image files</string>
|
||||
<string name="clear_save_data_confirm">Are you sure you want to clear all save files? This cannot be undone.</string>
|
||||
<string name="confirm">Confirm</string>
|
||||
<string name="error_dialog_title">Error</string>
|
||||
<string name="install_dialog_title">Installing…</string>
|
||||
<string name="install_error">Failed to install game.</string>
|
||||
<string name="install_progress">Extracting %s</string>
|
||||
<string name="invalid_iso9660_image">This file is not a valid ISO9660 image.</string>
|
||||
<string name="missing_cd_image_metadata">Select a matching metadata file with the CD image file. In the file picker, long-press a file to enter multi-select mode.</string>
|
||||
<string name="no_data_to_import">No save/ folder in the ZIP archive.</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="save_data_clear_error">Failed to clear save files.</string>
|
||||
@@ -22,6 +29,7 @@
|
||||
<string name="save_data_import_success">Imported successfully.</string>
|
||||
<string name="save_data_export_success">Exported successfully.</string>
|
||||
<string name="uninstall_dialog_message">Uninstall \"%s\"?</string>
|
||||
<string name="unsupported_cd_image">This CD image format is not supported yet. Currently only .iso, .bin/.img + .cue/.ccd, and .mdf/.mds are supported.</string>
|
||||
<string name="unsupported_zip">This type of ZIP is not supported.</string>
|
||||
<string name="zip_extraction_error">Failed to extract ZIP.</string>
|
||||
<string name="usage">
|
||||
|
||||
@@ -0,0 +1,714 @@
|
||||
/* Copyright (C) 2026 <KichikuouChrome@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation; either version 2 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||
*
|
||||
*/
|
||||
package io.github.kichikuou.xsystem35
|
||||
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assert.fail
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.nio.charset.Charset
|
||||
|
||||
class Iso9660FileSystemTest {
|
||||
@get:Rule
|
||||
val tmp = TemporaryFolder()
|
||||
|
||||
@Test
|
||||
fun extractsGameDataDirectoryFromPrimaryDescriptor() = runBlocking {
|
||||
val iso = SyntheticIsoBuilder()
|
||||
.addGameDataFile("SA.ALD;1", "ald".toByteArray())
|
||||
.addGameDataFile("SUB/FILE.TXT;1", "text".toByteArray())
|
||||
.writeTo(tmp.newFile("game.iso"))
|
||||
|
||||
CdImageReader.openForFile(iso).use { reader ->
|
||||
val fs = Iso9660FileSystem(reader)
|
||||
val gameData = fs.findGameDataDirectory()
|
||||
?: throw AssertionError("GAMEDATA directory was not found")
|
||||
val extracted = mutableMapOf<String, ByteArray>()
|
||||
|
||||
fs.extractDirectory(gameData, "GAMEDATA") { path, input ->
|
||||
extracted[path] = input.readBytes()
|
||||
}
|
||||
|
||||
assertArrayEquals("ald".toByteArray(), extracted["GAMEDATA/SA.ALD"])
|
||||
assertArrayEquals("text".toByteArray(), extracted["GAMEDATA/SUB/FILE.TXT"])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun decodesPrimaryDescriptorNamesAsShiftJis() = runBlocking {
|
||||
val name = "表.TXT;1"
|
||||
val iso = SyntheticIsoBuilder()
|
||||
.addGameDataFile(name, "sjis".toByteArray())
|
||||
.writeTo(tmp.newFile("sjis.iso"))
|
||||
|
||||
CdImageReader.openForFile(iso).use { reader ->
|
||||
val fs = Iso9660FileSystem(reader)
|
||||
val gameData = fs.findGameDataDirectory()
|
||||
?: throw AssertionError("GAMEDATA directory was not found")
|
||||
val extracted = mutableMapOf<String, ByteArray>()
|
||||
|
||||
fs.extractDirectory(gameData, "GAMEDATA") { path, input ->
|
||||
extracted[path] = input.readBytes()
|
||||
}
|
||||
|
||||
assertArrayEquals("sjis".toByteArray(), extracted["GAMEDATA/表.TXT"])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun prefersJolietDescriptorOverPrimaryDescriptor() = runBlocking {
|
||||
val iso = SyntheticIsoBuilder()
|
||||
.addGameDataFile("PRIMARY.TXT;1", "primary".toByteArray())
|
||||
.addJolietGameDataFile("日本語.TXT;1", "joliet".toByteArray())
|
||||
.writeTo(tmp.newFile("joliet.iso"))
|
||||
|
||||
CdImageReader.openForFile(iso).use { reader ->
|
||||
val fs = Iso9660FileSystem(reader)
|
||||
val gameData = fs.findGameDataDirectory()
|
||||
?: throw AssertionError("GAMEDATA directory was not found")
|
||||
val extracted = mutableMapOf<String, ByteArray>()
|
||||
|
||||
fs.extractDirectory(gameData, "GAMEDATA") { path, input ->
|
||||
extracted[path] = input.readBytes()
|
||||
}
|
||||
|
||||
assertFalse(extracted.containsKey("GAMEDATA/PRIMARY.TXT"))
|
||||
assertArrayEquals("joliet".toByteArray(), extracted["GAMEDATA/日本語.TXT"])
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsInvalidVolumeDescriptorSignature() {
|
||||
val iso = tmp.newFile("invalid.iso")
|
||||
iso.writeBytes(ByteArray(18 * CdImageReader.ISO_SECTOR_SIZE))
|
||||
|
||||
val e = assertInstallFailure(R.string.invalid_iso9660_image) {
|
||||
CdImageReader.openForFile(iso).use { Iso9660FileSystem(it) }
|
||||
}
|
||||
assertEquals(R.string.invalid_iso9660_image, e.msgId)
|
||||
}
|
||||
}
|
||||
|
||||
class CdImageReaderTest {
|
||||
@get:Rule
|
||||
val tmp = TemporaryFolder()
|
||||
|
||||
@Test
|
||||
fun readsIsoSectorAt2048ByteOffset() {
|
||||
val sector0 = sectorFilledWith(0x10)
|
||||
val sector1 = sectorFilledWith(0x20)
|
||||
val image = tmp.newFile("plain.iso")
|
||||
image.writeBytes(sector0 + sector1)
|
||||
|
||||
CdImageReader.openForFile(image).use { reader ->
|
||||
val buffer = ByteArray(CdImageReader.ISO_SECTOR_SIZE)
|
||||
|
||||
reader.readSector(1, buffer)
|
||||
|
||||
assertArrayEquals(sector1, buffer)
|
||||
assertTrue(reader.audioTracks().isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cueMode12048ReadsDataSector() {
|
||||
val payload = sectorFilledWith(0x31)
|
||||
val image = tmp.newFile("mode2048.bin")
|
||||
image.writeBytes(payload)
|
||||
|
||||
CdImageReader.openForFile(image, "mode2048.cue", cue("MODE1/2048").toByteArray()).use { reader ->
|
||||
val buffer = ByteArray(CdImageReader.ISO_SECTOR_SIZE)
|
||||
|
||||
reader.readSector(0, buffer)
|
||||
|
||||
assertArrayEquals(payload, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cueMode12352ReadsPayloadAtOffset16() {
|
||||
val payload = sectorFilledWith(0x42)
|
||||
val image = tmp.newFile("mode2352.bin")
|
||||
image.writeBytes(rawDataSector(payload, 0x11))
|
||||
|
||||
CdImageReader.openForFile(image, "mode2352.cue", cue("MODE1/2352").toByteArray()).use { reader ->
|
||||
val buffer = ByteArray(CdImageReader.ISO_SECTOR_SIZE)
|
||||
|
||||
reader.readSector(0, buffer)
|
||||
|
||||
assertArrayEquals(payload, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun cuePregapAdvancesDataTrackStart() {
|
||||
val pregapPayload = sectorFilledWith(0x21)
|
||||
val dataPayload = sectorFilledWith(0x22)
|
||||
val image = tmp.newFile("pregap.bin")
|
||||
image.writeBytes(rawDataSector(pregapPayload, 0x01) + rawDataSector(dataPayload, 0x02))
|
||||
val metadata = """
|
||||
FILE "pregap.bin" BINARY
|
||||
TRACK 01 MODE1/2352
|
||||
INDEX 00 00:00:00
|
||||
INDEX 01 00:00:01
|
||||
""".trimIndent()
|
||||
|
||||
CdImageReader.openForFile(image, "pregap.cue", metadata.toByteArray()).use { reader ->
|
||||
val buffer = ByteArray(CdImageReader.ISO_SECTOR_SIZE)
|
||||
|
||||
reader.readSector(1, buffer)
|
||||
|
||||
assertArrayEquals(dataPayload, buffer)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun extractsCueAudioTrackAsWav() {
|
||||
val dataPayload = sectorFilledWith(0x33)
|
||||
val audio = ByteArray(RAW_AUDIO_SECTOR_SIZE) { it.toByte() }
|
||||
val image = tmp.newFile("audio.bin")
|
||||
image.writeBytes(rawDataSector(dataPayload, 0) + audio)
|
||||
val metadata = """
|
||||
FILE "audio.bin" BINARY
|
||||
TRACK 01 MODE1/2352
|
||||
INDEX 01 00:00:00
|
||||
TRACK 02 AUDIO
|
||||
INDEX 01 00:00:01
|
||||
""".trimIndent()
|
||||
|
||||
CdImageReader.openForFile(image, "audio.cue", metadata.toByteArray()).use { reader ->
|
||||
val output = ByteArrayOutputStream()
|
||||
|
||||
assertEquals(listOf(2), reader.audioTracks())
|
||||
reader.extractAudioTrack(2, output)
|
||||
|
||||
val wav = output.toByteArray()
|
||||
assertEquals("RIFF", wav.decodeAscii(0, 4))
|
||||
assertEquals(RAW_AUDIO_SECTOR_SIZE + 36, wav.readLeInt(4))
|
||||
assertEquals("WAVE", wav.decodeAscii(8, 4))
|
||||
assertEquals("data", wav.decodeAscii(36, 4))
|
||||
assertEquals(RAW_AUDIO_SECTOR_SIZE, wav.readLeInt(40))
|
||||
assertEquals(44100, wav.readLeInt(24))
|
||||
assertEquals(2, wav.readLeShort(22))
|
||||
assertEquals(16, wav.readLeShort(34))
|
||||
assertArrayEquals(audio, wav.copyOfRange(44, 44 + RAW_AUDIO_SECTOR_SIZE))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsUnsupportedCueMode() {
|
||||
val image = tmp.newFile("unsupported.bin")
|
||||
image.writeBytes(ByteArray(CdImageReader.ISO_SECTOR_SIZE))
|
||||
|
||||
assertInstallFailure(R.string.unsupported_cd_image) {
|
||||
CdImageReader.openForFile(image, "unsupported.cue", cue("MODE2/2352").toByteArray())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ccdUsesMode0ForAudioAndMode1PayloadOffsetForData() {
|
||||
val dataPayload = sectorFilledWith(0x52)
|
||||
val audio = ByteArray(RAW_AUDIO_SECTOR_SIZE) { (255 - it).toByte() }
|
||||
val image = tmp.newFile("disc.img")
|
||||
image.writeBytes(rawDataSector(dataPayload, 0x08) + audio)
|
||||
val metadata = """
|
||||
[TRACK 1]
|
||||
MODE=1
|
||||
INDEX 1=0
|
||||
[TRACK 2]
|
||||
MODE=0
|
||||
INDEX 1=1
|
||||
""".trimIndent()
|
||||
|
||||
CdImageReader.openForFile(image, "disc.ccd", metadata.toByteArray()).use { reader ->
|
||||
val buffer = ByteArray(CdImageReader.ISO_SECTOR_SIZE)
|
||||
val output = ByteArrayOutputStream()
|
||||
|
||||
reader.readSector(0, buffer)
|
||||
reader.extractAudioTrack(2, output)
|
||||
|
||||
assertArrayEquals(dataPayload, buffer)
|
||||
assertEquals(listOf(2), reader.audioTracks())
|
||||
assertArrayEquals(audio, output.toByteArray().copyOfRange(44, 44 + RAW_AUDIO_SECTOR_SIZE))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun mdsMapsModeAAToDataAndModeA9ToAudio() {
|
||||
val dataPayload = sectorFilledWith(0x61)
|
||||
val audio = ByteArray(RAW_AUDIO_SECTOR_SIZE) { (it * 3).toByte() }
|
||||
val image = tmp.newFile("disc.mdf")
|
||||
image.writeBytes(rawDataSector(dataPayload, 0x09) + audio)
|
||||
|
||||
CdImageReader.openForFile(image, "disc.mds", mds(track1Sectors = 1, track2Sectors = 1)).use { reader ->
|
||||
val buffer = ByteArray(CdImageReader.ISO_SECTOR_SIZE)
|
||||
val output = ByteArrayOutputStream()
|
||||
|
||||
reader.readSector(0, buffer)
|
||||
reader.extractAudioTrack(2, output)
|
||||
|
||||
assertArrayEquals(dataPayload, buffer)
|
||||
assertEquals(listOf(2), reader.audioTracks())
|
||||
assertArrayEquals(audio, output.toByteArray().copyOfRange(44, 44 + RAW_AUDIO_SECTOR_SIZE))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsMdsWithoutMediaDescriptorSignature() {
|
||||
val image = tmp.newFile("bad.mdf")
|
||||
image.writeBytes(ByteArray(RAW_AUDIO_SECTOR_SIZE))
|
||||
|
||||
assertInstallFailure(R.string.unsupported_cd_image) {
|
||||
CdImageReader.openForFile(image, "bad.mds", ByteArray(0x70))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GameInstallerCdImageTest {
|
||||
@get:Rule
|
||||
val tmp = TemporaryFolder()
|
||||
|
||||
@Test
|
||||
fun installsDataOnlyIso() = runBlocking {
|
||||
val root = tmp.newFolder("root")
|
||||
val iso = SyntheticIsoBuilder()
|
||||
.addGameDataFile("SA.ALD;1", "ald".toByteArray())
|
||||
.addGameDataFile("TITLE.TXT;1", "title".toByteArray())
|
||||
.writeTo(tmp.newFile("game.iso"))
|
||||
|
||||
CdImageReader.openForFile(iso).use { reader ->
|
||||
val gameDir = GameInstaller(GameStore(root)).installCdImageForTest(reader) {}
|
||||
|
||||
assertEquals(File(root, "0/GAMEDATA").canonicalFile, gameDir.canonicalFile)
|
||||
assertEquals("GAMEDATA", File(root, "0/${Launcher.GAMEDIR_FILE}").readText())
|
||||
assertEquals("ald", File(gameDir, "SA.ALD").readText())
|
||||
assertEquals("title", File(gameDir, "TITLE.TXT").readText())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun installsCueAudioTrackAndPlaylist() = runBlocking {
|
||||
val root = tmp.newFolder("root")
|
||||
val isoSector = SyntheticIsoBuilder()
|
||||
.addGameDataFile("SA.ALD;1", "ald".toByteArray())
|
||||
.toByteArray()
|
||||
val dataSectors = isoSector.toList().chunked(CdImageReader.ISO_SECTOR_SIZE)
|
||||
.joinToByteArray { rawDataSector(it.toByteArray(), 0) }
|
||||
val audio = ByteArray(RAW_AUDIO_SECTOR_SIZE) { (it and 0x7f).toByte() }
|
||||
val image = tmp.newFile("audio.bin")
|
||||
image.writeBytes(dataSectors + audio)
|
||||
val dataSectorCount = isoSector.size / CdImageReader.ISO_SECTOR_SIZE
|
||||
val metadata = """
|
||||
FILE "audio.bin" BINARY
|
||||
TRACK 01 MODE1/2352
|
||||
INDEX 01 00:00:00
|
||||
TRACK 02 AUDIO
|
||||
INDEX 01 ${dataSectorCount.toMsf()}
|
||||
""".trimIndent()
|
||||
|
||||
CdImageReader.openForFile(image, "audio.cue", metadata.toByteArray()).use { reader ->
|
||||
val gameDir = GameInstaller(GameStore(root)).installCdImageForTest(reader) {}
|
||||
|
||||
val wav = File(gameDir, "cdda/track02.wav")
|
||||
assertTrue(wav.exists())
|
||||
assertEquals("\ncdda/track02.wav", File(gameDir, Launcher.PLAYLIST_FILE).readText())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsIsoWithoutGameDataDirectoryAndDeletesIncompleteInstall() = runBlocking {
|
||||
val root = tmp.newFolder("root")
|
||||
val iso = SyntheticIsoBuilder(includeGameData = false).writeTo(tmp.newFile("nogamedata.iso"))
|
||||
|
||||
CdImageReader.openForFile(iso).use { reader ->
|
||||
assertInstallFailure(R.string.cannot_find_game_data_directory) {
|
||||
runBlocking {
|
||||
GameInstaller(GameStore(root)).installCdImageForTest(reader) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertFalse(File(root, "0").exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rejectsIsoWithoutAldAndDeletesIncompleteInstall() = runBlocking {
|
||||
val root = tmp.newFolder("root")
|
||||
val iso = SyntheticIsoBuilder()
|
||||
.addGameDataFile("README.TXT;1", "readme".toByteArray())
|
||||
.writeTo(tmp.newFile("noald.iso"))
|
||||
|
||||
CdImageReader.openForFile(iso).use { reader ->
|
||||
assertInstallFailure(R.string.cannot_find_ald) {
|
||||
runBlocking {
|
||||
GameInstaller(GameStore(root)).installCdImageForTest(reader) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assertFalse(File(root, "0").exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun resolveOutputPathRejectsTraversal() {
|
||||
val base = tmp.newFolder("out")
|
||||
|
||||
assertThrowsIOException {
|
||||
resolveOutputPath(base, "../evil")
|
||||
}
|
||||
assertThrowsIOException {
|
||||
resolveOutputPath(base, File(base.parentFile, "evil").absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class SyntheticIsoBuilder(
|
||||
private val includeGameData: Boolean = true,
|
||||
) {
|
||||
private val primaryFiles = mutableMapOf<String, ByteArray>()
|
||||
private val jolietFiles = mutableMapOf<String, ByteArray>()
|
||||
|
||||
fun addGameDataFile(path: String, content: ByteArray): SyntheticIsoBuilder {
|
||||
primaryFiles[path] = content
|
||||
return this
|
||||
}
|
||||
|
||||
fun addJolietGameDataFile(path: String, content: ByteArray): SyntheticIsoBuilder {
|
||||
jolietFiles[path] = content
|
||||
return this
|
||||
}
|
||||
|
||||
fun writeTo(file: File): File {
|
||||
file.writeBytes(toByteArray())
|
||||
return file
|
||||
}
|
||||
|
||||
fun toByteArray(): ByteArray {
|
||||
val sectors = MutableList(80) { ByteArray(CdImageReader.ISO_SECTOR_SIZE) }
|
||||
var nextSector = 30
|
||||
val primaryTree = buildTree(primaryFiles, Charset.forName("Shift_JIS"))
|
||||
val primaryRoot = writeTree(sectors, primaryTree, ROOT_SECTOR, GAMEDATA_SECTOR, nextSector, false)
|
||||
nextSector = primaryRoot.nextSector
|
||||
|
||||
writeVolumeDescriptor(sectors[16], 1, ROOT_SECTOR, CdImageReader.ISO_SECTOR_SIZE, false)
|
||||
var terminatorSector = 17
|
||||
if (jolietFiles.isNotEmpty()) {
|
||||
val jolietTree = buildTree(jolietFiles, Charsets.UTF_16BE)
|
||||
writeTree(sectors, jolietTree, JOLIET_ROOT_SECTOR, JOLIET_GAMEDATA_SECTOR, nextSector, true)
|
||||
writeVolumeDescriptor(sectors[17], 2, JOLIET_ROOT_SECTOR, CdImageReader.ISO_SECTOR_SIZE, true)
|
||||
terminatorSector = 18
|
||||
}
|
||||
writeTerminator(sectors[terminatorSector])
|
||||
return sectors.flattenToByteArray()
|
||||
}
|
||||
|
||||
private fun buildTree(files: Map<String, ByteArray>, charset: Charset): DirectoryNode {
|
||||
val root = DirectoryNode("")
|
||||
if (!includeGameData) {
|
||||
return root
|
||||
}
|
||||
val gameData = DirectoryNode("GAMEDATA")
|
||||
root.directories["GAMEDATA"] = gameData
|
||||
for ((path, content) in files) {
|
||||
var directory = gameData
|
||||
val parts = path.split("/")
|
||||
for (part in parts.dropLast(1)) {
|
||||
directory = directory.directories.getOrPut(part.substringBefore(";")) {
|
||||
DirectoryNode(part.substringBefore(";"))
|
||||
}
|
||||
}
|
||||
val rawName = parts.last().toByteArray(charset)
|
||||
directory.files.add(FileNode(rawName, content))
|
||||
}
|
||||
return root
|
||||
}
|
||||
|
||||
private fun writeTree(
|
||||
sectors: MutableList<ByteArray>,
|
||||
root: DirectoryNode,
|
||||
rootSector: Int,
|
||||
gameDataSector: Int,
|
||||
firstFileSector: Int,
|
||||
joliet: Boolean,
|
||||
): TreeWriteResult {
|
||||
var nextSector = firstFileSector
|
||||
root.extent = rootSector
|
||||
root.size = CdImageReader.ISO_SECTOR_SIZE
|
||||
if (includeGameData) {
|
||||
root.directories["GAMEDATA"]!!.extent = gameDataSector
|
||||
root.directories["GAMEDATA"]!!.size = CdImageReader.ISO_SECTOR_SIZE
|
||||
}
|
||||
assignDirectorySectors(root, gameDataSector + 1)
|
||||
nextSector = writeFileContents(sectors, root, nextSector)
|
||||
writeDirectorySector(sectors[rootSector], root, root, joliet)
|
||||
if (includeGameData) {
|
||||
writeDirectorySectors(sectors, root.directories["GAMEDATA"]!!, root, joliet)
|
||||
}
|
||||
return TreeWriteResult(nextSector)
|
||||
}
|
||||
|
||||
private fun assignDirectorySectors(directory: DirectoryNode, nextDirectorySector: Int): Int {
|
||||
var sector = nextDirectorySector
|
||||
for (child in directory.directories.values) {
|
||||
if (child.name == "GAMEDATA") {
|
||||
sector = assignDirectorySectors(child, sector)
|
||||
continue
|
||||
}
|
||||
child.extent = sector++
|
||||
child.size = CdImageReader.ISO_SECTOR_SIZE
|
||||
sector = assignDirectorySectors(child, sector)
|
||||
}
|
||||
return sector
|
||||
}
|
||||
|
||||
private fun writeFileContents(
|
||||
sectors: MutableList<ByteArray>,
|
||||
directory: DirectoryNode,
|
||||
firstFileSector: Int,
|
||||
): Int {
|
||||
var sector = firstFileSector
|
||||
for (child in directory.directories.values) {
|
||||
sector = writeFileContents(sectors, child, sector)
|
||||
}
|
||||
for (file in directory.files) {
|
||||
file.extent = sector
|
||||
file.size = file.content.size
|
||||
val sectorCount = (file.content.size + CdImageReader.ISO_SECTOR_SIZE - 1) /
|
||||
CdImageReader.ISO_SECTOR_SIZE
|
||||
for (i in 0 until sectorCount) {
|
||||
file.content.copyInto(
|
||||
sectors[sector + i],
|
||||
0,
|
||||
i * CdImageReader.ISO_SECTOR_SIZE,
|
||||
minOf(file.content.size, (i + 1) * CdImageReader.ISO_SECTOR_SIZE)
|
||||
)
|
||||
}
|
||||
sector += maxOf(1, sectorCount)
|
||||
}
|
||||
return sector
|
||||
}
|
||||
|
||||
private fun writeDirectorySectors(
|
||||
sectors: MutableList<ByteArray>,
|
||||
directory: DirectoryNode,
|
||||
parent: DirectoryNode,
|
||||
joliet: Boolean,
|
||||
) {
|
||||
writeDirectorySector(sectors[directory.extent], directory, parent, joliet)
|
||||
for (child in directory.directories.values) {
|
||||
writeDirectorySectors(sectors, child, directory, joliet)
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeDirectorySector(
|
||||
sector: ByteArray,
|
||||
directory: DirectoryNode,
|
||||
parent: DirectoryNode,
|
||||
joliet: Boolean,
|
||||
) {
|
||||
var offset = 0
|
||||
offset += writeRecord(sector, offset, byteArrayOf(0), directory.extent, directory.size, true)
|
||||
offset += writeRecord(sector, offset, byteArrayOf(1), parent.extent, parent.size, true)
|
||||
for (child in directory.directories.values) {
|
||||
val name = if (joliet) child.name.toByteArray(Charsets.UTF_16BE) else child.name.toByteArray()
|
||||
offset += writeRecord(sector, offset, name, child.extent, child.size, true)
|
||||
}
|
||||
for (file in directory.files) {
|
||||
offset += writeRecord(sector, offset, file.rawName, file.extent, file.size, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun writeVolumeDescriptor(
|
||||
sector: ByteArray,
|
||||
type: Int,
|
||||
rootExtent: Int,
|
||||
rootSize: Int,
|
||||
joliet: Boolean,
|
||||
) {
|
||||
sector[0] = type.toByte()
|
||||
"CD001".toByteArray(Charsets.US_ASCII).copyInto(sector, 1)
|
||||
sector[6] = 1
|
||||
if (joliet) {
|
||||
sector[88] = 0x25
|
||||
sector[89] = 0x2f
|
||||
sector[90] = 0x45
|
||||
}
|
||||
writeRecord(sector, 156, byteArrayOf(0), rootExtent, rootSize, true)
|
||||
}
|
||||
|
||||
private fun writeTerminator(sector: ByteArray) {
|
||||
sector[0] = 255.toByte()
|
||||
"CD001".toByteArray(Charsets.US_ASCII).copyInto(sector, 1)
|
||||
sector[6] = 1
|
||||
}
|
||||
|
||||
private fun writeRecord(
|
||||
buffer: ByteArray,
|
||||
offset: Int,
|
||||
rawName: ByteArray,
|
||||
extent: Int,
|
||||
size: Int,
|
||||
isDirectory: Boolean,
|
||||
): Int {
|
||||
val length = 33 + rawName.size + if (rawName.size % 2 == 0) 1 else 0
|
||||
buffer[offset] = length.toByte()
|
||||
buffer.writeLeInt(offset + 2, extent)
|
||||
buffer.writeLeInt(offset + 10, size)
|
||||
buffer[offset + 25] = if (isDirectory) 0x02 else 0x00
|
||||
buffer[offset + 32] = rawName.size.toByte()
|
||||
rawName.copyInto(buffer, offset + 33)
|
||||
return length
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ROOT_SECTOR = 20
|
||||
private const val GAMEDATA_SECTOR = 21
|
||||
private const val JOLIET_ROOT_SECTOR = 24
|
||||
private const val JOLIET_GAMEDATA_SECTOR = 25
|
||||
}
|
||||
}
|
||||
|
||||
private data class DirectoryNode(
|
||||
val name: String,
|
||||
val directories: MutableMap<String, DirectoryNode> = linkedMapOf(),
|
||||
val files: MutableList<FileNode> = mutableListOf(),
|
||||
var extent: Int = 0,
|
||||
var size: Int = 0,
|
||||
)
|
||||
|
||||
private data class FileNode(
|
||||
val rawName: ByteArray,
|
||||
val content: ByteArray,
|
||||
var extent: Int = 0,
|
||||
var size: Int = 0,
|
||||
)
|
||||
|
||||
private data class TreeWriteResult(val nextSector: Int)
|
||||
|
||||
private const val RAW_AUDIO_SECTOR_SIZE = 2352
|
||||
|
||||
private fun cue(mode: String): String = """
|
||||
FILE "image.bin" BINARY
|
||||
TRACK 01 $mode
|
||||
INDEX 01 00:00:00
|
||||
""".trimIndent()
|
||||
|
||||
private fun mds(track1Sectors: Int, track2Sectors: Int): ByteArray {
|
||||
val bytes = ByteArray(0x70 + 2 * 0x50 + 2 * 8)
|
||||
"MEDIA DESCRIPTOR".toByteArray(Charsets.US_ASCII).copyInto(bytes, 0)
|
||||
bytes[0x62] = 2
|
||||
val track1 = 0x70
|
||||
bytes[track1] = 0xaa.toByte()
|
||||
bytes[track1 + 0x04] = 1
|
||||
bytes.writeLeShort(track1 + 0x10, RAW_AUDIO_SECTOR_SIZE)
|
||||
bytes.writeLeInt(track1 + 0x28, 0)
|
||||
bytes.writeLeInt(0x70 + 2 * 0x50 + 0x04, track1Sectors)
|
||||
val track2 = 0x70 + 0x50
|
||||
bytes[track2] = 0xa9.toByte()
|
||||
bytes[track2 + 0x04] = 2
|
||||
bytes.writeLeShort(track2 + 0x10, RAW_AUDIO_SECTOR_SIZE)
|
||||
bytes.writeLeInt(track2 + 0x28, track1Sectors * RAW_AUDIO_SECTOR_SIZE)
|
||||
bytes.writeLeInt(0x70 + 2 * 0x50 + 8 + 0x04, track2Sectors)
|
||||
return bytes
|
||||
}
|
||||
|
||||
private fun sectorFilledWith(value: Int): ByteArray {
|
||||
return ByteArray(CdImageReader.ISO_SECTOR_SIZE) { value.toByte() }
|
||||
}
|
||||
|
||||
private fun rawDataSector(payload: ByteArray, prefixValue: Int): ByteArray {
|
||||
val sector = ByteArray(RAW_AUDIO_SECTOR_SIZE) { prefixValue.toByte() }
|
||||
payload.copyInto(sector, 16, 0, CdImageReader.ISO_SECTOR_SIZE)
|
||||
return sector
|
||||
}
|
||||
|
||||
private fun Int.toMsf(): String {
|
||||
val minutes = this / (60 * 75)
|
||||
val seconds = this / 75 % 60
|
||||
val frames = this % 75
|
||||
return "%02d:%02d:%02d".format(minutes, seconds, frames)
|
||||
}
|
||||
|
||||
private fun Iterable<Byte>.toByteArray(): ByteArray {
|
||||
val result = ByteArray(count())
|
||||
forEachIndexed { index, byte -> result[index] = byte }
|
||||
return result
|
||||
}
|
||||
|
||||
private fun List<ByteArray>.flattenToByteArray(): ByteArray {
|
||||
val result = ByteArray(sumOf { it.size })
|
||||
var offset = 0
|
||||
for (bytes in this) {
|
||||
bytes.copyInto(result, offset)
|
||||
offset += bytes.size
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun List<List<Byte>>.joinToByteArray(transform: (List<Byte>) -> ByteArray): ByteArray {
|
||||
return map(transform).flattenToByteArray()
|
||||
}
|
||||
|
||||
private fun ByteArray.decodeAscii(offset: Int, length: Int): String {
|
||||
return String(this, offset, length, Charsets.US_ASCII)
|
||||
}
|
||||
|
||||
private fun ByteArray.readLeShort(offset: Int): Int {
|
||||
return (this[offset].toInt() and 0xff) or ((this[offset + 1].toInt() and 0xff) shl 8)
|
||||
}
|
||||
|
||||
private fun ByteArray.readLeInt(offset: Int): Int {
|
||||
return readLeShort(offset) or (readLeShort(offset + 2) shl 16)
|
||||
}
|
||||
|
||||
private fun ByteArray.writeLeShort(offset: Int, value: Int) {
|
||||
this[offset] = value.toByte()
|
||||
this[offset + 1] = (value shr 8).toByte()
|
||||
}
|
||||
|
||||
private fun ByteArray.writeLeInt(offset: Int, value: Int) {
|
||||
writeLeShort(offset, value)
|
||||
writeLeShort(offset + 2, value shr 16)
|
||||
}
|
||||
|
||||
private fun assertInstallFailure(expectedMsgId: Int, block: () -> Unit): InstallFailureException {
|
||||
try {
|
||||
block()
|
||||
} catch (e: InstallFailureException) {
|
||||
assertEquals(expectedMsgId, e.msgId)
|
||||
return e
|
||||
}
|
||||
fail("Expected InstallFailureException")
|
||||
throw AssertionError()
|
||||
}
|
||||
|
||||
private fun assertThrowsIOException(block: () -> Unit) {
|
||||
try {
|
||||
block()
|
||||
} catch (e: java.io.IOException) {
|
||||
return
|
||||
}
|
||||
fail("Expected IOException")
|
||||
}
|
||||
+4
-4
@@ -1,17 +1,17 @@
|
||||
#cmakedefine PACKAGE "@PACKAGE@"
|
||||
#define VERSION "@XSYSTEM35_VERSION@"
|
||||
|
||||
#cmakedefine CACHE_TOTALSIZE @CACHE_TOTALSIZE@
|
||||
#ifndef XSYSTEM35_SDL_VERSION
|
||||
#define XSYSTEM35_SDL_VERSION @XSYSTEM35_SDL_VERSION@
|
||||
#endif
|
||||
|
||||
#cmakedefine ENABLE_DEBUGGER @ENABLE_DEBUGGER@
|
||||
#cmakedefine ENABLE_GTK @ENABLE_GTK@
|
||||
#cmakedefine ENABLE_MIDI_SDLMIXER @ENABLE_MIDI_SDLMIXER@
|
||||
#cmakedefine ENABLE_MIDI_PORTMIDI @ENABLE_MIDI_PORTMIDI@
|
||||
#cmakedefine ENABLE_NLS @ENABLE_NLS@
|
||||
|
||||
#cmakedefine DEFAULT_PLAYLIST_PATH "@DEFAULT_PLAYLIST_PATH@"
|
||||
#cmakedefine JOY_DEVICE "@JOY_DEVICE@"
|
||||
|
||||
#cmakedefine HAVE_LIBINTL @HAVE_LIBINTL@
|
||||
#cmakedefine HAVE_GETLOGIN @HAVE_GETLOGIN@
|
||||
#cmakedefine HAVE_MMAP @HAVE_MMAP@
|
||||
#cmakedefine HAVE_SIGACTION @HAVE_SIGACTION@
|
||||
|
||||
@@ -98,6 +98,9 @@ finds System 3.x game files (*.ALD) from the current directory.
|
||||
*-noimagecursor*::
|
||||
Disable custom mouse cursor images.
|
||||
|
||||
*-mute_on_unfocus*::
|
||||
Mute audio while the game window is not in focus.
|
||||
|
||||
*-version*::
|
||||
Print the version number and exit.
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
Copyright (c) 2024 rxi
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
of the Software, and to permit persons to whom the Software is furnished to do
|
||||
so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -12,8 +12,8 @@ or in your `.xsys35rc` file. For example:
|
||||
xsystem35 -censor misc/censor/kichikuou.txt
|
||||
```
|
||||
|
||||
The list files contain image numbers (integers), one per line. Lines starting
|
||||
with `#` are treated as comments.
|
||||
The list files contain image numbers (integers), one per line. A `#` starts a
|
||||
comment that continues to the end of the line.
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# Censor list for ランス -光をもとめて- (Rance: The Quest for Hikari)
|
||||
# Images marked with a * might be allowed in an 18+ stream.
|
||||
5
|
||||
6
|
||||
8
|
||||
22
|
||||
26
|
||||
27
|
||||
29
|
||||
32
|
||||
39
|
||||
40
|
||||
41
|
||||
51
|
||||
56
|
||||
57
|
||||
75
|
||||
76
|
||||
77
|
||||
78 # *
|
||||
80
|
||||
82
|
||||
89
|
||||
91
|
||||
96 # *
|
||||
97 # *
|
||||
98
|
||||
99
|
||||
101
|
||||
208
|
||||
@@ -0,0 +1,59 @@
|
||||
# Censor list for ランスII (Rance II)
|
||||
# Images marked with a * might be allowed in an 18+ stream.
|
||||
26
|
||||
39 # *
|
||||
40
|
||||
41
|
||||
77
|
||||
78
|
||||
86
|
||||
98
|
||||
103
|
||||
104
|
||||
121
|
||||
122
|
||||
123
|
||||
124
|
||||
130
|
||||
131
|
||||
132
|
||||
133
|
||||
134
|
||||
140
|
||||
141
|
||||
142
|
||||
148
|
||||
156
|
||||
157
|
||||
158
|
||||
159
|
||||
161
|
||||
166
|
||||
171
|
||||
172
|
||||
185
|
||||
187
|
||||
191
|
||||
195
|
||||
197
|
||||
200
|
||||
201
|
||||
234
|
||||
236
|
||||
245
|
||||
246
|
||||
247
|
||||
272
|
||||
274
|
||||
275
|
||||
276
|
||||
278
|
||||
280
|
||||
281
|
||||
282
|
||||
284
|
||||
289
|
||||
292
|
||||
299
|
||||
300
|
||||
302
|
||||
@@ -0,0 +1,121 @@
|
||||
# Censor list for Rance 3
|
||||
# Images marked with a * might be allowed in an 18+ stream.
|
||||
1 # *
|
||||
2 # *
|
||||
3
|
||||
6
|
||||
8
|
||||
9
|
||||
32 # *
|
||||
53 # *
|
||||
54
|
||||
70
|
||||
71
|
||||
72
|
||||
73
|
||||
75
|
||||
77 # *
|
||||
78
|
||||
79
|
||||
112 # *
|
||||
113
|
||||
114
|
||||
115
|
||||
116
|
||||
117
|
||||
118 # *
|
||||
150
|
||||
151
|
||||
152
|
||||
153
|
||||
154
|
||||
155
|
||||
166
|
||||
167
|
||||
180
|
||||
181
|
||||
183
|
||||
184
|
||||
185
|
||||
186
|
||||
187
|
||||
188
|
||||
189
|
||||
190
|
||||
195
|
||||
196
|
||||
208
|
||||
209
|
||||
210
|
||||
211
|
||||
212
|
||||
213
|
||||
215
|
||||
216
|
||||
217
|
||||
218
|
||||
219
|
||||
231
|
||||
232
|
||||
233
|
||||
234
|
||||
235
|
||||
236
|
||||
238
|
||||
239
|
||||
260
|
||||
261
|
||||
263
|
||||
264
|
||||
265
|
||||
266
|
||||
268
|
||||
269
|
||||
330
|
||||
331
|
||||
332
|
||||
334
|
||||
335
|
||||
336
|
||||
337
|
||||
338
|
||||
339
|
||||
420
|
||||
421
|
||||
422
|
||||
423
|
||||
424
|
||||
425
|
||||
426
|
||||
427
|
||||
428
|
||||
429
|
||||
440
|
||||
441
|
||||
442
|
||||
443
|
||||
444
|
||||
445
|
||||
447
|
||||
473
|
||||
474
|
||||
475
|
||||
476
|
||||
477
|
||||
479
|
||||
519 # *
|
||||
521 # *
|
||||
523
|
||||
524
|
||||
525
|
||||
526
|
||||
530
|
||||
533 # *
|
||||
534
|
||||
535
|
||||
536
|
||||
537
|
||||
570
|
||||
571
|
||||
572
|
||||
581
|
||||
@@ -0,0 +1,168 @@
|
||||
# Censor list for Rance IV
|
||||
# Images marked with a * might be allowed in an 18+ stream.
|
||||
1
|
||||
2
|
||||
10
|
||||
20
|
||||
26
|
||||
28
|
||||
29
|
||||
30 # *
|
||||
31
|
||||
32
|
||||
33
|
||||
34
|
||||
35
|
||||
36
|
||||
37
|
||||
38
|
||||
39 # *
|
||||
40
|
||||
41
|
||||
42
|
||||
43
|
||||
44
|
||||
45
|
||||
46
|
||||
48
|
||||
49
|
||||
75
|
||||
76
|
||||
77
|
||||
78
|
||||
79
|
||||
122
|
||||
126
|
||||
140
|
||||
142
|
||||
148
|
||||
149
|
||||
162 # *
|
||||
163
|
||||
164
|
||||
165 # *
|
||||
166
|
||||
167
|
||||
168
|
||||
169 # *
|
||||
170
|
||||
171
|
||||
172
|
||||
176
|
||||
177
|
||||
178
|
||||
185
|
||||
186
|
||||
188
|
||||
189
|
||||
205
|
||||
207
|
||||
217 # *
|
||||
220
|
||||
221
|
||||
222
|
||||
223
|
||||
228
|
||||
241
|
||||
242
|
||||
251
|
||||
252
|
||||
253
|
||||
254
|
||||
260
|
||||
261
|
||||
262 # *
|
||||
264 # *
|
||||
265
|
||||
266
|
||||
267
|
||||
268
|
||||
269
|
||||
280
|
||||
281
|
||||
283
|
||||
284
|
||||
285
|
||||
286 # *
|
||||
287
|
||||
288
|
||||
292
|
||||
294
|
||||
299
|
||||
370
|
||||
371
|
||||
372
|
||||
373
|
||||
376
|
||||
378
|
||||
382
|
||||
384
|
||||
385 # *
|
||||
386
|
||||
387
|
||||
388
|
||||
389
|
||||
414 # *
|
||||
416
|
||||
428 # *
|
||||
531 # *
|
||||
532
|
||||
533
|
||||
534 # *
|
||||
535
|
||||
550
|
||||
551
|
||||
552
|
||||
553
|
||||
554
|
||||
555
|
||||
562 # *
|
||||
565
|
||||
615
|
||||
625
|
||||
626
|
||||
711
|
||||
723
|
||||
1055
|
||||
1056
|
||||
1057
|
||||
1058
|
||||
1059
|
||||
1060
|
||||
1061
|
||||
1062
|
||||
1545 # *
|
||||
1546 # *
|
||||
1547 # *
|
||||
1548 # *
|
||||
1549 # *
|
||||
1653 # *
|
||||
1654 # *
|
||||
1655 # *
|
||||
1656 # *
|
||||
1657 # *
|
||||
1658 # *
|
||||
1659 # *
|
||||
1660 # *
|
||||
1661 # *
|
||||
1662 # *
|
||||
1663 # *
|
||||
1664 # *
|
||||
1665 # *
|
||||
1728
|
||||
1729
|
||||
2004
|
||||
2005
|
||||
2006
|
||||
2007
|
||||
2008
|
||||
2009
|
||||
2010
|
||||
2011
|
||||
2012
|
||||
2013
|
||||
2014
|
||||
2015
|
||||
2016
|
||||
2017
|
||||
2018
|
||||
@@ -12,7 +12,7 @@ static void Init() {
|
||||
int p1 = getCaliValue(); /* ISys3x */
|
||||
int p2 = getCaliValue(); /* IWinMsg */
|
||||
int p3 = getCaliValue(); /* ITimer */
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
*var = 1;
|
||||
|
||||
|
||||
@@ -84,7 +84,7 @@ endif()
|
||||
|
||||
target_compile_options(modules PRIVATE -Wno-pointer-sign)
|
||||
|
||||
target_link_libraries(modules PRIVATE sdl2 zlib)
|
||||
if (TARGET sdl2_mixer)
|
||||
target_link_libraries(modules PRIVATE sdl2_mixer)
|
||||
target_link_libraries(modules PRIVATE xsystem35_sdl zlib)
|
||||
if (TARGET xsystem35_sdl_mixer)
|
||||
target_link_libraries(modules PRIVATE xsystem35_sdl_mixer)
|
||||
endif()
|
||||
|
||||
@@ -15,8 +15,8 @@ static void Init() {
|
||||
|
||||
static void ExistKeyFile() {
|
||||
const char *p1 = sl_getString(0);
|
||||
int *p2 = getCaliVariable();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *p2 = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
*var = 1;
|
||||
|
||||
@@ -25,8 +25,8 @@ static void ExistKeyFile() {
|
||||
|
||||
static void CheckProtectFile() {
|
||||
const char *p1 = sl_getString(0);
|
||||
int *p2 = getCaliVariable();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *p2 = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
*var = 1;
|
||||
|
||||
@@ -35,8 +35,8 @@ static void CheckProtectFile() {
|
||||
|
||||
static void CreateKeyFile() {
|
||||
const char *p1 = sl_getString(0);
|
||||
int *p2 = getCaliVariable();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *p2 = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
*var = 1;
|
||||
|
||||
|
||||
+13
-13
@@ -135,7 +135,7 @@ static void Create() {
|
||||
height: surface の高さ
|
||||
bpp : surface の深さ(24bppのみサポート)
|
||||
*/
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
int width = getCaliValue();
|
||||
int height = getCaliValue();
|
||||
int bpp = getCaliValue();
|
||||
@@ -162,7 +162,7 @@ static void CreatePixelOnly() {
|
||||
height: surface の高さ
|
||||
bpp : surface の深さ(24bpp only)
|
||||
*/
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
int width = getCaliValue();
|
||||
int height = getCaliValue();
|
||||
int bpp = getCaliValue();
|
||||
@@ -188,7 +188,7 @@ static void CreateAMapOnly() {
|
||||
width : surface の幅
|
||||
height: surface の高さ
|
||||
*/
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
int width = getCaliValue();
|
||||
int height = getCaliValue();
|
||||
surface_t *s;
|
||||
@@ -214,7 +214,7 @@ static void IsSurface() {
|
||||
var : 結果を返す変数。surface ならば 1, !surface ならば 0
|
||||
*/
|
||||
int p1 = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
*var = sf_get(p1) ? 1 : 0;
|
||||
|
||||
@@ -229,7 +229,7 @@ static void IsPixel() {
|
||||
var : 結果を返す変数。pixel ならば 1, !pixel ならば 0
|
||||
*/
|
||||
int p1 = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
surface_t *s;
|
||||
|
||||
s = sf_get(p1);
|
||||
@@ -251,7 +251,7 @@ static void IsAlpha() {
|
||||
var : 結果を返す変数。alpha ならば 1, !alpha ならば 0
|
||||
*/
|
||||
int p1 = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
surface_t *s;
|
||||
|
||||
s = sf_get(p1);
|
||||
@@ -273,7 +273,7 @@ static void GetWidth() {
|
||||
var : 結果を返す変数。
|
||||
*/
|
||||
int p1 = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
surface_t *s;
|
||||
|
||||
s = sf_get(p1);
|
||||
@@ -295,7 +295,7 @@ static void GetHeight() {
|
||||
var : 結果を返す変数。
|
||||
*/
|
||||
int p1 = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
surface_t *s;
|
||||
|
||||
s = sf_get(p1);
|
||||
@@ -310,7 +310,7 @@ static void GetHeight() {
|
||||
}
|
||||
|
||||
static void GetCreatedSurface() { /* not used ? */
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
TRACE_UNIMPLEMENTED("Gpx.GetCreatedSurface %p:", var);
|
||||
}
|
||||
@@ -322,7 +322,7 @@ static void LoadCG() {
|
||||
var : 作成した surface の番号を返す変数
|
||||
p1 : 読み込む CG の番号
|
||||
*/
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
int p1 = getCaliValue();
|
||||
|
||||
*var = load_cg_main(p1 -1);
|
||||
@@ -331,14 +331,14 @@ static void LoadCG() {
|
||||
}
|
||||
|
||||
static void GetCGPosX() { /* not useed ? */
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
int p1 = getCaliValue();
|
||||
|
||||
TRACE_UNIMPLEMENTED("Gpx.GetCgPosX %p,%d:", var, p1);
|
||||
}
|
||||
|
||||
static void GetCGPosY() { /* not useed ? */
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
int p1 = getCaliValue();
|
||||
|
||||
TRACE_UNIMPLEMENTED("Gpx.GetCgPosY %p,%d:", var, p1);
|
||||
@@ -1117,7 +1117,7 @@ static void EffectCopy() {
|
||||
int sw = getCaliValue();
|
||||
int sh = getCaliValue();
|
||||
int time = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
TRACE("Gpx.EffectCopy %d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%p:", no, dx, dy, ss1, sx1, sy1, ss2, sx2, sy2, sw, sh, time, var);
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ void gpx_effect(int no,
|
||||
surface_t *src, int sx, int sy,
|
||||
int width, int height,
|
||||
int time,
|
||||
int *endtype) {
|
||||
vmvar_t *endtype) {
|
||||
surface_t *write = nact->ags.dib;
|
||||
if (!gr_clip(dst, &dx, &dy, &width, &height, write, &wx, &wy)) return;
|
||||
if (!gr_clip(src, &sx, &sy, &width, &height, write, &wx, &wy)) return;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#ifndef __GLEFFECTCOPY_H__
|
||||
#define __GLEFFECTCOPY_H__
|
||||
|
||||
#include "portab.h"
|
||||
#include "surface.h"
|
||||
|
||||
extern void gpx_effect(int no,
|
||||
@@ -9,6 +10,6 @@ extern void gpx_effect(int no,
|
||||
surface_t *src, int sx, int sy,
|
||||
int width, int height,
|
||||
int time,
|
||||
int *endtype);
|
||||
vmvar_t *endtype);
|
||||
|
||||
#endif /* __GLEFFECTCOPY_H__ */
|
||||
|
||||
+9
-8
@@ -3,7 +3,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
@@ -76,12 +76,13 @@ void gr_copy(surface_t *dst, int dx, int dy, surface_t *src, int sx, int sy, int
|
||||
SDL_Rect r;
|
||||
if (SDL_IntersectRect(&src_rect, &dst_rect, &r)) {
|
||||
SDL_Surface *view = gfx_createSurfaceView(src->sdl_surface, sx, sy, sw, sh);
|
||||
SDL_Surface *tmp = SDL_ConvertSurface(view, dst->sdl_surface->format, 0);
|
||||
SDL_Surface *tmp = sdl_convert_surface(
|
||||
view, sdl_surface_format(dst->sdl_surface));
|
||||
src_rect.x = 0;
|
||||
src_rect.y = 0;
|
||||
SDL_LowerBlit(tmp, &src_rect, dst->sdl_surface, &dst_rect);
|
||||
SDL_FreeSurface(tmp);
|
||||
SDL_FreeSurface(view);
|
||||
sdl_destroy_surface(tmp);
|
||||
sdl_destroy_surface(view);
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -99,7 +100,7 @@ void gr_copy_bright(surface_t *dst, int dx, int dy, surface_t *src, int sx, int
|
||||
|
||||
void gr_fill(surface_t *dst, int dx, int dy, int dw, int dh, int r, int g, int b) {
|
||||
SDL_Rect rect = {dx, dy, dw, dh};
|
||||
uint32_t color = SDL_MapRGB(dst->sdl_surface->format, r, g, b);
|
||||
uint32_t color = sdl_map_rgb(dst->sdl_surface, r, g, b);
|
||||
SDL_FillRect(dst->sdl_surface, &rect, color);
|
||||
}
|
||||
|
||||
@@ -227,12 +228,12 @@ void gr_copy_stretch_blend_alpha_map(surface_t *dst, int dx, int dy, int dw, int
|
||||
|
||||
// Fill the rectangle with the specified color (rgb) and blend rate (lv)
|
||||
void gr_fill_alpha_color(surface_t *dst, int dx, int dy, int dw, int dh, int r, int g, int b, int lv) {
|
||||
SDL_Surface *src = SDL_CreateRGBSurfaceWithFormat(0, dw, dh, 32, SDL_PIXELFORMAT_ARGB8888);
|
||||
SDL_FillRect(src, NULL, SDL_MapRGBA(src->format, r, g, b, lv));
|
||||
SDL_Surface *src = sdl_create_surface(dw, dh, 32, SDL_PIXELFORMAT_ARGB8888);
|
||||
SDL_FillRect(src, NULL, sdl_map_rgba(src, r, g, b, lv));
|
||||
SDL_SetSurfaceBlendMode(src, SDL_BLENDMODE_BLEND);
|
||||
SDL_Rect dstrect = {dx, dy, dw, dh};
|
||||
SDL_BlitSurface(src, NULL, dst->sdl_surface, &dstrect);
|
||||
SDL_FreeSurface(src);
|
||||
sdl_destroy_surface(src);
|
||||
}
|
||||
|
||||
#include "graph2.c"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "surface.h"
|
||||
@@ -15,7 +15,8 @@ static surface_t *create(int width, int height, bool has_pixel, bool has_alpha)
|
||||
s->height = height;
|
||||
|
||||
if (has_pixel) {
|
||||
s->sdl_surface = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_RGB888);
|
||||
s->sdl_surface = sdl_create_surface(
|
||||
width, height, 32, SDL_PIXELFORMAT_RGB888);
|
||||
}
|
||||
|
||||
if (has_alpha) {
|
||||
@@ -47,7 +48,7 @@ surface_t *sf_create_pixel(int width, int height) {
|
||||
*/
|
||||
void sf_free(surface_t *s) {
|
||||
if (s == NULL) return;
|
||||
if (s->sdl_surface) SDL_FreeSurface(s->sdl_surface);
|
||||
if (s->sdl_surface) sdl_destroy_surface(s->sdl_surface);
|
||||
if (s->alpha) free(s->alpha);
|
||||
free(s);
|
||||
}
|
||||
@@ -67,7 +68,8 @@ surface_t *sf_dup(surface_t *in) {
|
||||
memcpy(sf, in, sizeof(surface_t));
|
||||
|
||||
if (in->sdl_surface) {
|
||||
sf->sdl_surface = SDL_ConvertSurface(in->sdl_surface, in->sdl_surface->format, 0);
|
||||
sf->sdl_surface = sdl_convert_surface(
|
||||
in->sdl_surface, sdl_surface_format(in->sdl_surface));
|
||||
}
|
||||
|
||||
if (in->alpha) {
|
||||
|
||||
+2
-2
@@ -53,7 +53,7 @@ static void RandMTGet() {
|
||||
var: 結果を返す変数
|
||||
*/
|
||||
int num = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
if (num == 0 || num == 1) {
|
||||
*var = num;
|
||||
@@ -83,7 +83,7 @@ static void RandMTGetNumTable() {
|
||||
|
||||
var: 乱数を格納する変数
|
||||
*/
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
*var = (int)(genrand() * numtblmax) + 1;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ static bool valid;
|
||||
static int action;
|
||||
|
||||
static void Init() {
|
||||
int *p1 = getCaliVariable();
|
||||
vmvar_t *p1 = getCaliVariable();
|
||||
int p2 = getCaliValue(); /* ISys3x */
|
||||
int p3 = getCaliValue();
|
||||
int p4 = getCaliValue();
|
||||
@@ -30,7 +30,7 @@ static void Init() {
|
||||
}
|
||||
|
||||
static void Start() {
|
||||
int *p1 = getCaliVariable();
|
||||
vmvar_t *p1 = getCaliVariable();
|
||||
int p2 = getCaliValue();
|
||||
|
||||
char *fname_utf8 = sjis2utf(svar_get(p2));
|
||||
@@ -52,7 +52,7 @@ static void SetValid() {
|
||||
}
|
||||
|
||||
static void GetValid() {
|
||||
int *p1 = getCaliVariable();
|
||||
vmvar_t *p1 = getCaliVariable();
|
||||
|
||||
*p1 = valid;
|
||||
|
||||
@@ -68,7 +68,7 @@ static void SetAction() {
|
||||
}
|
||||
|
||||
static void GetAction() {
|
||||
int *p1 = getCaliVariable();
|
||||
vmvar_t *p1 = getCaliVariable();
|
||||
|
||||
*p1 = action;
|
||||
|
||||
|
||||
+25
-26
@@ -22,7 +22,7 @@ night_t nightprv;
|
||||
|
||||
|
||||
static void Init(void) { /* 0 */
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
int p1 = getCaliValue(); /* ISys3xCG */
|
||||
int p2 = getCaliValue(); /* ISys3xDIB */
|
||||
int p3 = getCaliValue(); /* ISys3xMsgString */
|
||||
@@ -56,8 +56,7 @@ static void InitGame() { /* 1 */
|
||||
|
||||
nact->msgout = ntmsg_add;
|
||||
nact->ags.eventcb = ntev_callback;
|
||||
nact->callback = ntev_main;
|
||||
|
||||
|
||||
nt_gr_init();
|
||||
ntmsg_init();
|
||||
nt_sstr_init();
|
||||
@@ -214,7 +213,7 @@ static void SetSelMode(void) { /* 18 */
|
||||
|
||||
// キー入力待ち後、改ページ
|
||||
static void AnalyzeMessage(void) { /* 19 */
|
||||
int *var = getCaliVariable(); // 入力されたキー
|
||||
vmvar_t *var = getCaliVariable(); // 入力されたキー
|
||||
|
||||
*var = ntmsg_ana();
|
||||
|
||||
@@ -298,7 +297,7 @@ static void ScreenCG(void) { /* 27 */
|
||||
}
|
||||
|
||||
static void RunGameMain(void) { /* 28 */
|
||||
int *p1 = getCaliVariable(); // result
|
||||
vmvar_t *p1 = getCaliVariable(); // result
|
||||
int p2 = getCaliValue(); // month
|
||||
int p3 = getCaliValue(); // day
|
||||
int p4 = getCaliValue(); // day of week
|
||||
@@ -314,7 +313,7 @@ static void RunGameMain(void) { /* 28 */
|
||||
}
|
||||
|
||||
static void CheckNewGame(void) { /* 29 */
|
||||
int *p1 = getCaliVariable();
|
||||
vmvar_t *p1 = getCaliVariable();
|
||||
|
||||
*p1 = 0;
|
||||
|
||||
@@ -348,9 +347,9 @@ static void SetDate(void) { /* 33 */
|
||||
}
|
||||
|
||||
static void GetDate(void) { /* 34 */
|
||||
int *p1 = getCaliVariable(); // month
|
||||
int *p2 = getCaliVariable(); // day
|
||||
int *p3 = getCaliVariable(); // day of weeek
|
||||
vmvar_t *p1 = getCaliVariable(); // month
|
||||
vmvar_t *p2 = getCaliVariable(); // day
|
||||
vmvar_t *p3 = getCaliVariable(); // day of weeek
|
||||
|
||||
*p1 = night.Month;
|
||||
*p2 = night.Day;
|
||||
@@ -364,7 +363,7 @@ static void SelectGameLevel(void) { /* 35 */
|
||||
}
|
||||
|
||||
static void RunEventDungeon(void) { /* 36 */
|
||||
int *p1 = getCaliVariable();
|
||||
vmvar_t *p1 = getCaliVariable();
|
||||
int p2 = getCaliValue();
|
||||
|
||||
*p1 = 1;
|
||||
@@ -479,7 +478,7 @@ static void RunSoundMode(void) { /* 48 */
|
||||
}
|
||||
|
||||
static void RunMapEditor(void) { /* 49 */
|
||||
int *p1 = getCaliVariable();
|
||||
vmvar_t *p1 = getCaliVariable();
|
||||
|
||||
TRACE_UNIMPLEMENTED("NIGHTDLL.RunMapEditor %p:", p1);
|
||||
}
|
||||
@@ -496,21 +495,21 @@ static void VisualListAdd(void) { /* 51 */
|
||||
}
|
||||
|
||||
static void GetLocalCountCG(void) { /* 52 */
|
||||
int *p1 = getCaliVariable();
|
||||
vmvar_t *p1 = getCaliVariable();
|
||||
int p2 = getCaliValue();
|
||||
|
||||
TRACE_UNIMPLEMENTED("NIGHTDLL.GetLocalCountCG %p,%d:", p1, p2);
|
||||
}
|
||||
|
||||
static void PlayMemory(void) { /* 53 */
|
||||
int *p1 = getCaliVariable(); // 回想ページ
|
||||
int *p2 = getCaliVariable(); // 回想RESULT
|
||||
vmvar_t *p1 = getCaliVariable(); // 回想ページ
|
||||
vmvar_t *p2 = getCaliVariable(); // 回想RESULT
|
||||
|
||||
TRACE_UNIMPLEMENTED("NIGHTDLL.PlayMemory %p,%p:", p1, p2);
|
||||
}
|
||||
|
||||
static void GetEventFlagTotal(void) { /* 54 */
|
||||
int *p1 = getCaliVariable();
|
||||
vmvar_t *p1 = getCaliVariable();
|
||||
int p2 = getCaliValue();
|
||||
|
||||
TRACE_UNIMPLEMENTED("NIGHTDLL.GetEventFlagTotal %p,%d:", p1, p2);
|
||||
@@ -529,25 +528,25 @@ static void GetPlayerName(void) { /* 56 */
|
||||
}
|
||||
|
||||
static void SaveGame(void) { /* 57 */
|
||||
int *p1 = getCaliVariable();
|
||||
vmvar_t *p1 = getCaliVariable();
|
||||
|
||||
TRACE_UNIMPLEMENTED("NIGHTDLL.SaveGame %p:", p1);
|
||||
}
|
||||
|
||||
static void LoadGame(void) { /* 58 */
|
||||
int *p1 = getCaliVariable();
|
||||
vmvar_t *p1 = getCaliVariable();
|
||||
|
||||
TRACE_UNIMPLEMENTED("NIGHTDLL.LoadGame %p:", p1);
|
||||
}
|
||||
|
||||
static void ExistSaveData(void) { /* 59 */
|
||||
int *p1 = getCaliVariable();
|
||||
vmvar_t *p1 = getCaliVariable();
|
||||
|
||||
TRACE_UNIMPLEMENTED("NIGHTDLL.ExistSaveData %p:", p1);
|
||||
}
|
||||
|
||||
static void ExistStartData(void) { /* 60 */
|
||||
int *p1 = getCaliVariable();
|
||||
vmvar_t *p1 = getCaliVariable();
|
||||
|
||||
TRACE_UNIMPLEMENTED("NIGHTDLL.ExistStartData %p:", p1);
|
||||
}
|
||||
@@ -581,13 +580,13 @@ static void DebugScenario(void) { /* 66 */
|
||||
}
|
||||
|
||||
static void GetDLLTime(void) { /* 67 */
|
||||
int *p1 = getCaliVariable();
|
||||
int *p2 = getCaliVariable();
|
||||
int *p3 = getCaliVariable();
|
||||
int *p4 = getCaliVariable();
|
||||
int *p5 = getCaliVariable();
|
||||
int *p6 = getCaliVariable();
|
||||
int *p7 = getCaliVariable();
|
||||
vmvar_t *p1 = getCaliVariable();
|
||||
vmvar_t *p2 = getCaliVariable();
|
||||
vmvar_t *p3 = getCaliVariable();
|
||||
vmvar_t *p4 = getCaliVariable();
|
||||
vmvar_t *p5 = getCaliVariable();
|
||||
vmvar_t *p6 = getCaliVariable();
|
||||
vmvar_t *p7 = getCaliVariable();
|
||||
|
||||
TRACE_UNIMPLEMENTED("NIGHTDLL.GetDLLTime %p,%p,%p,%p,%p,%p,%p:", p1, p2, p3, p4, p5, p6, p7);
|
||||
}
|
||||
|
||||
@@ -53,11 +53,6 @@ static void cb_waitkey_selection(agsevent_t *e) {
|
||||
}
|
||||
|
||||
void ntev_callback(agsevent_t *e) {
|
||||
// menu open中は無視
|
||||
if (nact->popupmenu_opened) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e->type == AGSEVENT_KEY_PRESS && e->code == KEY_CTRL) {
|
||||
night.waitskiplv = 2;
|
||||
night.waitkey = e->code;
|
||||
@@ -90,16 +85,5 @@ void ntev_callback(agsevent_t *e) {
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
system35のメインループからで呼ばれるコールバック
|
||||
*/
|
||||
void ntev_main() {
|
||||
// デフォルトのコールバックのうち、ここで必要なものだけ処理。
|
||||
if (nact->popupmenu_opened) {
|
||||
menu_gtkmainiteration();
|
||||
if (nact->is_quit) sys_exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
#define __NT_EVENT_H__
|
||||
|
||||
extern void ntev_callback(agsevent_t *e);
|
||||
extern void ntev_main();
|
||||
|
||||
|
||||
#endif /* __NT_EVENT_H__ */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include <stdio.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
@@ -194,5 +194,5 @@ void nt_gr_screencg(int no, int x, int y) {
|
||||
SDL_SetSurfaceBlendMode(sf, SDL_BLENDMODE_ADD);
|
||||
SDL_BlitSurface(sf, NULL, main_surface, &(SDL_Rect){x, y, sf->w, sf->h});
|
||||
ags_updateArea(x, y, sf->w, sf->h);
|
||||
SDL_FreeSurface(sf);
|
||||
sdl_destroy_surface(sf);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
@@ -142,7 +142,7 @@ void ntmsg_set_frame(int type) {
|
||||
nt_sp_set_show(night.sp[SPNO_MSGFRAME_BG], true);
|
||||
nt_sp_set_show(night.sp[SPNO_MSGFRAME_FG], true);
|
||||
|
||||
SDL_FillRect(sf, &msgframe_0, SDL_MapRGBA(sf->format, 16, 32, 64, 192));
|
||||
SDL_FillRect(sf, &msgframe_0, sdl_map_rgba(sf, 16, 32, 64, 192));
|
||||
ntmsg_clear(SPNO_MSGFRAME_FG);
|
||||
nt_sp_update_all(true);
|
||||
break;
|
||||
@@ -151,7 +151,7 @@ void ntmsg_set_frame(int type) {
|
||||
nt_sp_set_show(night.sp[SPNO_MSGFRAME_BG], false);
|
||||
nt_sp_set_show(night.sp[SPNO_MSGFRAME_FG], true);
|
||||
|
||||
SDL_FillRect(sf, &msgframe_1, SDL_MapRGBA(sf->format, 32, 32, 32, 128));
|
||||
SDL_FillRect(sf, &msgframe_1, sdl_map_rgba(sf, 32, 32, 32, 128));
|
||||
ntmsg_clear(SPNO_MSGFRAME_FG);
|
||||
nt_sp_update_all(true);
|
||||
|
||||
|
||||
@@ -55,19 +55,17 @@ static void ntmain(struct _scoadr inadr) {
|
||||
while (!nact->is_quit && !is_yield_requested()) {
|
||||
scheduler_on_command();
|
||||
//SACT_DEBUG("%d:%x", sl_getPage(), sl_getIndex());
|
||||
if (!nact->popupmenu_opened) {
|
||||
exec_command();
|
||||
if (sl_getPage() == inadr.page &&
|
||||
sl_getIndex() == inadr.index) {
|
||||
// ~E%05dからの戻り
|
||||
if (nact->fnc_return_value == 0) {
|
||||
return;
|
||||
} else {
|
||||
scono = nact->fnc_return_value;
|
||||
}
|
||||
curadr = scene2adr(scono);
|
||||
sl_callFar2(curadr.page -1, curadr.index);
|
||||
exec_command();
|
||||
if (sl_getPage() == inadr.page &&
|
||||
sl_getIndex() == inadr.index) {
|
||||
// ~E%05dからの戻り
|
||||
if (nact->fnc_return_value == 0) {
|
||||
return;
|
||||
} else {
|
||||
scono = nact->fnc_return_value;
|
||||
}
|
||||
curadr = scene2adr(scono);
|
||||
sl_callFar2(curadr.page -1, curadr.index);
|
||||
}
|
||||
nact->callback();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#include "config.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "sprite.h"
|
||||
@@ -62,7 +62,8 @@ sprite_t *nt_sp_msg_new(int no, int x, int y, int width, int height) {
|
||||
sp->height = height;
|
||||
sp->u.msg.dspcur.x = 0;
|
||||
sp->u.msg.dspcur.y = 0;
|
||||
sp->u.msg.canvas = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_ARGB8888);
|
||||
sp->u.msg.canvas = sdl_create_surface(
|
||||
width, height, 32, SDL_PIXELFORMAT_ARGB8888);
|
||||
sp->update = ntmsg_update;
|
||||
|
||||
return sp;
|
||||
@@ -76,7 +77,7 @@ void nt_sp_free(sprite_t *sp) {
|
||||
if (sp->cg3) scg_deref(sp->cg3);
|
||||
|
||||
if (sp->type == SPRITE_MSG) {
|
||||
SDL_FreeSurface(sp->u.msg.canvas);
|
||||
sdl_destroy_surface(sp->u.msg.canvas);
|
||||
}
|
||||
|
||||
free(sp);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#define __SPRITE_H__
|
||||
|
||||
#include "portab.h"
|
||||
#include <SDL_rect.h>
|
||||
#include "sdl_compat.h"
|
||||
#include "sactcg.h"
|
||||
|
||||
#define DEFAULT_UPDATE nt_sp_draw
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
@@ -55,7 +55,7 @@ void nt_sp_draw(sprite_t *sp, SDL_Rect *r) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SDL_ISPIXELFORMAT_ALPHA(cg->sf->format->format) || sp->blendrate < 255) {
|
||||
if (SDL_ISPIXELFORMAT_ALPHA(sdl_surface_format(cg->sf)) || sp->blendrate < 255) {
|
||||
SDL_SetSurfaceBlendMode(cg->sf, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetSurfaceAlphaMod(cg->sf, sp->blendrate);
|
||||
} else {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
@@ -204,5 +204,5 @@ void nt_sp_clear_updatelist(void) {
|
||||
|
||||
// デフォルトの壁紙update
|
||||
void nt_sp_draw_wall(sprite_t *sp, SDL_Rect *area) {
|
||||
SDL_FillRect(main_surface, area, SDL_MapRGB(main_surface->format, 0, 0, 0));
|
||||
SDL_FillRect(main_surface, area, sdl_map_rgb(main_surface, 0, 0, 0));
|
||||
}
|
||||
|
||||
@@ -60,10 +60,11 @@ static void ndd_run(int demonum) {
|
||||
|
||||
cgdata *cg = jpeg_extract(alk->entries[i].data, alk->entries[i].size);
|
||||
if (cg) {
|
||||
SDL_Surface *sf = SDL_CreateRGBSurfaceWithFormatFrom(cg->pic, cg->width, cg->height, 24, cg->width * 3, SDL_PIXELFORMAT_RGB24);
|
||||
SDL_Surface *sf = sdl_create_surface_from(cg->pic, cg->width,
|
||||
cg->height, 24, cg->width * 3, SDL_PIXELFORMAT_RGB24);
|
||||
SDL_BlitSurface(sf, NULL, main_surface, NULL);
|
||||
ags_updateFull();
|
||||
SDL_FreeSurface(sf);
|
||||
sdl_destroy_surface(sf);
|
||||
cgdata_free(cg);
|
||||
} else {
|
||||
WARNING("Cannot decode CG %d in %s", i, alk_path);
|
||||
@@ -90,7 +91,7 @@ static void Init() {
|
||||
int p1 = getCaliValue(); /* ISys3x */
|
||||
int p2 = getCaliValue(); /* IWinMsg */
|
||||
int p3 = getCaliValue(); /* ITimer */
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
*var = 1;
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ static void Get() {
|
||||
var: 結果を返す変数
|
||||
*/
|
||||
int num = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
if (num == 0 || num == 1) {
|
||||
*var = num;
|
||||
@@ -65,7 +65,7 @@ static void Get() {
|
||||
static void GetNoOverlap() {
|
||||
int min = getCaliValue();
|
||||
int n = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
*var = (int)(genrand() * n) + min;
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ static void CreateChannel(void) {
|
||||
static void Prepare(void) {
|
||||
int ch = getCaliValue();
|
||||
int no = getCaliValue();
|
||||
int *result = getCaliVariable();
|
||||
vmvar_t *result = getCaliVariable();
|
||||
|
||||
if (ch == 0) {
|
||||
current_no = no;
|
||||
@@ -97,7 +97,7 @@ static void PlayPosSample(void) {
|
||||
|
||||
static void IsPlay(void) {
|
||||
int ch = getCaliValue();
|
||||
int *result = getCaliVariable();
|
||||
vmvar_t *result = getCaliVariable();
|
||||
|
||||
if (ch == 0) {
|
||||
*result = current_no && musbgm_isplaying(current_no);
|
||||
@@ -127,10 +127,10 @@ static void Restart(void) {
|
||||
|
||||
static void GetPlayPos(void) {
|
||||
int ch = getCaliValue();
|
||||
int *hour = getCaliVariable();
|
||||
int *min = getCaliVariable();
|
||||
int *sec = getCaliVariable();
|
||||
int *msec = getCaliVariable();
|
||||
vmvar_t *hour = getCaliVariable();
|
||||
vmvar_t *min = getCaliVariable();
|
||||
vmvar_t *sec = getCaliVariable();
|
||||
vmvar_t *msec = getCaliVariable();
|
||||
if (ch == 0) {
|
||||
int time = musbgm_getpos(current_no);
|
||||
*hour = time / 360000;
|
||||
@@ -149,8 +149,8 @@ static void GetPlayPos(void) {
|
||||
|
||||
static void GetPlayPosSample(void) {
|
||||
int p1 = getCaliValue();
|
||||
int *var1 = getCaliVariable();
|
||||
int *var2 = getCaliVariable();
|
||||
vmvar_t *var1 = getCaliVariable();
|
||||
vmvar_t *var2 = getCaliVariable();
|
||||
TRACE_UNIMPLEMENTED("S3xMusic.GetPlayPosSample %d, %p, %p:", p1, var1, var2);
|
||||
}
|
||||
|
||||
@@ -192,7 +192,7 @@ static void FadeVolume(void) {
|
||||
|
||||
static void IsFade(void) {
|
||||
int ch = getCaliValue();
|
||||
int *result = getCaliVariable();
|
||||
vmvar_t *result = getCaliVariable();
|
||||
*result = 0;
|
||||
TRACE_UNIMPLEMENTED("S3xMusic.IsFade %d => %d:", ch, *result);
|
||||
}
|
||||
|
||||
+73
-63
@@ -25,7 +25,7 @@
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
#ifdef __EMSCRIPTEN__
|
||||
#include <emscripten.h>
|
||||
#endif
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
#include "ald_manager.h"
|
||||
#include "audio_meta.h"
|
||||
#include "input.h"
|
||||
#include "msgskip.h"
|
||||
#include "xsystem35.h"
|
||||
@@ -146,7 +147,8 @@ static void Init() {
|
||||
}
|
||||
|
||||
// create depth map
|
||||
sact.dmap = SDL_CreateRGBSurface(0, main_surface->w, main_surface->h, 16, 0, 0, 0, 0);
|
||||
sact.dmap = sdl_create_surface_with_masks(
|
||||
main_surface->w, main_surface->h, 16, 0, 0, 0, 0);
|
||||
|
||||
// その他 System35 のデフォルト動作の変更
|
||||
ags_setAntialiasedStringMode(true);
|
||||
@@ -286,8 +288,11 @@ static void DrawEffect() {
|
||||
if (sact.version >= 110) {
|
||||
wEffectkey = getCaliValue();
|
||||
}
|
||||
|
||||
sp_eupdate(wType, wEffectTime, wEffectkey);
|
||||
|
||||
if (sact.waitskiplv > 1 || msgskip_isSkipping())
|
||||
sp_update_all(true);
|
||||
else
|
||||
sp_eupdate(wType, wEffectTime, wEffectkey);
|
||||
|
||||
TRACE("SACT.DrawEffect %d,%d,%d:", wType, wEffectTime, wEffectkey);
|
||||
}
|
||||
@@ -304,7 +309,10 @@ static void DrawEffectAlphaMap() {
|
||||
int wEffectTime = getCaliValue();
|
||||
int wEffectKey = getCaliValue();
|
||||
|
||||
sp_eupdate_amap(nIndexAlphaMap, wEffectTime, wEffectKey);
|
||||
if (sact.waitskiplv > 1 || msgskip_isSkipping())
|
||||
sp_update_all(true);
|
||||
else
|
||||
sp_eupdate_amap(nIndexAlphaMap, wEffectTime, wEffectKey);
|
||||
|
||||
TRACE("SACT.DrawEffectAlphaMap %d,%d,%d:", nIndexAlphaMap, wEffectTime, wEffectKey);
|
||||
}
|
||||
@@ -622,9 +630,10 @@ static void QuakeSprite() {
|
||||
if (sact.version >= 110) {
|
||||
nfKeyEnable = getCaliValue();
|
||||
}
|
||||
|
||||
sp_quake_sprite(wType, wAmplitudeX, wAmplitudeY, wCount, nfKeyEnable);
|
||||
|
||||
|
||||
if (!msgskip_isSkipping())
|
||||
sp_quake_sprite(wType, wAmplitudeX, wAmplitudeY, wCount, nfKeyEnable);
|
||||
|
||||
TRACE("SACT.QuakeSprite %d,%d,%d,%d:", wType, wAmplitudeX, wAmplitudeY, wCount);
|
||||
}
|
||||
|
||||
@@ -636,7 +645,7 @@ static void QuakeSprite() {
|
||||
*/
|
||||
static void QuerySpriteIsExist() {
|
||||
int wNum = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
*var = sp_exists(wNum) ? 1 : 0;
|
||||
|
||||
@@ -654,10 +663,10 @@ static void QuerySpriteIsExist() {
|
||||
*/
|
||||
static void QuerySpriteInfo() {
|
||||
int wNum = getCaliValue();
|
||||
int *vType = getCaliVariable();
|
||||
int *vCG1 = getCaliVariable();
|
||||
int *vCG2 = getCaliVariable();
|
||||
int *vCG3 = getCaliVariable();
|
||||
vmvar_t *vType = getCaliVariable();
|
||||
vmvar_t *vCG1 = getCaliVariable();
|
||||
vmvar_t *vCG2 = getCaliVariable();
|
||||
vmvar_t *vCG3 = getCaliVariable();
|
||||
|
||||
sp_query_info(wNum, vType, vCG1, vCG2, vCG3);
|
||||
|
||||
@@ -672,7 +681,7 @@ static void QuerySpriteInfo() {
|
||||
*/
|
||||
static void QuerySpriteShow() {
|
||||
int wNum = getCaliValue();
|
||||
int *vShow = getCaliVariable();
|
||||
vmvar_t *vShow = getCaliVariable();
|
||||
|
||||
sp_query_show(wNum, vShow);
|
||||
|
||||
@@ -688,8 +697,8 @@ static void QuerySpriteShow() {
|
||||
*/
|
||||
static void QuerySpritePos() {
|
||||
int wNum = getCaliValue();
|
||||
int *vX = getCaliVariable();
|
||||
int *vY = getCaliVariable();
|
||||
vmvar_t *vX = getCaliVariable();
|
||||
vmvar_t *vY = getCaliVariable();
|
||||
|
||||
sp_query_pos(wNum, vX, vY);
|
||||
|
||||
@@ -705,8 +714,8 @@ static void QuerySpritePos() {
|
||||
*/
|
||||
static void QuerySpriteSize() {
|
||||
int wNum = getCaliValue();
|
||||
int *vWidth = getCaliVariable();
|
||||
int *vHeight = getCaliVariable();
|
||||
vmvar_t *vWidth = getCaliVariable();
|
||||
vmvar_t *vHeight = getCaliVariable();
|
||||
|
||||
sp_query_size(wNum, vWidth, vHeight);
|
||||
|
||||
@@ -722,8 +731,8 @@ static void QuerySpriteSize() {
|
||||
*/
|
||||
static void QueryTextPos() {
|
||||
int wNum = getCaliValue();
|
||||
int *vX = getCaliVariable();
|
||||
int *vY = getCaliVariable();
|
||||
vmvar_t *vX = getCaliVariable();
|
||||
vmvar_t *vY = getCaliVariable();
|
||||
|
||||
sp_query_textpos(wNum, vX, vY);
|
||||
|
||||
@@ -762,7 +771,7 @@ static void CG_Reset() {
|
||||
*/
|
||||
static void CG_QueryType() {
|
||||
int wNumCG = getCaliValue();
|
||||
int *vType = getCaliVariable();
|
||||
vmvar_t *vType = getCaliVariable();
|
||||
|
||||
*vType = scg_querytype(wNumCG);
|
||||
|
||||
@@ -778,8 +787,8 @@ static void CG_QueryType() {
|
||||
*/
|
||||
static void CG_QuerySize() {
|
||||
int wNumCG = getCaliValue();
|
||||
int *vWidth = getCaliVariable();
|
||||
int *vHeight = getCaliVariable();
|
||||
vmvar_t *vWidth = getCaliVariable();
|
||||
vmvar_t *vHeight = getCaliVariable();
|
||||
|
||||
scg_querysize(wNumCG, vWidth, vHeight);
|
||||
|
||||
@@ -794,7 +803,7 @@ static void CG_QuerySize() {
|
||||
*/
|
||||
static void CG_QueryBpp() {
|
||||
int wNumCG = getCaliValue();
|
||||
int *vBpp = getCaliVariable();
|
||||
vmvar_t *vBpp = getCaliVariable();
|
||||
|
||||
*vBpp = scg_querybpp(wNumCG);
|
||||
|
||||
@@ -809,7 +818,7 @@ static void CG_QueryBpp() {
|
||||
*/
|
||||
static void CG_ExistAlphaMap() {
|
||||
int wNumCG = getCaliValue();
|
||||
int *vMask = getCaliVariable();
|
||||
vmvar_t *vMask = getCaliVariable();
|
||||
|
||||
*vMask = scg_existalphamap(wNumCG) ? 1 : 0;
|
||||
|
||||
@@ -1021,7 +1030,7 @@ static void CG_PartCopy() {
|
||||
* @param vKey: 入力されたキー
|
||||
*/
|
||||
static void WaitKeySimple() {
|
||||
int *vKey = getCaliVariable();
|
||||
vmvar_t *vKey = getCaliVariable();
|
||||
|
||||
TRACE("SACT.WaitKeySimple %d:", vKey);
|
||||
|
||||
@@ -1071,10 +1080,10 @@ static void WaitKeyMessage() {
|
||||
* @param vRsv2: 予約
|
||||
*/
|
||||
static void WaitKeySprite() {
|
||||
int *vOK = getCaliVariable();
|
||||
int *vRND = getCaliVariable();
|
||||
int *vRsv1 = getCaliVariable();
|
||||
int *vRsv2 = getCaliVariable();
|
||||
vmvar_t *vOK = getCaliVariable();
|
||||
vmvar_t *vRND = getCaliVariable();
|
||||
vmvar_t *vRsv1 = getCaliVariable();
|
||||
vmvar_t *vRsv2 = getCaliVariable();
|
||||
|
||||
TRACE("SACT.WaitKeySprite %p,%p,%p,%p:", vOK, vRND, vRsv1, vRsv2);
|
||||
|
||||
@@ -1092,7 +1101,7 @@ static void WaitKeySprite() {
|
||||
static void PeekKey() {
|
||||
static int prevKeyCode = NUM_KEYCODES;
|
||||
int nKeyCode = getCaliValue();
|
||||
int *vResult = getCaliVariable();
|
||||
vmvar_t *vResult = getCaliVariable();
|
||||
|
||||
// This function is called successively with different nKeyCodes.
|
||||
// Only the first call hits the scheduler.
|
||||
@@ -1121,8 +1130,8 @@ static void WaitMsgSkipKeyUp() {
|
||||
* @param wTime: タイムアウト時間 (1/100sec)
|
||||
*/
|
||||
static void WaitKeySimpleTimeOut() {
|
||||
int *vRND = getCaliVariable();
|
||||
int *vD03 = getCaliVariable();
|
||||
vmvar_t *vRND = getCaliVariable();
|
||||
vmvar_t *vD03 = getCaliVariable();
|
||||
int wTime = getCaliValue();
|
||||
|
||||
sact.waittype = KEYWAIT_SIMPLE;
|
||||
@@ -1153,11 +1162,11 @@ static void WaitKeySimpleTimeOut() {
|
||||
* @param wTime: タイムアウト時間 (1/100sec)
|
||||
*/
|
||||
static void WaitKeySpriteTimeOut() {
|
||||
int *vOK = getCaliVariable();
|
||||
int *vRND = getCaliVariable();
|
||||
int *vD01 = getCaliVariable();
|
||||
int *vD02 = getCaliVariable();
|
||||
int *vD03 = getCaliVariable();
|
||||
vmvar_t *vOK = getCaliVariable();
|
||||
vmvar_t *vRND = getCaliVariable();
|
||||
vmvar_t *vD01 = getCaliVariable();
|
||||
vmvar_t *vD02 = getCaliVariable();
|
||||
vmvar_t *vD03 = getCaliVariable();
|
||||
int wTime = getCaliValue();
|
||||
|
||||
sp_keywait(vOK, vRND, vD01, vD02, vD03, wTime);
|
||||
@@ -1171,7 +1180,7 @@ static void WaitKeySpriteTimeOut() {
|
||||
* @param vSkip:
|
||||
*/
|
||||
static void QueryMessageSkip() {
|
||||
int *vSkip = getCaliVariable();
|
||||
vmvar_t *vSkip = getCaliVariable();
|
||||
|
||||
*vSkip = msgskip_isSkipping() ? 1 : 0;
|
||||
|
||||
@@ -1219,7 +1228,7 @@ static void MessageOutput() {
|
||||
int wMessageSpeed = getCaliValue();
|
||||
int wMessageLineSpace = getCaliValue();
|
||||
int wMessageAlign = 0;
|
||||
int *vMessageLength = NULL;
|
||||
vmvar_t *vMessageLength = NULL;
|
||||
|
||||
if (sact.version >= 110) {
|
||||
wMessageAlign = getCaliValue();
|
||||
@@ -1266,7 +1275,7 @@ static void MessageOutputEx() {
|
||||
int wRubySize = getCaliValue();
|
||||
int wRubyFont = getCaliValue();
|
||||
int wRubyLineSpace = getCaliValue();
|
||||
int *vLength = NULL;
|
||||
vmvar_t *vLength = NULL;
|
||||
|
||||
if (sact.version >= 120) {
|
||||
vLength = getCaliVariable();
|
||||
@@ -1311,7 +1320,7 @@ static void MessageClear() {
|
||||
* @param wResult: 結果を返す変数
|
||||
*/
|
||||
static void MessageIsEmpty() {
|
||||
int *wResult = getCaliVariable();
|
||||
vmvar_t *wResult = getCaliVariable();
|
||||
|
||||
*wResult = smsg_is_empty() ? 1 : 0;
|
||||
|
||||
@@ -1325,7 +1334,7 @@ static void MessageIsEmpty() {
|
||||
* @param nTopStringNum: バッファを取得する文字列変数の最初
|
||||
*/
|
||||
static void MessagePeek() {
|
||||
int *vCount = getCaliVariable();
|
||||
vmvar_t *vCount = getCaliVariable();
|
||||
int nTopStringNum = getCaliValue();
|
||||
|
||||
*vCount = smsg_peek(nTopStringNum);
|
||||
@@ -1388,7 +1397,7 @@ static void MenuAdd() {
|
||||
* @param nAlign: 行そろえ (0:左, 1:中央, 2: 右) (1.1~)
|
||||
*/
|
||||
static void MenuOpen() {
|
||||
int *wMenuResult = getCaliVariable();
|
||||
vmvar_t *wMenuResult = getCaliVariable();
|
||||
int wNum = getCaliValue();
|
||||
int wChoiceSize = getCaliValue();
|
||||
int wMenuOutSpc = getCaliValue();
|
||||
@@ -1464,7 +1473,7 @@ static void Numeral_SetCG() {
|
||||
static void Numeral_GetCG() {
|
||||
int nNum = getCaliValue();
|
||||
int nIndex = getCaliValue();
|
||||
int *vCG = getCaliVariable();
|
||||
vmvar_t *vCG = getCaliVariable();
|
||||
|
||||
sp_num_getcg(nNum, nIndex, vCG);
|
||||
|
||||
@@ -1497,8 +1506,8 @@ static void Numeral_SetPos() {
|
||||
*/
|
||||
static void Numeral_GetPos() {
|
||||
int nNum = getCaliValue();
|
||||
int *vX = getCaliVariable();
|
||||
int *vY = getCaliVariable();
|
||||
vmvar_t *vX = getCaliVariable();
|
||||
vmvar_t *vY = getCaliVariable();
|
||||
|
||||
sp_num_getpos(nNum, vX, vY);
|
||||
|
||||
@@ -1528,7 +1537,7 @@ static void Numeral_SetSpan() {
|
||||
*/
|
||||
static void Numeral_GetSpan() {
|
||||
int nNum = getCaliValue();
|
||||
int *vSpan = getCaliVariable();
|
||||
vmvar_t *vSpan = getCaliVariable();
|
||||
|
||||
sp_num_getspan(nNum, vSpan);
|
||||
|
||||
@@ -1596,7 +1605,7 @@ static void TimerSet() {
|
||||
*/
|
||||
static void TimerGet() {
|
||||
int wTimerID = getCaliValue();
|
||||
int *vRND = getCaliVariable();
|
||||
vmvar_t *vRND = getCaliVariable();
|
||||
|
||||
*vRND = stimer_get(wTimerID);
|
||||
|
||||
@@ -1615,7 +1624,7 @@ static void TimerWait() {
|
||||
|
||||
int msec = (wCount - stimer_get(wTimerID)) * 10;
|
||||
if (msec > 0)
|
||||
sys_keywait(msec, KEYWAIT_CTRL_CANCELABLE);
|
||||
sys_keywait(msec, KEYWAIT_CTRL_CANCELABLE | KEYWAIT_SKIPPABLE);
|
||||
|
||||
TRACE("SACT.TimerWait %d,%d:", wTimerID, wCount);
|
||||
}
|
||||
@@ -1628,7 +1637,7 @@ static void TimerWait() {
|
||||
static void Wait() {
|
||||
int wCount = getCaliValue();
|
||||
|
||||
sys_keywait(wCount * 10, KEYWAIT_CTRL_CANCELABLE);
|
||||
sys_keywait(wCount * 10, KEYWAIT_CTRL_CANCELABLE | KEYWAIT_SKIPPABLE);
|
||||
|
||||
TRACE("SACT.Wait %d:", wCount);
|
||||
}
|
||||
@@ -1696,7 +1705,7 @@ static void SoundWait() {
|
||||
*/
|
||||
static void SoundWaitKey() {
|
||||
int wNum = getCaliValue();
|
||||
int *vKey = getCaliVariable();
|
||||
vmvar_t *vKey = getCaliVariable();
|
||||
|
||||
ssnd_waitkey(wNum, vKey);
|
||||
|
||||
@@ -1797,7 +1806,7 @@ static void SpriteSoundOB() {
|
||||
*/
|
||||
static void MusicCheck() {
|
||||
int wNum = getCaliValue();
|
||||
int *vRND = getCaliVariable();
|
||||
vmvar_t *vRND = getCaliVariable();
|
||||
|
||||
*vRND = ald_exists(DRIFILE_BGM, wNum - 1) ? 1 : 0;
|
||||
|
||||
@@ -1812,7 +1821,7 @@ static void MusicCheck() {
|
||||
*/
|
||||
static void MusicGetLength() {
|
||||
int wNum = getCaliValue();
|
||||
int *vRND = getCaliVariable();
|
||||
vmvar_t *vRND = getCaliVariable();
|
||||
|
||||
*vRND = musbgm_getlen(wNum);
|
||||
|
||||
@@ -1827,7 +1836,7 @@ static void MusicGetLength() {
|
||||
*/
|
||||
static void MusicGetPos() {
|
||||
int wNum = getCaliValue();
|
||||
int *vRND = getCaliVariable();
|
||||
vmvar_t *vRND = getCaliVariable();
|
||||
|
||||
*vRND = musbgm_getpos(wNum);
|
||||
|
||||
@@ -1845,8 +1854,9 @@ static void MusicPlay() {
|
||||
int wNum = getCaliValue();
|
||||
int wFadeTime = getCaliValue();
|
||||
int wVolume = getCaliValue();
|
||||
bgi_t *bgi = bgi_find(wNum);
|
||||
|
||||
musbgm_play(wNum, wFadeTime, wVolume, 0);
|
||||
musbgm_play(wNum, wFadeTime, wVolume, bgi ? bgi->loopno : 0);
|
||||
|
||||
TRACE("SACT.MusicPlay %d,%d,%d:", wNum, wFadeTime, wVolume);
|
||||
}
|
||||
@@ -1937,7 +1947,7 @@ static void MusicWaitPos() {
|
||||
*/
|
||||
static void SoundGetLinkNum() {
|
||||
int wNum = getCaliValue();
|
||||
int *vRND = getCaliVariable();
|
||||
vmvar_t *vRND = getCaliVariable();
|
||||
|
||||
*vRND = ssnd_getlinknum(wNum);
|
||||
|
||||
@@ -1957,7 +1967,7 @@ static void SoundGetLinkNum() {
|
||||
* pos = ((pos2-pos1) / (val2-val1)) * (val-val1) + pos1
|
||||
*/
|
||||
static void ChartPos() {
|
||||
int *pos = getCaliVariable();
|
||||
vmvar_t *pos = getCaliVariable();
|
||||
int pos1 = getCaliValue();
|
||||
int pos2 = getCaliValue();
|
||||
int val1 = getCaliValue();
|
||||
@@ -2002,7 +2012,7 @@ static void Maze_Create() {
|
||||
* SACT.Maze_Get (1.0~)
|
||||
*/
|
||||
static void Maze_Get() {
|
||||
int *p1 = getCaliVariable();
|
||||
vmvar_t *p1 = getCaliVariable();
|
||||
int p2 = getCaliValue();
|
||||
int p3 = getCaliValue();
|
||||
|
||||
@@ -2013,7 +2023,7 @@ static void Maze_Get() {
|
||||
* SACT.EncryptWORD (1.0~)
|
||||
*/
|
||||
static void EncryptWORD() {
|
||||
int *array = getCaliVariable();
|
||||
vmvar_t *array = getCaliVariable();
|
||||
int num = getCaliValue();
|
||||
int key = getCaliValue();
|
||||
|
||||
@@ -2026,7 +2036,7 @@ static void EncryptWORD() {
|
||||
* SACT.DecryptWORD (1.0~)
|
||||
*/
|
||||
static void DecryptWORD() {
|
||||
int *array = getCaliVariable();
|
||||
vmvar_t *array = getCaliVariable();
|
||||
int num = getCaliValue();
|
||||
int key = getCaliValue();
|
||||
|
||||
@@ -2092,7 +2102,7 @@ static void XMenuRegister() {
|
||||
*/
|
||||
static void XMenuGetNum() {
|
||||
int nRegiNum = getCaliValue();
|
||||
int *vMenuID = getCaliVariable();
|
||||
vmvar_t *vMenuID = getCaliVariable();
|
||||
|
||||
*vMenuID = spxm_getnum(nRegiNum);
|
||||
|
||||
@@ -2145,7 +2155,7 @@ static void SACT_reset(void) {
|
||||
sp_reset();
|
||||
|
||||
scg_freeall();
|
||||
SDL_FreeSurface(sact.dmap);
|
||||
sdl_destroy_surface(sact.dmap);
|
||||
memset(&sact, 0, sizeof(sact));
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
#define __SACT_H__
|
||||
|
||||
#include "config.h"
|
||||
#include <SDL_rect.h>
|
||||
#include "sdl_compat.h"
|
||||
#include "portab.h"
|
||||
#include "list.h"
|
||||
#include "ags.h"
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
@@ -90,13 +90,14 @@ void sp_eupdate_amap(int index, int time, int cancel) {
|
||||
sp_update_all(true);
|
||||
return;
|
||||
}
|
||||
SDL_Surface *mask_sf = SDL_CreateRGBSurfaceFrom(mask->pic, mask->width, mask->height, 8, mask->width, 0, 0, 0, 0);
|
||||
SDL_Surface *mask_sf = sdl_create_surface_from(mask->pic, mask->width,
|
||||
mask->height, 8, mask->width, SDL_PIXELFORMAT_INDEX8);
|
||||
sp_update_all(false); // old = gfx_texture, new = main_surface
|
||||
struct effect *eff = effect_sactamask_init(mask_sf);
|
||||
ags_runEffect(time * 10, cancel, (ags_EffectStepFunc)effect_step, eff);
|
||||
effect_finish(eff);
|
||||
ags_updateFull();
|
||||
|
||||
SDL_FreeSurface(mask_sf);
|
||||
sdl_destroy_surface(mask_sf);
|
||||
cgdata_free(mask);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
#include "portab.h"
|
||||
|
||||
// グラフ用チャート作成
|
||||
void schart_pos(int *pos, int pos1, int pos2, int val1, int val2, int val) {
|
||||
void schart_pos(vmvar_t *pos, int pos1, int pos2, int val1, int val2, int val) {
|
||||
if (val1 == val2) {
|
||||
*pos = 0;
|
||||
} else {
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
#ifndef __SACTCHART_H__
|
||||
#define __SACTCHART_H__
|
||||
|
||||
void schart_pos(int *pos, int pos1, int pos2, int val1, int val2, int val);
|
||||
#include "portab.h"
|
||||
|
||||
void schart_pos(vmvar_t *pos, int pos1, int pos2, int val1, int val2, int val);
|
||||
|
||||
#endif
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
適当でいいとおもう
|
||||
*/
|
||||
|
||||
void scryp_encrypt_word(int *array, int num, int key) {
|
||||
void scryp_encrypt_word(vmvar_t *array, int num, int key) {
|
||||
WARNING("NOT IMPLEMENTED");
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,9 @@
|
||||
#ifndef __SACTCRYPT_H__
|
||||
#define __SACTCRYPT_H__
|
||||
|
||||
void scryp_encrypt_word(int *array, int num, int key);
|
||||
#include "portab.h"
|
||||
|
||||
void scryp_encrypt_word(vmvar_t *array, int num, int key);
|
||||
void scryp_decrypt_word(int *array, int num, int key);
|
||||
void scryp_encrypt_str(int strno, int key);
|
||||
void scryp_decrypt_str(int strno, int key);
|
||||
|
||||
+11
-6
@@ -26,7 +26,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
@@ -73,8 +73,9 @@ static void draw_log() {
|
||||
load_mincho_font();
|
||||
#endif
|
||||
|
||||
SDL_Surface *hline = SDL_CreateRGBSurfaceWithFormat(0, main_surface->w, 3, 32, SDL_PIXELFORMAT_RGB888);
|
||||
SDL_FillRect(hline, NULL, SDL_MapRGB(hline->format, 255, 255, 255));
|
||||
SDL_Surface *hline = sdl_create_surface(
|
||||
main_surface->w, 3, 32, SDL_PIXELFORMAT_RGB888);
|
||||
SDL_FillRect(hline, NULL, sdl_map_rgb(hline, 255, 255, 255));
|
||||
SDL_SetSurfaceBlendMode(hline, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetSurfaceAlphaMod(hline, 128);
|
||||
|
||||
@@ -97,7 +98,11 @@ static void draw_log() {
|
||||
if (0 == strcmp(str, "\n")) {
|
||||
SDL_BlitSurface(hline, NULL, main_surface, &(SDL_Rect){0, y + FONTSIZE/2, main_surface->w, 3});
|
||||
} else {
|
||||
FontSpec font_spec = { .type = cur < 6 ? FONT_MINCHO : FONT_GOTHIC, .size = FONTSIZE };
|
||||
FontSpec font_spec = {
|
||||
.type = cur < 6 ? FONT_MINCHO : FONT_GOTHIC,
|
||||
.weight = cur < 6 ? FONT_WEIGHT_NORMAL : FONT_WEIGHT_BOLD,
|
||||
.size = FONTSIZE
|
||||
};
|
||||
gfx_drawString(0, y, str, 255, font_spec);
|
||||
}
|
||||
y += FONTSIZE;
|
||||
@@ -118,7 +123,7 @@ bool sblog_start(void) {
|
||||
for (int i = 0; i < LOGMSG_LINES; i++)
|
||||
sact.log = list_append(sact.log, logmsg[i]);
|
||||
|
||||
back = SDL_ConvertSurface(main_surface, main_surface->format, 0);
|
||||
back = sdl_convert_surface(main_surface, sdl_surface_format(main_surface));
|
||||
curline = 6;
|
||||
draw_log();
|
||||
return true;
|
||||
@@ -131,7 +136,7 @@ void sblog_end(void) {
|
||||
SDL_BlitSurface(back, NULL, main_surface, NULL);
|
||||
ags_updateFull();
|
||||
|
||||
SDL_FreeSurface(back);
|
||||
sdl_destroy_surface(back);
|
||||
|
||||
// Remove instruction text
|
||||
for (i = 0; i < LOGMSG_LINES; i++) {
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
#include "system.h"
|
||||
#include "nact.h"
|
||||
#include "input.h"
|
||||
#include "msgskip.h"
|
||||
#include "sactsound.h"
|
||||
#include "music.h"
|
||||
#include "sact.h"
|
||||
@@ -112,7 +113,7 @@ void ssnd_wait(int no) {
|
||||
}
|
||||
|
||||
// 指定の効果音が終了するか、キーが押されるまで待つ
|
||||
void ssnd_waitkey(int no, int *res) {
|
||||
void ssnd_waitkey(int no, vmvar_t *res) {
|
||||
int slot = slt_find(no);
|
||||
|
||||
if (slot == -1) {
|
||||
@@ -120,7 +121,7 @@ void ssnd_waitkey(int no, int *res) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sact.waitskiplv > 1) {
|
||||
if (sact.waitskiplv > 1 || msgskip_isSkipping()) {
|
||||
*res = SYS35KEY_RET;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -24,11 +24,13 @@
|
||||
#ifndef __SACTSOUND_H__
|
||||
#define __SACTSOUND_H__
|
||||
|
||||
#include "portab.h"
|
||||
|
||||
void ssnd_init(void);
|
||||
void ssnd_play(int no);
|
||||
void ssnd_stop(int no, int fadetime);
|
||||
void ssnd_wait(int no);
|
||||
void ssnd_waitkey(int no, int *res);
|
||||
void ssnd_waitkey(int no, vmvar_t *res);
|
||||
void ssnd_prepare(int no);
|
||||
void ssnd_prepareLRrev(int no);
|
||||
void ssnd_playLRrev(int no);
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
#include "sacttimer.h"
|
||||
#include "system.h"
|
||||
|
||||
#define MAX_TIMER 10
|
||||
#define MAX_TIMER 128
|
||||
#define TICKS_PER_CENTISECOND 10
|
||||
|
||||
uint32_t ticks_base[MAX_TIMER];
|
||||
|
||||
+14
-12
@@ -26,7 +26,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
@@ -65,7 +65,8 @@ static int compare_spriteno_smallfirst(const void *a, const void *b) {
|
||||
|
||||
// デフォルトの壁紙update
|
||||
static void sp_draw_wall(sprite_t *sp) {
|
||||
SDL_FillRect(main_surface, &sact.updaterect, SDL_MapRGB(main_surface->format, 0, 0, 0));
|
||||
SDL_FillRect(main_surface, &sact.updaterect,
|
||||
sdl_map_rgb(main_surface, 0, 0, 0));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,7 +208,8 @@ void sp_new_msg(int no, int x, int y, int width, int height) {
|
||||
sp->u.msg.buf = NULL;
|
||||
|
||||
// canvas for drawing text
|
||||
sp->u.msg.canvas = SDL_CreateRGBSurfaceWithFormat(0, width, height, 32, SDL_PIXELFORMAT_ARGB8888);
|
||||
sp->u.msg.canvas = sdl_create_surface(
|
||||
width, height, 32, SDL_PIXELFORMAT_ARGB8888);
|
||||
|
||||
// スプライト再描画用コールバック
|
||||
sp->update = smsg_update;
|
||||
@@ -279,7 +281,7 @@ void sp_free(int no) {
|
||||
|
||||
if (sp->type == SPRITE_MSG) {
|
||||
slist_free(sp->u.msg.buf);
|
||||
SDL_FreeSurface(sp->u.msg.canvas);
|
||||
sdl_destroy_surface(sp->u.msg.canvas);
|
||||
}
|
||||
sact.updatelist = slist_remove(sact.updatelist, sp);
|
||||
|
||||
@@ -455,7 +457,7 @@ bool sp_exists(int wNum) {
|
||||
}
|
||||
|
||||
// スプライトのタイプと何番のCGがセットされているかの取得
|
||||
bool sp_query_info(int wNum, int *vtype, int *vcg1, int *vcg2, int *vcg3) {
|
||||
bool sp_query_info(int wNum, vmvar_t *vtype, vmvar_t *vcg1, vmvar_t *vcg2, vmvar_t *vcg3) {
|
||||
sprite_t *sp;
|
||||
|
||||
if (wNum >= SPRITEMAX) goto errexit;
|
||||
@@ -479,7 +481,7 @@ bool sp_query_info(int wNum, int *vtype, int *vcg1, int *vcg2, int *vcg3) {
|
||||
}
|
||||
|
||||
// スプライトの表示状態の取得
|
||||
bool sp_query_show(int wNum, int *vShow) {
|
||||
bool sp_query_show(int wNum, vmvar_t *vShow) {
|
||||
if (wNum >= SPRITEMAX) goto errexit;
|
||||
if (sact.sp[wNum]->type == SPRITE_NONE) goto errexit;
|
||||
|
||||
@@ -492,7 +494,7 @@ bool sp_query_show(int wNum, int *vShow) {
|
||||
}
|
||||
|
||||
// スプライトの表示位置の取得
|
||||
bool sp_query_pos(int wNum, int *vx, int *vy) {
|
||||
bool sp_query_pos(int wNum, vmvar_t *vx, vmvar_t *vy) {
|
||||
if (wNum >= SPRITEMAX) goto errexit;
|
||||
if (sact.sp[wNum]->type == SPRITE_NONE) goto errexit;
|
||||
|
||||
@@ -507,7 +509,7 @@ bool sp_query_pos(int wNum, int *vx, int *vy) {
|
||||
}
|
||||
|
||||
// スプライトの大きさの取得
|
||||
bool sp_query_size(int wNum, int *vw, int *vh) {
|
||||
bool sp_query_size(int wNum, vmvar_t *vw, vmvar_t *vh) {
|
||||
sprite_t *sp;
|
||||
|
||||
if (wNum >= SPRITEMAX) goto errexit;
|
||||
@@ -528,7 +530,7 @@ bool sp_query_size(int wNum, int *vw, int *vh) {
|
||||
}
|
||||
|
||||
// テキストスプライトの現在の文字表示位置の取得
|
||||
bool sp_query_textpos(int wNum, int *vx, int *vy) {
|
||||
bool sp_query_textpos(int wNum, vmvar_t *vx, vmvar_t *vy) {
|
||||
if (wNum >= SPRITEMAX) goto errexit;
|
||||
sprite_t *sp = sact.sp[wNum];
|
||||
if (sp->type != SPRITE_MSG) goto errexit;
|
||||
@@ -551,7 +553,7 @@ void sp_num_setcg(int nNum, int nIndex, int nCG) {
|
||||
}
|
||||
|
||||
// NumeralXXXのCGの取得
|
||||
void sp_num_getcg(int nNum, int nIndex, int *vCG) {
|
||||
void sp_num_getcg(int nNum, int nIndex, vmvar_t *vCG) {
|
||||
SP_ASSERT_NO(nNum);
|
||||
|
||||
*vCG = sact.sp[nNum]->numeral.cg[nIndex];
|
||||
@@ -566,7 +568,7 @@ void sp_num_setpos(int nNum, int nX, int nY) {
|
||||
}
|
||||
|
||||
// NumeralXXXの位置の取得
|
||||
void sp_num_getpos(int nNum, int *vX, int *vY) {
|
||||
void sp_num_getpos(int nNum, vmvar_t *vX, vmvar_t *vY) {
|
||||
SP_ASSERT_NO(nNum);
|
||||
|
||||
*vX = sact.sp[nNum]->numeral.pos.x;
|
||||
@@ -581,7 +583,7 @@ void sp_num_setspan(int nNum, int nSpan) {
|
||||
}
|
||||
|
||||
// NumeralXXXのスパンの取得
|
||||
void sp_num_getspan(int nNum, int *vSpan) {
|
||||
void sp_num_getspan(int nNum, vmvar_t *vSpan) {
|
||||
SP_ASSERT_NO(nNum);
|
||||
|
||||
*vSpan = sact.sp[nNum]->numeral.span;
|
||||
|
||||
+11
-10
@@ -24,6 +24,7 @@
|
||||
#ifndef __SPRITE_H__
|
||||
#define __SPRITE_H__
|
||||
|
||||
#include "portab.h"
|
||||
#include "sact.h"
|
||||
|
||||
#define DEFAULT_UPDATE sp_draw
|
||||
@@ -51,17 +52,17 @@ void sp_set_animeinterval(int wNum, int wTime);
|
||||
bool sp_is_insprite(sprite_t *sp, int x, int y);
|
||||
void sp_set_blendrate(int wNum, int wCount, int rate);
|
||||
bool sp_exists(int wNum);
|
||||
bool sp_query_info(int wNum, int *vtype, int *vcg1, int *vcg2, int *vcg3);
|
||||
bool sp_query_show(int wNum, int *vShow);
|
||||
bool sp_query_pos(int wNum, int *vx, int *vy);
|
||||
bool sp_query_size(int wNum, int *vw, int *vh);
|
||||
bool sp_query_textpos(int wNum, int *vx, int *vy);
|
||||
bool sp_query_info(int wNum, vmvar_t *vtype, vmvar_t *vcg1, vmvar_t *vcg2, vmvar_t *vcg3);
|
||||
bool sp_query_show(int wNum, vmvar_t *vShow);
|
||||
bool sp_query_pos(int wNum, vmvar_t *vx, vmvar_t *vy);
|
||||
bool sp_query_size(int wNum, vmvar_t *vw, vmvar_t *vh);
|
||||
bool sp_query_textpos(int wNum, vmvar_t *vx, vmvar_t *vy);
|
||||
void sp_num_setcg(int nNum, int nIndex, int nCG);
|
||||
void sp_num_getcg(int nNum, int nIndex, int *vCG);
|
||||
void sp_num_getcg(int nNum, int nIndex, vmvar_t *vCG);
|
||||
void sp_num_setpos(int nNum, int nX, int nY);
|
||||
void sp_num_getpos(int nNum, int *vX, int *vY);
|
||||
void sp_num_getpos(int nNum, vmvar_t *vX, vmvar_t *vY);
|
||||
void sp_num_setspan(int nNum, int nSpan);
|
||||
void sp_num_getspan(int nNUm, int *vSpan);
|
||||
void sp_num_getspan(int nNUm, vmvar_t *vSpan);
|
||||
void sp_exp_clear(void);
|
||||
void sp_exp_add(int nNumSP1, int nNumSP2);
|
||||
void sp_exp_del(int wNum);
|
||||
@@ -85,7 +86,7 @@ void sp_draw_dmap(void* data, void* userdata);
|
||||
// in sprite_msg.c
|
||||
void smsg_add(const char *msg);
|
||||
void smsg_newline(int wNum, int size);
|
||||
void smsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB, int wFont, int wSpeed, int wLineSpace, int wAlign, int wRSize, int wRFont, int wRLineSpace, int *wLength);
|
||||
void smsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB, int wFont, int wSpeed, int wLineSpace, int wAlign, int wRSize, int wRFont, int wRLineSpace, vmvar_t *wLength);
|
||||
void smsg_clear(int wNum);
|
||||
bool smsg_is_empty();
|
||||
int smsg_peek(int nTopStringNum);
|
||||
@@ -133,7 +134,7 @@ void sp_eupdate(int type, int time, int key);
|
||||
void sp_quake_sprite(int wType, int wAmplitudeX, int wAmplitude, int wCount, int cancel);
|
||||
|
||||
// in sprite_keywait.c
|
||||
void sp_keywait(int *vOK, int *vRND, int *vRsv1, int *vRsv2, int *vRsv3, int timeout);
|
||||
void sp_keywait(vmvar_t *vOK, vmvar_t *vRND, vmvar_t *vRsv1, vmvar_t *vRsv2, vmvar_t *vRsv3, int timeout);
|
||||
|
||||
// in screen_quake.c
|
||||
void sp_quake_screen(int type, int p1, int p2, int time, int cancel);
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
@@ -38,7 +38,7 @@
|
||||
static void fill_dmap_mask(SDL_Surface *src, int sx, int sy, int dx ,int dy, int w, int h, uint16_t val) {
|
||||
uint8_t *sp, *dp;
|
||||
int x, y;
|
||||
assert(src->format->Amask == 0xff000000);
|
||||
assert(sdl_surface_alpha_mask(src) == 0xff000000);
|
||||
dp = PIXEL_AT(sact.dmap, dx, dy);
|
||||
sp = ALPHA_AT(src, sx, sy);
|
||||
|
||||
@@ -75,7 +75,7 @@ void sp_draw(sprite_t *sp) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SDL_ISPIXELFORMAT_ALPHA(cg->sf->format->format) || sp->blendrate < 255) {
|
||||
if (SDL_ISPIXELFORMAT_ALPHA(sdl_surface_format(cg->sf)) || sp->blendrate < 255) {
|
||||
SDL_SetSurfaceBlendMode(cg->sf, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetSurfaceAlphaMod(cg->sf, sp->blendrate);
|
||||
} else {
|
||||
@@ -116,7 +116,7 @@ void sp_draw_dmap(void* data, void* userdata) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SDL_ISPIXELFORMAT_ALPHA(cg->sf->format->format)) {
|
||||
if (SDL_ISPIXELFORMAT_ALPHA(sdl_surface_format(cg->sf))) {
|
||||
fill_dmap_mask(cg->sf, sx, sy, dx, dy, w, h, sp->no);
|
||||
} else {
|
||||
SDL_FillRect(sact.dmap, &(SDL_Rect){dx, dy, w, h}, sp->no);
|
||||
|
||||
@@ -39,11 +39,6 @@
|
||||
@param cancel: キー抜け(0:なし, 1:あり)
|
||||
*/
|
||||
void sp_eupdate(int type, int time, int cancel) {
|
||||
if (sact.waitskiplv > 1) {
|
||||
sp_update_all(true);
|
||||
return;
|
||||
}
|
||||
|
||||
sp_update_all(false);
|
||||
|
||||
enum effect_type effect = from_sact_effect(type);
|
||||
|
||||
@@ -426,11 +426,6 @@ static void cb_waitkey_backlog(agsevent_t *e) {
|
||||
X|SDL のイベントディスパッチャからくる最初の場所
|
||||
*/
|
||||
void spev_callback(agsevent_t *e) {
|
||||
// menu open中は無視
|
||||
if (nact->popupmenu_opened) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sact.waittype != KEYWAIT_BACKLOG) {
|
||||
if (e->type == AGSEVENT_KEY_PRESS && e->code == KEY_CTRL) {
|
||||
sact.waitskiplv = 2;
|
||||
|
||||
@@ -113,7 +113,7 @@ static bool waitcond(int endtime) {
|
||||
@param vD03: タイムアウトした場合=1, しない場合=0
|
||||
@param wTime: タイムアウト時間 (1/100sec)
|
||||
*/
|
||||
void sp_keywait(int *vOK, int *vRND, int *vD01, int *vD02, int *vD03, int timeout) {
|
||||
void sp_keywait(vmvar_t *vOK, vmvar_t *vRND, vmvar_t *vD01, vmvar_t *vD02, vmvar_t *vD03, int timeout) {
|
||||
int curtime, endtime;
|
||||
|
||||
// とりあえず全更新
|
||||
@@ -171,4 +171,3 @@ void sp_keywait(int *vOK, int *vRND, int *vD01, int *vD02, int *vD03, int timeou
|
||||
sact.waittype = KEYWAIT_NONE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -30,9 +30,11 @@
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
#include "ags.h"
|
||||
#include "gfx.h"
|
||||
#include "nact.h"
|
||||
#include "variable.h"
|
||||
#include "input.h"
|
||||
#include "msgskip.h"
|
||||
#include "sact.h"
|
||||
#include "sprite.h"
|
||||
#include "drawtext.h"
|
||||
@@ -236,11 +238,12 @@ void smsg_newline(int wNum, int size) {
|
||||
@param wRLineSpace: ルビと本文の文字間隔
|
||||
@param vLength: ???
|
||||
*/
|
||||
void smsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB, int wFont, int wSpeed, int wLineSpace, int wAlign, int wRSize, int wRFont, int wRLineSpace, int *wLength) {
|
||||
void smsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB, int wFont, int wSpeed, int wLineSpace, int wAlign, int wRSize, int wRFont, int wRLineSpace, vmvar_t *wLength) {
|
||||
char *msg;
|
||||
sprite_t *sp;
|
||||
int len = 0; // 処理した文字数?
|
||||
bool needupdate = false;
|
||||
bool has_batched_output = false;
|
||||
bool flush_batched_output = wSpeed > 0;
|
||||
SDL_Rect uparea = {0,0,0,0};
|
||||
|
||||
// wRSize == 0 -> ルビ無し(SACT.MessageOutputからの呼出)
|
||||
@@ -250,7 +253,7 @@ void smsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB, int wF
|
||||
if (!is_messagesprite(wNum)) return;
|
||||
|
||||
// MessageSkip中は文字送り速度を最大に
|
||||
if (sact.waitskiplv > 1) wSpeed = 0;
|
||||
if (sact.waitskiplv > 1 || msgskip_isSkipping()) wSpeed = 0;
|
||||
|
||||
// shortcut
|
||||
sp = sact.sp[wNum];
|
||||
@@ -302,7 +305,7 @@ void smsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB, int wF
|
||||
mbuf,
|
||||
wColorR, wColorG, wColorB);
|
||||
|
||||
needupdate = true;
|
||||
has_batched_output = true;
|
||||
|
||||
append_to_log(mbuf);
|
||||
|
||||
@@ -313,7 +316,7 @@ void smsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB, int wF
|
||||
cw,
|
||||
wSize + wRSize + wRLineSpace);
|
||||
sp_update_clipped();
|
||||
needupdate = false;
|
||||
has_batched_output = false;
|
||||
|
||||
// keywait
|
||||
delta = sys_get_ticks() - wcnt;
|
||||
@@ -333,11 +336,17 @@ void smsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB, int wF
|
||||
// バッファリング中の文字のクリア
|
||||
sact.msgbuf[0] = '\0';
|
||||
|
||||
// Waitなしの出力は最後にupdate
|
||||
if (needupdate) {
|
||||
// Register the update area after rendering without waits.
|
||||
if (has_batched_output) {
|
||||
uparea.w = sp->width;
|
||||
uparea.h = min(sp->height, uparea.y - sp->u.msg.dspcur.y + wLineSpace + wLineSpace + wRSize);
|
||||
uparea.h = min(sp->height - uparea.y,
|
||||
sp->u.msg.dspcur.y - uparea.y + wSize + wRSize + wRLineSpace);
|
||||
sp_updateme_part(sp, uparea.x, uparea.y, uparea.w, uparea.h);
|
||||
// Present now if waiting was skipped.
|
||||
if (flush_batched_output) {
|
||||
sp_update_clipped();
|
||||
gfx_updateScreen();
|
||||
}
|
||||
}
|
||||
|
||||
// ????
|
||||
@@ -408,7 +417,7 @@ int smsg_keywait(int wNum1, int wNum2, int msglen) {
|
||||
struct markinfo minfo[6];
|
||||
int i = 0, j, maxstep;
|
||||
|
||||
if (sact.waitskiplv > 0) {
|
||||
if (sact.waitskiplv > 0 || msgskip_isSkipping()) {
|
||||
sys_getInputInfo();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "nact.h"
|
||||
@@ -140,14 +140,14 @@ static void cb_select_release(agsevent_t *e) {
|
||||
}
|
||||
|
||||
static void draw_box(SDL_Surface *dst, int x, int y, int w, int h) {
|
||||
SDL_FillRect(dst, &(SDL_Rect){x, y, w, h}, SDL_MapRGB(dst->format, 0, 0, 0));
|
||||
SDL_FillRect(dst, &(SDL_Rect){x, y, w, h}, sdl_map_rgb(dst, 0, 0, 0));
|
||||
SDL_Rect border_rects[4] = {
|
||||
{x, y, w, 1},
|
||||
{x, y + h - 1, w, 1},
|
||||
{x, y, 1, h},
|
||||
{x + w - 1, y, 1, h}
|
||||
};
|
||||
SDL_FillRects(dst, border_rects, 4, SDL_MapRGB(dst->format, 255, 255, 255));
|
||||
SDL_FillRects(dst, border_rects, 4, sdl_map_rgb(dst, 255, 255, 255));
|
||||
}
|
||||
|
||||
// 選択ウィンドを更新するときの callback
|
||||
@@ -180,7 +180,8 @@ static void setup_selwindow() {
|
||||
int i;
|
||||
|
||||
// 選択肢文字用 canvas
|
||||
sact.sel.charcanvas = SDL_CreateRGBSurfaceWithFormat(0, sp->cg1->sf->w, sp->cg1->sf->h, 32, SDL_PIXELFORMAT_ARGB8888);
|
||||
sact.sel.charcanvas = sdl_create_surface(
|
||||
sp->cg1->sf->w, sp->cg1->sf->h, 32, SDL_PIXELFORMAT_ARGB8888);
|
||||
SDL_SetSurfaceBlendMode(sact.sel.charcanvas, SDL_BLENDMODE_BLEND);
|
||||
|
||||
dt_setfont(sact.sel.font_type, sact.sel.font_size);
|
||||
@@ -228,7 +229,7 @@ static void remove_selwindow() {
|
||||
|
||||
// 作業用 surface の削除
|
||||
if (sact.sel.charcanvas)
|
||||
SDL_FreeSurface(sact.sel.charcanvas);
|
||||
sdl_destroy_surface(sact.sel.charcanvas);
|
||||
sact.sel.charcanvas = NULL;
|
||||
}
|
||||
|
||||
@@ -266,7 +267,7 @@ void ssel_init() {
|
||||
void ssel_reset(void) {
|
||||
ssel_clear();
|
||||
if (sact.sel.charcanvas) {
|
||||
SDL_FreeSurface(sact.sel.charcanvas);
|
||||
sdl_destroy_surface(sact.sel.charcanvas);
|
||||
sact.sel.charcanvas = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,11 +117,4 @@ void spev_main() {
|
||||
|
||||
e.type = AGSEVENT_TIMER;
|
||||
tevent_callback(&e);
|
||||
|
||||
// デフォルトのコールバックのうち、ここで必要なものだけ
|
||||
// 処理。(VAコマンドcallbackはなし)
|
||||
if (nact->popupmenu_opened) {
|
||||
menu_gtkmainiteration();
|
||||
if (nact->is_quit) sys_exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
+83
-83
@@ -43,10 +43,10 @@ static void GetAtArray(void) { /* 0 */
|
||||
type: 演算の種類
|
||||
vResult: 演算結果を返す変数
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int type = getCaliValue();
|
||||
int *vResult = getCaliVariable();
|
||||
vmvar_t *vResult = getCaliVariable();
|
||||
int i, j;
|
||||
|
||||
TRACE("ShArray.GetAtArray %p,%d,%d,%p:", vAry, cnt, type, vResult);
|
||||
@@ -88,8 +88,8 @@ static void AddAtArray(void) { /* 1 */
|
||||
vAry2: 配列2
|
||||
cnt : 個数
|
||||
*/
|
||||
int *vAry1 = getCaliVariable();
|
||||
int *vAry2 = getCaliVariable();
|
||||
vmvar_t *vAry1 = getCaliVariable();
|
||||
vmvar_t *vAry2 = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int i;
|
||||
|
||||
@@ -114,8 +114,8 @@ static void SubAtArray(void) { /* 2 */
|
||||
vAry2: 配列2
|
||||
cnt : 個数
|
||||
*/
|
||||
int *vAry1 = getCaliVariable();
|
||||
int *vAry2 = getCaliVariable();
|
||||
vmvar_t *vAry1 = getCaliVariable();
|
||||
vmvar_t *vAry2 = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int i;
|
||||
|
||||
@@ -140,8 +140,8 @@ static void MulAtArray(void) { /* 3 */
|
||||
vAry2: 配列2
|
||||
cnt : 個数
|
||||
*/
|
||||
int *vAry1 = getCaliVariable();
|
||||
int *vAry2 = getCaliVariable();
|
||||
vmvar_t *vAry1 = getCaliVariable();
|
||||
vmvar_t *vAry2 = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int i;
|
||||
|
||||
@@ -166,8 +166,8 @@ static void DivAtArray(void) { /* 4 */
|
||||
vAry2: 配列2
|
||||
cnt : 個数
|
||||
*/
|
||||
int *vAry1 = getCaliVariable();
|
||||
int *vAry2 = getCaliVariable();
|
||||
vmvar_t *vAry1 = getCaliVariable();
|
||||
vmvar_t *vAry2 = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int i;
|
||||
|
||||
@@ -196,8 +196,8 @@ static void MinAtArray(void) { /* 5 */
|
||||
vAry2: 配列2
|
||||
cnt : 個数
|
||||
*/
|
||||
int *vAry1 = getCaliVariable();
|
||||
int *vAry2 = getCaliVariable();
|
||||
vmvar_t *vAry1 = getCaliVariable();
|
||||
vmvar_t *vAry2 = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int i;
|
||||
|
||||
@@ -219,8 +219,8 @@ static void MaxAtArray(void) { /* 6 */
|
||||
vAry2: 配列2
|
||||
cnt : 個数
|
||||
*/
|
||||
int *vAry1 = getCaliVariable();
|
||||
int *vAry2 = getCaliVariable();
|
||||
vmvar_t *vAry1 = getCaliVariable();
|
||||
vmvar_t *vAry2 = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int i;
|
||||
|
||||
@@ -242,7 +242,7 @@ static void AndNumArray(void) { /* 7 */
|
||||
cnt : 個数
|
||||
val : ANDをとる値
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int val = getCaliValue();
|
||||
int i;
|
||||
@@ -263,7 +263,7 @@ static void OrNumArray(void) { /* 8 */
|
||||
cnt : 個数
|
||||
val : ORをとる値
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int val = getCaliValue();
|
||||
int i;
|
||||
@@ -284,7 +284,7 @@ static void XorNumArray(void) { /* 9 */
|
||||
cnt : 個数
|
||||
val : XORをとる値
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int val = getCaliValue();
|
||||
int i;
|
||||
@@ -307,10 +307,10 @@ static void SetEquArray(void) { /* 10 */
|
||||
val : 比較する値
|
||||
vResults : 結果を格納する配列
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int val = getCaliValue();
|
||||
int *vResults = getCaliVariable();
|
||||
vmvar_t *vResults = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.SetEquArray %p,%d,%d,%p:", vAry, cnt, val, vResults);
|
||||
@@ -331,10 +331,10 @@ static void SetNotArray(void) { /* 11 */
|
||||
val : 比較する値
|
||||
vResults : 結果を格納する配列
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int val = getCaliValue();
|
||||
int *vResults = getCaliVariable();
|
||||
vmvar_t *vResults = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.SetNotArray %p,%d,%d,%p:", vAry, cnt, val, vResults);
|
||||
@@ -354,10 +354,10 @@ static void SetLowArray(void) { /* 12 */
|
||||
val : 閾値
|
||||
vResult: 結果を返す変数
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int val = getCaliValue();
|
||||
int *vResults = getCaliVariable();
|
||||
vmvar_t *vResults = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.SetLowArray %p,%d,%d,%p:", vAry, cnt, val, vResults);
|
||||
@@ -377,10 +377,10 @@ static void SetHighArray(void) { /* 13 */
|
||||
val : 閾値
|
||||
vResults: 結果を返す変数
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int val = getCaliValue();
|
||||
int *vResults = getCaliVariable();
|
||||
vmvar_t *vResults = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.SetHighArray %p,%d,%d,%p:", vAry, cnt, val, vResults);
|
||||
@@ -403,11 +403,11 @@ static void SetRangeArray(void) { /* 14 */
|
||||
min < vAry < max の時 vResults = 1;
|
||||
それ以外 vResults = 0;
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int min = getCaliValue();
|
||||
int max = getCaliValue();
|
||||
int *vResults = getCaliVariable();
|
||||
vmvar_t *vResults = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.SetRangeArray %p,%d,%d,%d,%p:", vAry, cnt, min, max, vResults);
|
||||
@@ -430,11 +430,11 @@ static void SetAndEquArray(void) { /* 15 */
|
||||
vResults : 結果を代入する配列
|
||||
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int mask = getCaliValue();
|
||||
int cnt = getCaliValue();
|
||||
int val = getCaliValue();
|
||||
int *vResults = getCaliVariable();
|
||||
vmvar_t *vResults = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.SetAndEquArray: %p,%d,%d,%d,%p:", vAry, mask, cnt, val, vResults);
|
||||
@@ -455,10 +455,10 @@ static void AndEquArray(void) { /* 16 */
|
||||
val : 比較する値
|
||||
vResults : 結果を代入する配列
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int val = getCaliValue();
|
||||
int *vResults = getCaliVariable();
|
||||
vmvar_t *vResults = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.AndEquArray %p,%d,%d,%p:", vAry, cnt, val, vResults);
|
||||
@@ -479,10 +479,10 @@ static void AndNotArray(void) { /* 17 */
|
||||
val : 比較する値
|
||||
vResults : 結果を代入する配列
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int val = getCaliValue();
|
||||
int *vResults = getCaliVariable();
|
||||
vmvar_t *vResults = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.AndNotArray %p,%d,%d,%p:", vAry, cnt, val, vResults);
|
||||
@@ -503,10 +503,10 @@ static void AndLowArray(void) { /* 18 */
|
||||
min : 最小値
|
||||
vResults : 結果を代入する配列
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int min = getCaliValue();
|
||||
int *vResults = getCaliVariable();
|
||||
vmvar_t *vResults = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.AndLowArray: %d,%d,%d,%d:", vAry, cnt, min, vResults);
|
||||
@@ -527,10 +527,10 @@ static void AndHighArray(void) { /* 19 */
|
||||
max : 最小値
|
||||
vResults : 結果を代入する配列
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int max = getCaliValue();
|
||||
int *vResults = getCaliVariable();
|
||||
vmvar_t *vResults = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.AndHighArray: %p,%d,%d,%p:", vAry, cnt, max, vResults);
|
||||
@@ -552,11 +552,11 @@ static void AndRangeArray(void) { /* 20 */
|
||||
max: 最大値
|
||||
vResults: 結果を返す配列
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int min = getCaliValue();
|
||||
int max = getCaliValue();
|
||||
int *vResults = getCaliVariable();
|
||||
vmvar_t *vResults = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.AndRangeArray %d,%d,%d,%d,%d:", vAry, cnt, min, max, vResults);
|
||||
@@ -579,11 +579,11 @@ static void AndAndEquArray(void) { /* 21 */
|
||||
vResults : 結果を代入する配列
|
||||
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int mask = getCaliValue();
|
||||
int cnt = getCaliValue();
|
||||
int val = getCaliValue();
|
||||
int *vResults = getCaliVariable();
|
||||
vmvar_t *vResults = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.AndAndEquArray: %d,%d,%d,%d,%d:", vAry, mask, cnt, val, vResults);
|
||||
@@ -612,10 +612,10 @@ static void OrNotArray(void) { /* 23 */
|
||||
val : 比較する値
|
||||
vResults: 結果を書き込む変数
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int val = getCaliValue();
|
||||
int *vResults = getCaliVariable();
|
||||
vmvar_t *vResults = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.OrNotArray %p,%d,%d,%p:", vAry, cnt, val, vResults);
|
||||
@@ -674,10 +674,10 @@ static void EnumEquArray(void) { /* 28 */
|
||||
val : 比較する値
|
||||
vResult: 一致する個数を返す変数
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int val = getCaliValue();
|
||||
int *vResult = getCaliVariable();
|
||||
vmvar_t *vResult = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.EnumEquArray %p,%d,%d,%p:", vAry, cnt, val, vResult);
|
||||
@@ -713,12 +713,12 @@ static void EnumEquNotArray2(void) { /* 30 */
|
||||
val2: 配列2と比較する値
|
||||
vResult: 条件に一致する数を返す変数
|
||||
*/
|
||||
int *vAry1 = getCaliVariable();
|
||||
int *vAry2 = getCaliVariable();
|
||||
vmvar_t *vAry1 = getCaliVariable();
|
||||
vmvar_t *vAry2 = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int val1 = getCaliValue();
|
||||
int val2 = getCaliValue();
|
||||
int *vResult = getCaliVariable();
|
||||
vmvar_t *vResult = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.EnumEquNotArray2 %p,%p,%d,%d,%d,%p:", vAry1, vAry2, cnt, val1, val2, vResult);
|
||||
@@ -742,10 +742,10 @@ static void EnumNotArray(void) { /* 31 */
|
||||
val: 比較する値
|
||||
vResult: 等しくないものの数
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int val = getCaliValue();
|
||||
int *vResult = getCaliVariable();
|
||||
vmvar_t *vResult = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.EnumNotArray %p, %d, %d, %p:", vAry, cnt, val, vResult);
|
||||
@@ -799,11 +799,11 @@ static void EnumRangeArray(void) { /* 35 */
|
||||
max : 最大値
|
||||
vResult: 一致した数を返す変数
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int min = getCaliValue();
|
||||
int max = getCaliValue();
|
||||
int *vResult = getCaliVariable();
|
||||
vmvar_t *vResult = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.EnumRangeArray %d,%d,%d,%d,%d:", vAry, cnt, min, max, vResult);
|
||||
@@ -830,11 +830,11 @@ static void GrepEquArray(void) { /* 36 */
|
||||
vMatch: 一致したインデックス
|
||||
vResult: 一つでも val と同じ値があれば 1
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int val = getCaliValue();
|
||||
int *vMatch = getCaliVariable();
|
||||
int *vResult = getCaliVariable();
|
||||
vmvar_t *vMatch = getCaliVariable();
|
||||
vmvar_t *vResult = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.GrepEquArray %p,%d,%d,%p,%p:", vAry, cnt, val, vMatch, vResult);
|
||||
@@ -862,11 +862,11 @@ static void GrepNotArray(void) { /* 37 */
|
||||
vMatch: 一致するindex
|
||||
vResult: 一つでも val と同じ値があれば 1
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int val = getCaliValue();
|
||||
int *vMatch = getCaliVariable();
|
||||
int *vResult = getCaliVariable();
|
||||
vmvar_t *vMatch = getCaliVariable();
|
||||
vmvar_t *vResult = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.GrepNotArray %p,%d,%d,%p,%p:", vAry, cnt, val, vMatch, vResult);
|
||||
@@ -930,11 +930,11 @@ static void GrepLowArray(void) { /* 41 */
|
||||
vMatch: 一致したインデックス
|
||||
vResult: 一つでも val と同じ値があれば 1
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int min = getCaliValue();
|
||||
int *vMatch = getCaliVariable();
|
||||
int *vResult = getCaliVariable();
|
||||
vmvar_t *vMatch = getCaliVariable();
|
||||
vmvar_t *vResult = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.GrepLowArray: %p,%d,%d,%p,%p:", vAry, cnt, min, vMatch, vResult);
|
||||
@@ -962,11 +962,11 @@ static void GrepHighArray(void) { /* 42 */
|
||||
vMatch: 一致したインデックス
|
||||
vResult: 一つでも val と同じ値があれば 1
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int max = getCaliValue();
|
||||
int *vMatch = getCaliVariable();
|
||||
int *vResult = getCaliVariable();
|
||||
vmvar_t *vMatch = getCaliVariable();
|
||||
vmvar_t *vResult = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.GrepHighArray: %p,%d,%d,%p,%p:", vAry, cnt, max, vMatch, vResult);
|
||||
@@ -995,12 +995,12 @@ static void GrepRangeArray(void) { /* 43 */
|
||||
vMatch: 一致したインデックス
|
||||
vResult: 一つでも val と同じ値があれば 1
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int min = getCaliValue();
|
||||
int max = getCaliValue();
|
||||
int *vMatch = getCaliVariable();
|
||||
int *vResult = getCaliVariable();
|
||||
vmvar_t *vMatch = getCaliVariable();
|
||||
vmvar_t *vResult = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShArray.GrepRangeArray %p,%d,%d,%d,%p,%p:", vAry, cnt, max, min, vMatch, vResult);
|
||||
@@ -1031,13 +1031,13 @@ static void GrepLowOrderArray(void) { /* 44 */
|
||||
vLastMatch: 最小値を示す配列のindex
|
||||
vResult: 最小値が見つかれば 1, 見つからなければ 0
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int min = getCaliValue();
|
||||
int max = getCaliValue();
|
||||
int *v1 = getCaliVariable();
|
||||
int *vLastMatch = getCaliVariable();
|
||||
int *vResult = getCaliVariable();
|
||||
vmvar_t *v1 = getCaliVariable();
|
||||
vmvar_t *vLastMatch = getCaliVariable();
|
||||
vmvar_t *vResult = getCaliVariable();
|
||||
int i, j, k = 0;
|
||||
|
||||
TRACE("ShArray.GrepLowOrderArray %p,%d,%d,%d,%p,%p,%p:", vAry, cnt, min, max, v1, vLastMatch, vResult);
|
||||
@@ -1082,13 +1082,13 @@ static void GrepHighOrderArray(void) { /* 45 */
|
||||
vLastMatch: 最大値を示す配列のindex
|
||||
vResult: 最大値が見つかれば 1, 見つからなければ 0
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int min = getCaliValue();
|
||||
int max = getCaliValue();
|
||||
int *v1 = getCaliVariable();
|
||||
int *vLastMatch = getCaliVariable();
|
||||
int *vResult = getCaliVariable();
|
||||
vmvar_t *v1 = getCaliVariable();
|
||||
vmvar_t *vLastMatch = getCaliVariable();
|
||||
vmvar_t *vResult = getCaliVariable();
|
||||
int i, j, k = 0;
|
||||
|
||||
TRACE("ShArray.GrepHighOrderArray %p,%d,%d,%d,%p,%p,%p:", vAry, cnt, min, max, v1, vLastMatch, vResult);
|
||||
@@ -1119,7 +1119,7 @@ static void GrepHighOrderArray(void) { /* 45 */
|
||||
}
|
||||
|
||||
static void ChangeEquArray(void) { /* 46 */
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int src = getCaliValue();
|
||||
int dst = getCaliValue();
|
||||
@@ -1172,7 +1172,7 @@ static void ChangeRangeArray(void) { /* 50 */
|
||||
max : 最大値
|
||||
val : 置き換える値
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int min = getCaliValue();
|
||||
int max = getCaliValue();
|
||||
@@ -1202,12 +1202,12 @@ static void CopyArrayToRect(void) { /* 51 */
|
||||
dw : コピー先 width
|
||||
dh : コピー先 height
|
||||
*/
|
||||
int *vSrc = getCaliVariable();
|
||||
vmvar_t *vSrc = getCaliVariable();
|
||||
int sw = getCaliValue();
|
||||
int sh = getCaliValue();
|
||||
int sx = getCaliValue();
|
||||
int sy = getCaliValue();
|
||||
int *vDst = getCaliVariable();
|
||||
vmvar_t *vDst = getCaliVariable();
|
||||
int dw = getCaliValue();
|
||||
int dh = getCaliValue();
|
||||
int x, y;
|
||||
@@ -1236,10 +1236,10 @@ static void CopyRectToArray(void) { /* 52 */
|
||||
dx : コピー先 x
|
||||
dy : コピー先 y
|
||||
*/
|
||||
int *vSrc = getCaliVariable();
|
||||
vmvar_t *vSrc = getCaliVariable();
|
||||
int sw = getCaliValue();
|
||||
int sh = getCaliValue();
|
||||
int *vDst = getCaliVariable();
|
||||
vmvar_t *vDst = getCaliVariable();
|
||||
int dw = getCaliValue();
|
||||
int dh = getCaliValue();
|
||||
int dx = getCaliValue();
|
||||
@@ -1266,10 +1266,10 @@ static void ChangeSecretArray(void) { /* 53 */
|
||||
type: 機能番号
|
||||
vResult: 結果を返す変数
|
||||
*/
|
||||
int *vAry = getCaliVariable();
|
||||
vmvar_t *vAry = getCaliVariable();
|
||||
int cnt = getCaliValue();
|
||||
int type = getCaliValue();
|
||||
int *vResult = getCaliVariable();
|
||||
vmvar_t *vResult = getCaliVariable();
|
||||
static uint16_t key[4] = { 0x7A7A, 0xADAD, 0xBCBC, 0xCECE }; /* key */
|
||||
|
||||
TRACE("ShArray.ChangeSecretArray %p,%d,%d,%p:", vAry, cnt, type, vResult);
|
||||
|
||||
+19
-19
@@ -50,7 +50,7 @@ static int64_t div64(int64_t a1, int64_t a2) {
|
||||
return a1 / a2;
|
||||
}
|
||||
|
||||
static int64_t get32(int *var) {
|
||||
static int64_t get32(vmvar_t *var) {
|
||||
return var[0] + mul64(var[1], 0x10000);
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ static void SetIntNum16(void) { /* 1 */
|
||||
|
||||
var: 数値の入った変数
|
||||
*/
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
accumulator = mul64(*var, numbase);
|
||||
|
||||
@@ -82,7 +82,7 @@ static void SetIntNum16(void) { /* 1 */
|
||||
}
|
||||
|
||||
static void SetIntNum32(void) { /* 2 */
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
accumulator = mul64(get32(var), numbase);
|
||||
|
||||
@@ -95,7 +95,7 @@ static void GetIntNum16(void) { /* 3 */
|
||||
|
||||
var: 数値をいれる変数
|
||||
*/
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
int64_t i;
|
||||
|
||||
i = div64(accumulator, numbase);
|
||||
@@ -110,7 +110,7 @@ static void GetIntNum16(void) { /* 3 */
|
||||
}
|
||||
|
||||
static void GetIntNum32(void) { /* 4 */
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
int64_t i = div64(accumulator, numbase);
|
||||
|
||||
var[0] = i & 0xFFFF;
|
||||
@@ -126,7 +126,7 @@ static void AddIntNum16(void) { /* 5 */
|
||||
|
||||
var: 足す数の入った変数
|
||||
*/
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
accumulator += mul64(*var, numbase);
|
||||
|
||||
@@ -134,7 +134,7 @@ static void AddIntNum16(void) { /* 5 */
|
||||
}
|
||||
|
||||
static void AddIntNum32(void) { /* 6 */
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
accumulator += mul64(get32(var), numbase);
|
||||
|
||||
@@ -148,7 +148,7 @@ static void SubIntNum16(void) { /* 7 */
|
||||
}
|
||||
|
||||
static void SubIntNum32(void) { /* 8 */
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
accumulator -= mul64(get32(var), numbase);
|
||||
|
||||
@@ -161,7 +161,7 @@ static void MulIntNum16(void) { /* 9 */
|
||||
|
||||
var: 掛ける数の入った変数
|
||||
*/
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
accumulator *= mul64(*var, numbase);
|
||||
|
||||
@@ -180,7 +180,7 @@ static void DivIntNum16(void) { /* 11 */
|
||||
|
||||
var: 足す数の入った変数
|
||||
*/
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
int64_t i;
|
||||
|
||||
i = mul64(*var, numbase);
|
||||
@@ -204,9 +204,9 @@ static void CmpIntNum16(void) { /* 13 */
|
||||
}
|
||||
|
||||
static void CmpIntNum32(void) { /* 14 */
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
int op = getCaliValue();
|
||||
int *result = getCaliVariable();
|
||||
vmvar_t *result = getCaliVariable();
|
||||
|
||||
int64_t val = mul64(get32(var), numbase);
|
||||
|
||||
@@ -230,8 +230,8 @@ static void GetLengthNum16(void) { /* 15 */
|
||||
var: 数値
|
||||
vResult: 数値の桁数を返す変数
|
||||
*/
|
||||
int *var = getCaliVariable();
|
||||
int *vResult = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
vmvar_t *vResult = getCaliVariable();
|
||||
|
||||
if (*var >= 10000) {
|
||||
*vResult = 5;
|
||||
@@ -270,7 +270,7 @@ static void NumToRate(void) { /* 17 */
|
||||
int p2 = getCaliValue();
|
||||
int p3 = getCaliValue();
|
||||
int flag = getCaliValue();
|
||||
int *vResult = getCaliVariable();
|
||||
vmvar_t *vResult = getCaliVariable();
|
||||
int i;
|
||||
|
||||
i = (p1 * p3) / p2;
|
||||
@@ -302,7 +302,7 @@ static void NumToRateNum(void) { /* 18 */
|
||||
int p2 = getCaliValue();
|
||||
int p3 = getCaliValue();
|
||||
int flag = getCaliValue();
|
||||
int *vResult = getCaliVariable();
|
||||
vmvar_t *vResult = getCaliVariable();
|
||||
int i;
|
||||
|
||||
i = (p1 * p2) / p3;
|
||||
@@ -333,7 +333,7 @@ static void SetRandomSeed() {
|
||||
|
||||
static void GetRandomNumA() {
|
||||
int num = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
if (num == 0 || num == 1) {
|
||||
*var = num;
|
||||
@@ -351,7 +351,7 @@ static void NumToBit() {
|
||||
var: 値を返す変数
|
||||
*/
|
||||
int beki = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
int i, j = 1;
|
||||
|
||||
if (beki < 17) {
|
||||
@@ -379,7 +379,7 @@ static void BitToNum() {
|
||||
var: 値を返す変数
|
||||
*/
|
||||
int val = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
int i;
|
||||
|
||||
TRACE("ShCalc.BitToNum %d,%p:", val, var);
|
||||
|
||||
+14
-14
@@ -59,8 +59,8 @@ struct animsrc {
|
||||
static struct animsrc src[SLOT]; /* アニメーション各コマ転送元 */
|
||||
|
||||
struct _s0 {
|
||||
int *dst_p1;
|
||||
int *dst_p2;
|
||||
vmvar_t *dst_p1;
|
||||
vmvar_t *dst_p2;
|
||||
int dw_1000A188;
|
||||
};
|
||||
static struct _s0 s0[SLOT];
|
||||
@@ -84,15 +84,15 @@ struct _s2 {
|
||||
};
|
||||
static struct _s2 s2[SLOT];
|
||||
|
||||
static int* add_p5[SLOT]; /* どこまでアニメーションのコマが進んだか */
|
||||
static vmvar_t *add_p5[SLOT]; /* どこまでアニメーションのコマが進んだか */
|
||||
|
||||
static void copy_sprite(int sx, int sy, int width, int height, int dx, int dy, int r, int g, int b) {
|
||||
SDL_Surface *sf = nact->ags.dib->sdl_surface;
|
||||
SDL_SetColorKey(sf, SDL_TRUE, SDL_MapRGB(sf->format, r, g, b));
|
||||
sdl_set_surface_color_key(sf, true, sdl_map_rgb(sf, r, g, b));
|
||||
SDL_Rect src_rect = { sx, sy, width, height };
|
||||
SDL_Rect dst_rect = { dx, dy, width, height };
|
||||
SDL_BlitSurface(sf, &src_rect, sf, &dst_rect);
|
||||
SDL_SetColorKey(sf, SDL_FALSE, 0);
|
||||
sdl_set_surface_color_key(sf, false, 0);
|
||||
}
|
||||
|
||||
static void Init() {
|
||||
@@ -115,8 +115,8 @@ static void ChangeEquColor() {
|
||||
int p2 = getCaliValue();
|
||||
int p3 = getCaliValue();
|
||||
int p4 = getCaliValue();
|
||||
int *p5 = getCaliVariable();
|
||||
int *p6 = getCaliVariable();
|
||||
vmvar_t *p5 = getCaliVariable();
|
||||
vmvar_t *p6 = getCaliVariable();
|
||||
int p7 = getCaliValue(); /* ISurface */
|
||||
|
||||
TRACE_UNIMPLEMENTED("ShGraph.ChangeEquColor %d,%d,%d,%d,%p,%p,%d:", p1, p2, p3, p4, p5, p6, p7);
|
||||
@@ -137,8 +137,8 @@ static void ChangeNotColor() {
|
||||
int y0 = getCaliValue();
|
||||
int width = getCaliValue();
|
||||
int height = getCaliValue();
|
||||
int *src = getCaliVariable(); /* r, g, b */
|
||||
int *dst = getCaliVariable(); /* r, g, b */
|
||||
vmvar_t *src = getCaliVariable(); /* r, g, b */
|
||||
vmvar_t *dst = getCaliVariable(); /* r, g, b */
|
||||
int p7 = getCaliValue(); /* ISurface */
|
||||
surface_t *dib;
|
||||
int x, y;
|
||||
@@ -216,7 +216,7 @@ static void ResetAnimeData() {
|
||||
}
|
||||
|
||||
memset(s1, 0, sizeof(struct _s1) * SLOT);
|
||||
memset(add_p5, 0, sizeof(int *) * SLOT);
|
||||
memset(add_p5, 0, sizeof(vmvar_t *) * SLOT);
|
||||
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ static void SetAnimeSrc() {
|
||||
int h = getCaliValue();
|
||||
int uw = getCaliValue();
|
||||
int uh = getCaliValue();
|
||||
int *pal = getCaliVariable();
|
||||
vmvar_t *pal = getCaliVariable();
|
||||
int r, g, b;
|
||||
|
||||
TRACE("ShGraph.SetAnimeSrc %d,%d,%d,%d,%d,%d,%d,%p:", no, x0, y0, w, h, uw, uh, pal);
|
||||
@@ -276,8 +276,8 @@ static void SetAnimeDst() {
|
||||
p6: 描画先オフセット追加分 (h) 10000が中央
|
||||
*/
|
||||
int no = getCaliValue();
|
||||
int *p1 = getCaliVariable();
|
||||
int *p2 = getCaliVariable();
|
||||
vmvar_t *p1 = getCaliVariable();
|
||||
vmvar_t *p2 = getCaliVariable();
|
||||
int p3 = getCaliValue();
|
||||
int p4 = getCaliValue();
|
||||
int p5 = getCaliValue();
|
||||
@@ -313,7 +313,7 @@ static void AddAnimeData() {
|
||||
int p2 = getCaliValue();
|
||||
int p3 = getCaliValue();
|
||||
int p4 = getCaliValue();
|
||||
int *p5 = getCaliVariable();
|
||||
vmvar_t *p5 = getCaliVariable();
|
||||
int p6 = getCaliValue();
|
||||
int i;
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
#include "xsystem35.h"
|
||||
#include "modules.h"
|
||||
#include "input.h"
|
||||
#include "menu.h"
|
||||
#include "input_modal.h"
|
||||
|
||||
// キー変換テーブル
|
||||
#define KEYMAP_MAX 8
|
||||
@@ -47,7 +47,7 @@ static void OutputMessageBox(void) { /* 0 */
|
||||
int p2 = getCaliValue();
|
||||
int title = getCaliValue();
|
||||
int msg = getCaliValue();
|
||||
int *res = getCaliVariable();
|
||||
vmvar_t *res = getCaliVariable();
|
||||
int ISys3xSystem = getCaliValue();
|
||||
|
||||
char *title_utf8 = toUTF8(svar_get(title));
|
||||
@@ -64,10 +64,10 @@ static void OutputMessageBox(void) { /* 0 */
|
||||
static void InputListNum(void) { /* 1 */
|
||||
int flags = getCaliValue();
|
||||
int title = getCaliValue();
|
||||
int *val = getCaliVariable();
|
||||
vmvar_t *val = getCaliVariable();
|
||||
int minval = getCaliValue();
|
||||
int maxval = getCaliValue();
|
||||
int *res = getCaliVariable();
|
||||
vmvar_t *res = getCaliVariable();
|
||||
int ISys3xSystem = getCaliValue();
|
||||
|
||||
INPUTNUM_PARAM ni_param = {
|
||||
@@ -77,12 +77,11 @@ static void InputListNum(void) { /* 1 */
|
||||
.title = toUTF8(svar_get(title)),
|
||||
};
|
||||
|
||||
menu_inputnumber(&ni_param);
|
||||
if (ni_param.value < 0) {
|
||||
*res = 0;
|
||||
} else {
|
||||
if (input_modal_number(&ni_param)) {
|
||||
*val = (uint16_t)ni_param.value;
|
||||
*res = 1;
|
||||
} else {
|
||||
*res = 0;
|
||||
}
|
||||
|
||||
free(ni_param.title);
|
||||
@@ -152,7 +151,7 @@ static void SetKeyStatus(void) {
|
||||
*/
|
||||
static void GetKeyStatus(void) {
|
||||
int no = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
int i;
|
||||
|
||||
if (no >= KEYMAP_MAX) {
|
||||
|
||||
@@ -25,8 +25,9 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "LittleEndian.h"
|
||||
@@ -44,7 +45,7 @@ static struct {
|
||||
} memwav;
|
||||
|
||||
static void free_memory_wav(void) {
|
||||
SDL_FreeWAV(memwav.buf);
|
||||
sdl_free_wav(memwav.buf);
|
||||
memwav.buf = NULL;
|
||||
}
|
||||
|
||||
@@ -132,11 +133,15 @@ static void wavLoadMemory() {
|
||||
WARNING("cannot open WAVE %d", no - 1);
|
||||
return;
|
||||
}
|
||||
if (!SDL_LoadWAV_RW(SDL_RWFromConstMem(dfile->data, dfile->size), 1, &memwav.spec, &memwav.buf, &memwav.len)) {
|
||||
sdl_iostream_t *stream = sdl_io_from_const_memory(
|
||||
dfile->data, dfile->size);
|
||||
if (!stream || !sdl_load_wav_io(
|
||||
stream, true, &memwav.spec, &memwav.buf, &memwav.len)) {
|
||||
WARNING("cannot load WAVE %d", no - 1);
|
||||
return;
|
||||
}
|
||||
if (memwav.spec.channels != 2 || memwav.spec.format != AUDIO_S16LSB) {
|
||||
if (memwav.spec.channels != 2 ||
|
||||
memwav.spec.format != sdl_audio_s16le_format()) {
|
||||
WARNING("unexpected audio format");
|
||||
free_memory_wav();
|
||||
return;
|
||||
@@ -268,7 +273,7 @@ static void wavPlayRing() {
|
||||
*/
|
||||
int start = getCaliValue();
|
||||
int cnt = getCaliValue();
|
||||
int *cur = getCaliVariable();
|
||||
vmvar_t *cur = getCaliVariable();
|
||||
|
||||
mus_wav_play(start + (*cur % cnt), 1);
|
||||
*cur = (*cur + 1) % cnt;
|
||||
@@ -321,7 +326,7 @@ static void wavIsPlay() {
|
||||
*result: 0なら停止中、!0なら再生中
|
||||
*/
|
||||
int slot = getCaliValue();
|
||||
int *result = getCaliVariable();
|
||||
vmvar_t *result = getCaliVariable();
|
||||
|
||||
*result = mus_wav_get_playposition(slot);
|
||||
|
||||
@@ -338,7 +343,7 @@ static void wavIsPlayRange() {
|
||||
*/
|
||||
int slot = getCaliValue();
|
||||
int range = getCaliValue();
|
||||
int *result = getCaliVariable();
|
||||
vmvar_t *result = getCaliVariable();
|
||||
int i, ret = 0;
|
||||
|
||||
for (i = slot; i < (slot + range); i++) {
|
||||
|
||||
@@ -127,7 +127,7 @@ static void SetStringNum16(void) {
|
||||
p2: 変換された数値を格納する変数
|
||||
*/
|
||||
int st = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
const char *str = svar_get(st);
|
||||
char _dst[100];
|
||||
char *dst = _dst;
|
||||
@@ -160,7 +160,7 @@ static void SetStringNum16(void) {
|
||||
|
||||
static void SetStringNum32(void) {
|
||||
int p1 = getCaliValue();
|
||||
int *p2 = getCaliVariable();
|
||||
vmvar_t *p2 = getCaliVariable();
|
||||
|
||||
TRACE_UNIMPLEMENTED("ShString.SetStringNum32: %d,%p:", p1, p2);
|
||||
}
|
||||
|
||||
+60
-54
@@ -2,7 +2,7 @@
|
||||
|
||||
#include <stdio.h>
|
||||
#include <math.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
@@ -31,7 +31,7 @@ static void render_quad(SDL_Texture *tex, SDL_Vertex verts[4]) {
|
||||
static void fade_overlay(Uint8 r, Uint8 g, Uint8 b, Uint8 alpha) {
|
||||
SDL_SetRenderDrawBlendMode(gfx_renderer, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderDrawColor(gfx_renderer, r, g, b, alpha);
|
||||
SDL_RenderFillRect(gfx_renderer, NULL);
|
||||
sdl_render_fill_rect(gfx_renderer, NULL);
|
||||
}
|
||||
|
||||
#define NR_SCENES 13
|
||||
@@ -94,7 +94,7 @@ static void Init() {
|
||||
int p1 = getCaliValue();
|
||||
int p2 = getCaliValue();
|
||||
int p3 = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
if (!nact->files.alk[0]) {
|
||||
WARNING("dDEMO.alk not found");
|
||||
@@ -121,7 +121,7 @@ static void Init() {
|
||||
continue;
|
||||
}
|
||||
dd.images[i] = SDL_CreateTextureFromSurface(gfx_renderer, sf);
|
||||
SDL_FreeSurface(sf);
|
||||
sdl_destroy_surface(sf);
|
||||
}
|
||||
*var = 1;
|
||||
TRACE("dDemo.Init %d,%d,%d,%p:", p1, p2, p3, var);
|
||||
@@ -144,7 +144,7 @@ static void render_affine_bg(SDL_Texture *scroll_bg, int elapsed, int duration,
|
||||
if (!scroll_bg) return;
|
||||
|
||||
int bg_w, bg_h;
|
||||
SDL_QueryTexture(scroll_bg, NULL, NULL, &bg_w, &bg_h);
|
||||
sdl_get_texture_size(scroll_bg, &bg_w, &bg_h);
|
||||
|
||||
int xs[4], ys[4];
|
||||
const float angle = angle_deg * (float)M_PI / 180.0f;
|
||||
@@ -183,7 +183,7 @@ static void render_affine_bg(SDL_Texture *scroll_bg, int elapsed, int duration,
|
||||
for (int i = 0; i < 4; i++) {
|
||||
verts[i].position.x = (i == 1 || i == 2) ? w : 0;
|
||||
verts[i].position.y = (i == 2 || i == 3) ? h : (float)y_toggle;
|
||||
verts[i].color = (SDL_Color){255, 255, 255, 255};
|
||||
verts[i].color = sdl_vertex_color(255, 255, 255, 255);
|
||||
verts[i].tex_coord.x = uv[i][0];
|
||||
verts[i].tex_coord.y = uv[i][1];
|
||||
}
|
||||
@@ -194,7 +194,7 @@ static void render_affine_bg(SDL_Texture *scroll_bg, int elapsed, int duration,
|
||||
y_toggle = (y_toggle + 1) & 1;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
verts[i].position.y = (i == 2 || i == 3) ? h : (float)y_toggle;
|
||||
verts[i].color = (SDL_Color){255, 255, 255, 128};
|
||||
verts[i].color = sdl_vertex_color(255, 255, 255, 128);
|
||||
}
|
||||
// Swap mapping: TL<->BR, TR<->BL for 180° rotation effect
|
||||
verts[0].tex_coord.x = uv[2][0]; verts[0].tex_coord.y = uv[2][1];
|
||||
@@ -212,7 +212,7 @@ static void render_scene0(int elapsed) {
|
||||
if (!sprite) return;
|
||||
|
||||
int spr_w, spr_h;
|
||||
SDL_QueryTexture(sprite, NULL, NULL, &spr_w, &spr_h);
|
||||
sdl_get_texture_size(sprite, &spr_w, &spr_h);
|
||||
const int slide_end = 4700;
|
||||
const int duration = scene_duration[0]; // 5400
|
||||
|
||||
@@ -234,12 +234,12 @@ static void render_scene0(int elapsed) {
|
||||
int dest_w = a0 - ((a0 - spr_w) * elapsed) / slide_end;
|
||||
if (dest_w <= 0) continue;
|
||||
SDL_Rect dst = {view_w - 2 * dest_w, dest_y, dest_w, spr_h};
|
||||
SDL_RenderCopy(gfx_renderer, sprite, NULL, &dst);
|
||||
sdl_render_texture(gfx_renderer, sprite, NULL, &dst);
|
||||
}
|
||||
} else {
|
||||
SDL_SetTextureAlphaMod(sprite, 0xd0);
|
||||
SDL_Rect dst = {view_w - 2 * spr_w, dest_y, spr_w, spr_h};
|
||||
SDL_RenderCopy(gfx_renderer, sprite, NULL, &dst);
|
||||
sdl_render_texture(gfx_renderer, sprite, NULL, &dst);
|
||||
}
|
||||
|
||||
// 3. Horizontal accent line (grows rightward over 4700ms)
|
||||
@@ -247,12 +247,12 @@ static void render_scene0(int elapsed) {
|
||||
SDL_SetRenderDrawBlendMode(gfx_renderer, SDL_BLENDMODE_NONE);
|
||||
SDL_SetRenderDrawColor(gfx_renderer, 0xc0, 0xc0, 0xc0, 0xff);
|
||||
SDL_Rect hline = {0, (spr_h + view_h) / 2 - 4, line_w, 2};
|
||||
SDL_RenderFillRect(gfx_renderer, &hline);
|
||||
sdl_render_fill_rect(gfx_renderer, &hline);
|
||||
|
||||
// 4. Vertical accent line (right edge of sprite, grows downward)
|
||||
int line_h = elapsed < slide_end ? 479 * elapsed / slide_end + 1 : 480;
|
||||
SDL_Rect vline = {view_w - spr_w, 0, 2, line_h};
|
||||
SDL_RenderFillRect(gfx_renderer, &vline);
|
||||
sdl_render_fill_rect(gfx_renderer, &vline);
|
||||
}
|
||||
|
||||
static void render_scene1(int elapsed) {
|
||||
@@ -261,7 +261,7 @@ static void render_scene1(int elapsed) {
|
||||
if (!sprite) return;
|
||||
|
||||
int spr_w, spr_h;
|
||||
SDL_QueryTexture(sprite, NULL, NULL, &spr_w, &spr_h);
|
||||
sdl_get_texture_size(sprite, &spr_w, &spr_h);
|
||||
const int duration = scene_duration[1]; // 4752
|
||||
|
||||
// --- Background: DrawAffineSurfaceAlpha50 x2 (alpha=50%) ---
|
||||
@@ -275,7 +275,7 @@ static void render_scene1(int elapsed) {
|
||||
SDL_SetTextureBlendMode(sprite, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetTextureAlphaMod(sprite, (Uint8)alpha);
|
||||
SDL_Rect dst = {(view_w - spr_w) / 2, (view_h - spr_h) / 2, spr_w, spr_h};
|
||||
SDL_RenderCopy(gfx_renderer, sprite, NULL, &dst);
|
||||
sdl_render_texture(gfx_renderer, sprite, NULL, &dst);
|
||||
}
|
||||
|
||||
// Fade to black: graph_FillAlphaColor(0,0,0), starts at 4000ms.
|
||||
@@ -397,12 +397,14 @@ static void render_scene_2_4_6_3d(int elapsed, int phase_dur, SDL_Texture *tex1,
|
||||
// Object border quad: solid color RGB(244,221,181) = BGR(0xb5,0xdd,0xf4),
|
||||
// drawn first as a backdrop. The bg quad drawn on top leaves only the thin
|
||||
// border ring visible.
|
||||
for (int i = 0; i < 4; i++) obj_verts[i].color = (SDL_Color){0xf4, 0xdd, 0xb5, 0xff};
|
||||
for (int i = 0; i < 4; i++)
|
||||
obj_verts[i].color = sdl_vertex_color(0xf4, 0xdd, 0xb5, 0xff);
|
||||
SDL_SetRenderDrawBlendMode(gfx_renderer, SDL_BLENDMODE_NONE);
|
||||
render_quad(NULL, obj_verts);
|
||||
|
||||
// Background quad: textured, opaque, drawn on top (covers center of border quad)
|
||||
for (int i = 0; i < 4; i++) bg_verts[i].color = (SDL_Color){255, 255, 255, 255};
|
||||
for (int i = 0; i < 4; i++)
|
||||
bg_verts[i].color = sdl_vertex_color(255, 255, 255, 255);
|
||||
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_NONE);
|
||||
render_quad(tex, bg_verts);
|
||||
}
|
||||
@@ -425,7 +427,7 @@ static void render_scene_2_4_6(int elapsed, int scene_idx, int sprite_idx, int t
|
||||
if (bg) {
|
||||
SDL_SetTextureBlendMode(bg, SDL_BLENDMODE_NONE);
|
||||
SDL_SetTextureColorMod(bg, 0xff, 0xff, 0xff);
|
||||
SDL_RenderCopy(gfx_renderer, bg, NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, bg, NULL, NULL);
|
||||
}
|
||||
|
||||
// 2. 3D rotating quad animation (8-phase timing)
|
||||
@@ -439,23 +441,23 @@ static void render_scene_2_4_6(int elapsed, int scene_idx, int sprite_idx, int t
|
||||
// 3. Scrolling sprite: x moves from -sprite_w (off-screen left) to view_w (off-screen right)
|
||||
int sprite_w = 0, sprite_h = 0;
|
||||
if (sprite)
|
||||
SDL_QueryTexture(sprite, NULL, NULL, &sprite_w, &sprite_h);
|
||||
sdl_get_texture_size(sprite, &sprite_w, &sprite_h);
|
||||
int x = sprite_w ? ((sprite_w + view_w) * elapsed) / duration - sprite_w : 0;
|
||||
|
||||
// Shadow drawn first (color-modded to black), sprite on top (full color)
|
||||
if (sprite_shadow) {
|
||||
int sw, sh;
|
||||
SDL_QueryTexture(sprite_shadow, NULL, NULL, &sw, &sh);
|
||||
sdl_get_texture_size(sprite_shadow, &sw, &sh);
|
||||
SDL_SetTextureBlendMode(sprite_shadow, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetTextureColorMod(sprite_shadow, 0, 0, 0);
|
||||
SDL_Rect dst = {x, 0, sw, sh};
|
||||
SDL_RenderCopy(gfx_renderer, sprite_shadow, NULL, &dst);
|
||||
sdl_render_texture(gfx_renderer, sprite_shadow, NULL, &dst);
|
||||
}
|
||||
if (sprite) {
|
||||
SDL_SetTextureBlendMode(sprite, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetTextureColorMod(sprite, 0xff, 0xff, 0xff);
|
||||
SDL_Rect dst = {x, 0, sprite_w, sprite_h};
|
||||
SDL_RenderCopy(gfx_renderer, sprite, NULL, &dst);
|
||||
sdl_render_texture(gfx_renderer, sprite, NULL, &dst);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,7 +492,7 @@ static void render_scene_3_5_7_9(int elapsed, int image_idx) {
|
||||
if (alpha > 0) {
|
||||
SDL_SetTextureBlendMode(main_img, alpha < 255 ? SDL_BLENDMODE_BLEND : SDL_BLENDMODE_NONE);
|
||||
SDL_SetTextureAlphaMod(main_img, (Uint8)alpha);
|
||||
SDL_RenderCopy(gfx_renderer, main_img, NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, main_img, NULL, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,10 +502,14 @@ static void render_scene_3_5_7_9(int elapsed, int image_idx) {
|
||||
float t3 = (float)elapsed / (4000.0f * 3.0f); // 0 → 1/3
|
||||
float w = (float)view_w, h = (float)view_h;
|
||||
SDL_Vertex verts[4] = {
|
||||
/* TL */ {{0.f, 0.f}, {255, 255, 255, 128}, {t3, 0.f }},
|
||||
/* TR */ {{w, 0.f}, {255, 255, 255, 128}, {1.0f, t3 }},
|
||||
/* BR */ {{w, h }, {255, 255, 255, 128}, {1.0f - t3, 1.0f }},
|
||||
/* BL */ {{0.f, h }, {255, 255, 255, 128}, {0.f, 1.0f-t3}},
|
||||
{{0.f, 0.f}, sdl_vertex_color(255, 255, 255, 128),
|
||||
{t3, 0.f}},
|
||||
{{w, 0.f}, sdl_vertex_color(255, 255, 255, 128),
|
||||
{1.0f, t3}},
|
||||
{{w, h}, sdl_vertex_color(255, 255, 255, 128),
|
||||
{1.0f - t3, 1.0f}},
|
||||
{{0.f, h}, sdl_vertex_color(255, 255, 255, 128),
|
||||
{0.f, 1.0f - t3}},
|
||||
};
|
||||
SDL_SetTextureBlendMode(overlay, SDL_BLENDMODE_BLEND);
|
||||
render_quad(overlay, verts);
|
||||
@@ -527,7 +533,7 @@ static void render_scene8(int elapsed) {
|
||||
if (imgs[0]) {
|
||||
SDL_SetTextureBlendMode(imgs[0], SDL_BLENDMODE_BLEND);
|
||||
SDL_SetTextureAlphaMod(imgs[0], (Uint8)SDL_min(alpha, 255));
|
||||
SDL_RenderCopy(gfx_renderer, imgs[0], NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, imgs[0], NULL, NULL);
|
||||
}
|
||||
} else if (elapsed < 6 * seg) {
|
||||
// Segments 2–6: crossfade image[idx] → image[idx+1]
|
||||
@@ -536,19 +542,19 @@ static void render_scene8(int elapsed) {
|
||||
if (imgs[idx]) {
|
||||
SDL_SetTextureBlendMode(imgs[idx], SDL_BLENDMODE_NONE);
|
||||
SDL_SetTextureAlphaMod(imgs[idx], 255);
|
||||
SDL_RenderCopy(gfx_renderer, imgs[idx], NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, imgs[idx], NULL, NULL);
|
||||
}
|
||||
if (imgs[idx + 1]) {
|
||||
SDL_SetTextureBlendMode(imgs[idx + 1], SDL_BLENDMODE_BLEND);
|
||||
SDL_SetTextureAlphaMod(imgs[idx + 1], (Uint8)SDL_min(alpha, 255));
|
||||
SDL_RenderCopy(gfx_renderer, imgs[idx + 1], NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, imgs[idx + 1], NULL, NULL);
|
||||
}
|
||||
} else {
|
||||
// Segment 7: hold image[5] at full opacity
|
||||
if (imgs[5]) {
|
||||
SDL_SetTextureBlendMode(imgs[5], SDL_BLENDMODE_NONE);
|
||||
SDL_SetTextureAlphaMod(imgs[5], 255);
|
||||
SDL_RenderCopy(gfx_renderer, imgs[5], NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, imgs[5], NULL, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -588,7 +594,7 @@ static void render_scene10(int elapsed) {
|
||||
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_NONE);
|
||||
SDL_SetTextureAlphaMod(tex, 255);
|
||||
SDL_SetTextureColorMod(tex, 255, 255, 255);
|
||||
SDL_RenderCopy(gfx_renderer, tex, NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, tex, NULL, NULL);
|
||||
}
|
||||
} else if (elapsed < 9610) {
|
||||
// Phase 2: zoom in
|
||||
@@ -607,7 +613,7 @@ static void render_scene10(int elapsed) {
|
||||
SDL_SetTextureBlendMode(surface3, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetTextureAlphaMod(surface3, 255);
|
||||
SDL_SetTextureColorMod(surface3, 255, 255, 255);
|
||||
SDL_RenderCopy(gfx_renderer, surface3, &src_rect, NULL);
|
||||
sdl_render_texture(gfx_renderer, surface3, &src_rect, NULL);
|
||||
}
|
||||
} else {
|
||||
// Phase 3: fade out to black
|
||||
@@ -618,7 +624,7 @@ static void render_scene10(int elapsed) {
|
||||
combined < 255 ? SDL_BLENDMODE_BLEND : SDL_BLENDMODE_NONE);
|
||||
SDL_SetTextureAlphaMod(surface3, (Uint8)combined);
|
||||
SDL_SetTextureColorMod(surface3, 255, 255, 255);
|
||||
SDL_RenderCopy(gfx_renderer, surface3, NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, surface3, NULL, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -644,12 +650,12 @@ static void render_scene11(int elapsed) {
|
||||
// srcY = (bg_h - view_h) * (duration - elapsed) / duration
|
||||
if (bg) {
|
||||
int bg_w, bg_h;
|
||||
SDL_QueryTexture(bg, NULL, NULL, &bg_w, &bg_h);
|
||||
sdl_get_texture_size(bg, &bg_w, &bg_h);
|
||||
int scroll = (int)((long long)(bg_h - view_h) * elapsed / duration);
|
||||
int src_y = SDL_max(0, (bg_h - view_h) - scroll);
|
||||
SDL_Rect src = {0, src_y, SDL_min(view_w, bg_w), view_h};
|
||||
SDL_SetTextureBlendMode(bg, SDL_BLENDMODE_NONE);
|
||||
SDL_RenderCopy(gfx_renderer, bg, &src, NULL);
|
||||
sdl_render_texture(gfx_renderer, bg, &src, NULL);
|
||||
}
|
||||
|
||||
if (elapsed < unit) {
|
||||
@@ -659,12 +665,12 @@ static void render_scene11(int elapsed) {
|
||||
SDL_Texture *tex = slide[2];
|
||||
if (tex) {
|
||||
int tw, th;
|
||||
SDL_QueryTexture(tex, NULL, NULL, &tw, &th);
|
||||
sdl_get_texture_size(tex, &tw, &th);
|
||||
int y = (int)((long long)(th + view_h) * (elapsed - unit) / unit) - th;
|
||||
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetTextureAlphaMod(tex, 0x80);
|
||||
SDL_Rect dst = {(view_w - tw) / 2, y, tw, th};
|
||||
SDL_RenderCopy(gfx_renderer, tex, NULL, &dst);
|
||||
sdl_render_texture(gfx_renderer, tex, NULL, &dst);
|
||||
}
|
||||
} else if (elapsed < unit * 3) {
|
||||
// Segment 2: overlay[0] (ALK 69) pingpong fade
|
||||
@@ -675,19 +681,19 @@ static void render_scene11(int elapsed) {
|
||||
int alpha = (t2 * 255) / (unit / 2);
|
||||
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetTextureAlphaMod(tex, (Uint8)SDL_min(alpha, 255));
|
||||
SDL_RenderCopy(gfx_renderer, tex, NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, tex, NULL, NULL);
|
||||
}
|
||||
} else if (elapsed < unit * 4) {
|
||||
// Segment 3: slide[1] (ALK 62) passes top-to-bottom at 50% alpha
|
||||
SDL_Texture *tex = slide[1];
|
||||
if (tex) {
|
||||
int tw, th;
|
||||
SDL_QueryTexture(tex, NULL, NULL, &tw, &th);
|
||||
sdl_get_texture_size(tex, &tw, &th);
|
||||
int y = (int)((long long)(th + view_h) * (elapsed - unit * 3) / unit) - th;
|
||||
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetTextureAlphaMod(tex, 0x80);
|
||||
SDL_Rect dst = {(view_w - tw) / 2, y, tw, th};
|
||||
SDL_RenderCopy(gfx_renderer, tex, NULL, &dst);
|
||||
sdl_render_texture(gfx_renderer, tex, NULL, &dst);
|
||||
}
|
||||
} else if (elapsed < unit * 5) {
|
||||
// Segment 4: overlay[1] (ALK 70) pingpong fade
|
||||
@@ -698,19 +704,19 @@ static void render_scene11(int elapsed) {
|
||||
int alpha = (t2 * 255) / (unit / 2);
|
||||
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetTextureAlphaMod(tex, (Uint8)SDL_min(alpha, 255));
|
||||
SDL_RenderCopy(gfx_renderer, tex, NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, tex, NULL, NULL);
|
||||
}
|
||||
} else if (elapsed < unit * 6) {
|
||||
// Segment 5: slide[0] (ALK 61) passes top-to-bottom at 50% alpha
|
||||
SDL_Texture *tex = slide[0];
|
||||
if (tex) {
|
||||
int tw, th;
|
||||
SDL_QueryTexture(tex, NULL, NULL, &tw, &th);
|
||||
sdl_get_texture_size(tex, &tw, &th);
|
||||
int y = (int)((long long)(th + view_h) * (elapsed - unit * 5) / unit) - th;
|
||||
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetTextureAlphaMod(tex, 0x80);
|
||||
SDL_Rect dst = {(view_w - tw) / 2, y, tw, th};
|
||||
SDL_RenderCopy(gfx_renderer, tex, NULL, &dst);
|
||||
sdl_render_texture(gfx_renderer, tex, NULL, &dst);
|
||||
}
|
||||
} else if (elapsed < unit * 7) {
|
||||
// Segment 6: overlay[2] (ALK 71) pingpong fade
|
||||
@@ -721,7 +727,7 @@ static void render_scene11(int elapsed) {
|
||||
int alpha = (t2 * 255) / (unit / 2);
|
||||
SDL_SetTextureBlendMode(tex, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetTextureAlphaMod(tex, (Uint8)SDL_min(alpha, 255));
|
||||
SDL_RenderCopy(gfx_renderer, tex, NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, tex, NULL, NULL);
|
||||
}
|
||||
} else {
|
||||
// Segment 7: white fade out
|
||||
@@ -799,7 +805,7 @@ static void render_scene12_3d(int elapsed) {
|
||||
// NDC → screen
|
||||
verts[i].position.x = (wx * PROJ_SCALE_X / w + 1.0f) * half_w;
|
||||
verts[i].position.y = (wy * PROJ_SCALE_Y / w + 1.0f) * half_h;
|
||||
verts[i].color = (SDL_Color){255, 255, 255, 255};
|
||||
verts[i].color = sdl_vertex_color(255, 255, 255, 255);
|
||||
verts[i].tex_coord.x = muv[i][0];
|
||||
verts[i].tex_coord.y = muv[i][1];
|
||||
}
|
||||
@@ -817,7 +823,7 @@ static void render_scene12(int elapsed) {
|
||||
// 0–2076ms: bg_a with white fade-in overlay
|
||||
if (bg_a) {
|
||||
SDL_SetTextureBlendMode(bg_a, SDL_BLENDMODE_NONE);
|
||||
SDL_RenderCopy(gfx_renderer, bg_a, NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, bg_a, NULL, NULL);
|
||||
}
|
||||
int white_alpha = 0xff - (elapsed * 0xff) / 2076;
|
||||
fade_overlay(0xff, 0xff, 0xff, (Uint8)white_alpha);
|
||||
@@ -825,32 +831,32 @@ static void render_scene12(int elapsed) {
|
||||
// 2076–2226ms: bg_a static
|
||||
if (bg_a) {
|
||||
SDL_SetTextureBlendMode(bg_a, SDL_BLENDMODE_NONE);
|
||||
SDL_RenderCopy(gfx_renderer, bg_a, NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, bg_a, NULL, NULL);
|
||||
}
|
||||
} else if (elapsed < 3826) {
|
||||
// 2226–3826ms: crossfade bg_a → bg_b
|
||||
if (bg_a) {
|
||||
SDL_SetTextureBlendMode(bg_a, SDL_BLENDMODE_NONE);
|
||||
SDL_RenderCopy(gfx_renderer, bg_a, NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, bg_a, NULL, NULL);
|
||||
}
|
||||
if (bg_b) {
|
||||
int alpha = ((elapsed - 2226) * 0xff) / 1600;
|
||||
SDL_SetTextureBlendMode(bg_b, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetTextureAlphaMod(bg_b, (Uint8)SDL_min(alpha, 0xff));
|
||||
SDL_RenderCopy(gfx_renderer, bg_b, NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, bg_b, NULL, NULL);
|
||||
}
|
||||
} else if (elapsed < 3976) {
|
||||
// 3826–3976ms: bg_b static
|
||||
if (bg_b) {
|
||||
SDL_SetTextureBlendMode(bg_b, SDL_BLENDMODE_NONE);
|
||||
SDL_RenderCopy(gfx_renderer, bg_b, NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, bg_b, NULL, NULL);
|
||||
}
|
||||
} else if (elapsed < 7803) {
|
||||
// 3976–7803ms: TODO wave distortion on bg_b; for now just show bg_b,
|
||||
// then fade to white
|
||||
if (bg_b) {
|
||||
SDL_SetTextureBlendMode(bg_b, SDL_BLENDMODE_BLEND);
|
||||
SDL_RenderCopy(gfx_renderer, bg_b, NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, bg_b, NULL, NULL);
|
||||
}
|
||||
int white_alpha = ((elapsed - 3976) * 0xff) / 3827;
|
||||
fade_overlay(0xff, 0xff, 0xff, (Uint8)SDL_min(white_alpha, 0xff));
|
||||
@@ -858,19 +864,19 @@ static void render_scene12(int elapsed) {
|
||||
// 7803–9397ms: white bg + 3D rotating quad + overlay fade-in
|
||||
SDL_SetRenderDrawBlendMode(gfx_renderer, SDL_BLENDMODE_NONE);
|
||||
SDL_SetRenderDrawColor(gfx_renderer, 0xff, 0xff, 0xff, 0xff);
|
||||
SDL_RenderFillRect(gfx_renderer, NULL);
|
||||
sdl_render_fill_rect(gfx_renderer, NULL);
|
||||
render_scene12_3d(elapsed);
|
||||
if (overlay) {
|
||||
int alpha = ((elapsed - 7803) * 0xff) / 1594;
|
||||
SDL_SetTextureBlendMode(overlay, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetTextureAlphaMod(overlay, (Uint8)SDL_min(alpha, 0xff));
|
||||
SDL_RenderCopy(gfx_renderer, overlay, NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, overlay, NULL, NULL);
|
||||
}
|
||||
} else {
|
||||
// 9397–10338ms: overlay static
|
||||
if (overlay) {
|
||||
SDL_SetTextureBlendMode(overlay, SDL_BLENDMODE_NONE);
|
||||
SDL_RenderCopy(gfx_renderer, overlay, NULL, NULL);
|
||||
sdl_render_texture(gfx_renderer, overlay, NULL, NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ static void Init() {
|
||||
int p1 = getCaliValue();
|
||||
int p2 = getCaliValue();
|
||||
int p3 = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
*var = 1;
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ static void Init() {
|
||||
int p1 = getCaliValue();
|
||||
int p2 = getCaliValue();
|
||||
int p3 = getCaliValue();
|
||||
int *var = getCaliVariable();
|
||||
vmvar_t *var = getCaliVariable();
|
||||
|
||||
*var = 1;
|
||||
|
||||
|
||||
@@ -31,8 +31,11 @@
|
||||
#include "ags.h"
|
||||
#include "font.h"
|
||||
|
||||
static FontType ftype;
|
||||
static int fsize; // フォントの大きさ
|
||||
static FontSpec dt_spec = {
|
||||
.type = FONT_GOTHIC,
|
||||
.weight = FONT_WEIGHT_BOLD,
|
||||
.size = 12
|
||||
};
|
||||
|
||||
/**
|
||||
* 次に描く文字のフォントの種類と大きさを設定
|
||||
@@ -48,8 +51,8 @@ void dt_setfont(FontType type, int size) {
|
||||
}
|
||||
#endif
|
||||
|
||||
ftype = type;
|
||||
fsize = size;
|
||||
dt_spec.type = type;
|
||||
dt_spec.size = size;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -66,12 +69,12 @@ void dt_setfont(FontType type, int size) {
|
||||
* @return: 実際に描画した幅
|
||||
*/
|
||||
int dt_drawtext_col(SDL_Surface *sf, int x, int y, char *buf, int r, int g, int b) {
|
||||
SDL_Surface *glyph = ags_drawStringToSurface(buf, r, g, b, (FontSpec){ .type = ftype, .size = fsize });
|
||||
SDL_Surface *glyph = ags_drawStringToSurface(buf, r, g, b, dt_spec);
|
||||
if (glyph == NULL) return 0;
|
||||
|
||||
SDL_Rect rect = {x, y, glyph->w, glyph->h};
|
||||
SDL_SetSurfaceBlendMode(glyph, SDL_BLENDMODE_NONE);
|
||||
SDL_BlitSurface(glyph, NULL, sf, &rect);
|
||||
SDL_FreeSurface(glyph);
|
||||
sdl_destroy_surface(glyph);
|
||||
return rect.w;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ static void gre_BlendUseAMap(surface_t *dst, int dx, int dy, surface_t *src, int
|
||||
uint8_t *dp = GETOFFSET_PIXEL(dst, dx, dy);
|
||||
uint8_t *ap = GETOFFSET_ALPHA(src, sx, sy);
|
||||
|
||||
switch (dst->sdl_surface->format->BitsPerPixel) {
|
||||
switch (sdl_surface_bits_per_pixel(dst->sdl_surface)) {
|
||||
case 16:
|
||||
for (int y = 0; y < height; y++) {
|
||||
uint16_t *yls = (uint16_t *)(sp + y * src->sdl_surface->pitch);
|
||||
@@ -55,4 +55,3 @@ void gr_blend_alpha_map(surface_t *dst, int dx, int dy, surface_t *src, int sx,
|
||||
|
||||
gre_BlendUseAMap(dst, dx, dy, src, sx, sy, sw, sh);
|
||||
}
|
||||
|
||||
|
||||
+32
-22
@@ -27,7 +27,7 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "nact.h"
|
||||
@@ -103,7 +103,7 @@ void scg_deref(cginfo_t *cg) {
|
||||
return;
|
||||
|
||||
if (cg->sf)
|
||||
SDL_FreeSurface(cg->sf);
|
||||
sdl_destroy_surface(cg->sf);
|
||||
free(cg);
|
||||
}
|
||||
|
||||
@@ -111,8 +111,9 @@ void scg_deref(cginfo_t *cg) {
|
||||
void scg_create(int wNumCG, int wWidth, int wHeight, int wR, int wG, int wB, int wBlendRate) {
|
||||
SPCG_ASSERT_NO(wNumCG);
|
||||
|
||||
SDL_Surface *sf = SDL_CreateRGBSurfaceWithFormat(0, wWidth, wHeight, 32, SDL_PIXELFORMAT_ARGB8888);
|
||||
SDL_FillRect(sf, NULL, SDL_MapRGBA(sf->format, wR, wG, wB, wBlendRate));
|
||||
SDL_Surface *sf = sdl_create_surface(
|
||||
wWidth, wHeight, 32, SDL_PIXELFORMAT_ARGB8888);
|
||||
SDL_FillRect(sf, NULL, sdl_map_rgba(sf, wR, wG, wB, wBlendRate));
|
||||
scg_new(CG_SET, wNumCG, sf);
|
||||
}
|
||||
|
||||
@@ -125,7 +126,7 @@ void scg_create_reverse(int wNumCG, int wNumSrcCG, int wReverseX, int wReverseY)
|
||||
if (!src)
|
||||
return;
|
||||
|
||||
SDL_Surface *sf = SDL_ConvertSurface(src->sf, src->sf->format, 0);
|
||||
SDL_Surface *sf = sdl_convert_surface(src->sf, sdl_surface_format(src->sf));
|
||||
if (wReverseX)
|
||||
gfx_FlipSurfaceHorizontal(sf);
|
||||
if (wReverseY)
|
||||
@@ -142,15 +143,17 @@ void scg_create_stretch(int wNumCG, int wWidth, int wHeight, int wNumSrcCG) {
|
||||
if (!src)
|
||||
return;
|
||||
|
||||
SDL_Surface *sf = SDL_CreateRGBSurfaceWithFormat(0, wWidth, wHeight, src->sf->format->BitsPerPixel, src->sf->format->format);
|
||||
SDL_Surface *sf = sdl_create_surface(wWidth, wHeight,
|
||||
sdl_surface_bits_per_pixel(src->sf), sdl_surface_format(src->sf));
|
||||
SDL_BlitScaled(src->sf, NULL, sf, NULL);
|
||||
scg_new(CG_STRETCH, wNumCG, sf);
|
||||
}
|
||||
|
||||
static SDL_Surface *blend(SDL_Surface *base, int x, int y, SDL_Surface *blend, int mode) {
|
||||
assert(blend->format->format == SDL_PIXELFORMAT_ARGB8888 || blend->format->format == SDL_PIXELFORMAT_XRGB8888);
|
||||
bool src_has_alpha = SDL_ISPIXELFORMAT_ALPHA(blend->format->format);
|
||||
SDL_Surface *dst = SDL_ConvertSurfaceFormat(base, SDL_PIXELFORMAT_ARGB8888, 0);
|
||||
assert(sdl_surface_format(blend) == SDL_PIXELFORMAT_ARGB8888 ||
|
||||
sdl_surface_format(blend) == SDL_PIXELFORMAT_XRGB8888);
|
||||
bool src_has_alpha = SDL_ISPIXELFORMAT_ALPHA(sdl_surface_format(blend));
|
||||
SDL_Surface *dst = sdl_convert_surface(base, SDL_PIXELFORMAT_ARGB8888);
|
||||
SDL_Rect rect = {x, y, blend->w, blend->h};
|
||||
SDL_IntersectRect(&(SDL_Rect){0, 0, dst->w, dst->h}, &rect, &rect);
|
||||
|
||||
@@ -220,12 +223,14 @@ void scg_create_text(int wNumCG, int wSize, int wR, int wG, int wB, int wText) {
|
||||
// 勝手に出ていいのかな?
|
||||
if (svar_length(wText) == 0) return;
|
||||
|
||||
SDL_Surface *glyph = ags_drawStringToSurface(svar_get(wText), wR, wG, wB, (FontSpec){ .size = wSize });
|
||||
FontSpec spec = { .type = FONT_GOTHIC, .weight = FONT_WEIGHT_BOLD, .size = wSize };
|
||||
SDL_Surface *glyph = ags_drawStringToSurface(svar_get(wText), wR, wG, wB, spec);
|
||||
|
||||
SDL_Surface *sf = SDL_CreateRGBSurfaceWithFormat(0, glyph->w, wSize, 32, SDL_PIXELFORMAT_ARGB8888);
|
||||
SDL_Surface *sf = sdl_create_surface(
|
||||
glyph->w, wSize, 32, SDL_PIXELFORMAT_ARGB8888);
|
||||
SDL_SetSurfaceBlendMode(glyph, SDL_BLENDMODE_NONE);
|
||||
SDL_BlitSurface(glyph, NULL, sf, NULL);
|
||||
SDL_FreeSurface(glyph);
|
||||
sdl_destroy_surface(glyph);
|
||||
|
||||
scg_new(CG_SET, wNumCG, sf);
|
||||
}
|
||||
@@ -244,12 +249,14 @@ void scg_create_textnum(int wNumCG, int wSize, int wR, int wG, int wB, int wFigs
|
||||
}
|
||||
sprintf(s, ss, wValue);
|
||||
|
||||
SDL_Surface *glyph = ags_drawStringToSurface(s, wR, wG, wB, (FontSpec){ .size = wSize });
|
||||
FontSpec spec = { .type = FONT_GOTHIC, .weight = FONT_WEIGHT_BOLD, .size = wSize };
|
||||
SDL_Surface *glyph = ags_drawStringToSurface(s, wR, wG, wB, spec);
|
||||
|
||||
SDL_Surface *sf = SDL_CreateRGBSurfaceWithFormat(0, glyph->w, wSize, 32, SDL_PIXELFORMAT_ARGB8888);
|
||||
SDL_Surface *sf = sdl_create_surface(
|
||||
glyph->w, wSize, 32, SDL_PIXELFORMAT_ARGB8888);
|
||||
SDL_SetSurfaceBlendMode(glyph, SDL_BLENDMODE_NONE);
|
||||
SDL_BlitSurface(glyph, NULL, sf, NULL);
|
||||
SDL_FreeSurface(glyph);
|
||||
sdl_destroy_surface(glyph);
|
||||
|
||||
scg_new(CG_SET, wNumCG, sf);
|
||||
}
|
||||
@@ -263,7 +270,8 @@ void scg_copy(int wNumDstCG, int wNumSrcCG) {
|
||||
if (!src)
|
||||
return;
|
||||
|
||||
scg_new(CG_SET, wNumDstCG, SDL_ConvertSurface(src->sf, src->sf->format, 0));
|
||||
scg_new(CG_SET, wNumDstCG,
|
||||
sdl_convert_surface(src->sf, sdl_surface_format(src->sf)));
|
||||
}
|
||||
|
||||
// CGの一部を切りぬいたCGを作成
|
||||
@@ -275,7 +283,8 @@ void scg_cut(int wNumDstCG, int wNumSrcCG, int wX, int wY, int wWidth, int wHeig
|
||||
if (!src)
|
||||
return;
|
||||
|
||||
SDL_Surface *sf = SDL_CreateRGBSurfaceWithFormat(0, wWidth, wHeight, src->sf->format->BitsPerPixel, src->sf->format->format);
|
||||
SDL_Surface *sf = sdl_create_surface(wWidth, wHeight,
|
||||
sdl_surface_bits_per_pixel(src->sf), sdl_surface_format(src->sf));
|
||||
SDL_BlitSurface(src->sf, &(SDL_Rect){wX, wY, wWidth, wHeight}, sf, NULL);
|
||||
|
||||
scg_new(CG_SET, wNumDstCG, sf);
|
||||
@@ -290,8 +299,9 @@ void scg_partcopy(int wNumDstCG, int wNumSrcCG, int wX, int wY, int wWidth, int
|
||||
if (!src)
|
||||
return;
|
||||
|
||||
SDL_Surface *sf = SDL_CreateRGBSurfaceWithFormat(0, src->sf->w, src->sf->h, src->sf->format->BitsPerPixel, src->sf->format->format);
|
||||
SDL_FillRect(sf, NULL, SDL_MapRGBA(sf->format, 0, 0, 0, 255));
|
||||
SDL_Surface *sf = sdl_create_surface(src->sf->w, src->sf->h,
|
||||
sdl_surface_bits_per_pixel(src->sf), sdl_surface_format(src->sf));
|
||||
SDL_FillRect(sf, NULL, sdl_map_rgba(sf, 0, 0, 0, 255));
|
||||
SDL_BlitSurface(src->sf, &(SDL_Rect){wX, wY, wWidth, wHeight}, sf, &(SDL_Rect){wX, wY, wWidth, wHeight});
|
||||
|
||||
scg_new(CG_SET, wNumDstCG, sf);
|
||||
@@ -331,7 +341,7 @@ int scg_querytype(int wNumCG) {
|
||||
}
|
||||
|
||||
// CGの大きさを取得
|
||||
bool scg_querysize(int wNumCG, int *w, int *h) {
|
||||
bool scg_querysize(int wNumCG, vmvar_t *w, vmvar_t *h) {
|
||||
if (wNumCG >= (CGMAX -1)) goto errexit;
|
||||
if (!cg_store || !cg_store[wNumCG]) goto errexit;
|
||||
if (cg_store[wNumCG]->sf == NULL) goto errexit;
|
||||
@@ -351,7 +361,7 @@ int scg_querybpp(int wNumCG) {
|
||||
if (wNumCG >= (CGMAX -1)) return 0;
|
||||
if (!cg_store || !cg_store[wNumCG]) return 0;
|
||||
if (cg_store[wNumCG]->sf == NULL) return 0;
|
||||
return cg_store[wNumCG]->sf->format->BitsPerPixel;
|
||||
return sdl_surface_bits_per_pixel(cg_store[wNumCG]->sf);
|
||||
}
|
||||
|
||||
// CGの alphamap が存在するかを取得
|
||||
@@ -359,5 +369,5 @@ bool scg_existalphamap(int wNumCG) {
|
||||
if (wNumCG >= (CGMAX -1)) return false;
|
||||
if (!cg_store || !cg_store[wNumCG]) return false;
|
||||
if (cg_store[wNumCG]->sf == NULL) return false;
|
||||
return SDL_ISPIXELFORMAT_ALPHA(cg_store[wNumCG]->sf->format->format);
|
||||
return SDL_ISPIXELFORMAT_ALPHA(sdl_surface_format(cg_store[wNumCG]->sf));
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user