mirror of
https://github.com/kichikuou/xsystem35-sdl2.git
synced 2026-10-03 20:27:58 +03:00
Compare commits
| 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 | ||
|
|
50d05e5b56 | ||
|
|
c4026f9eb1 | ||
|
|
3318bebf6c | ||
|
|
d4ad329583 | ||
|
|
2e828710ca | ||
|
|
fad4236458 | ||
|
|
77d62c7c13 | ||
|
|
a726d577ef | ||
|
|
4a5cd4a1c1 | ||
|
|
022d6af4f1 | ||
|
|
df816e3708 | ||
|
|
2c9079c875 | ||
|
|
54b8d6c993 | ||
|
|
f2fe6b9724 | ||
|
|
38dc9a7b0b | ||
|
|
d2ac3889b5 | ||
|
|
0342141358 | ||
|
|
8806bc9545 | ||
|
|
5e62405f17 | ||
|
|
714c7d44b5 | ||
|
|
0f6d4e2343 | ||
|
|
f1536a801a | ||
|
|
ab372b7873 | ||
|
|
2c02f40dc4 | ||
|
|
a395366cf8 | ||
|
|
e416fc20d3 | ||
|
|
7d03fc6757 | ||
|
|
e3b8c90611 | ||
|
|
361ca89016 | ||
|
|
5bd4622031 | ||
|
|
c7c0f69ec4 | ||
|
|
f38a3eda5c | ||
|
|
dc17b78365 | ||
|
|
b9f68eedd3 | ||
|
|
adf362ea57 | ||
|
|
b5bf439b8e | ||
|
|
9a01d28075 | ||
|
|
945dbd7722 | ||
|
|
1c54487041 | ||
|
|
92389289da | ||
|
|
4de03cc77f | ||
|
|
5a81ac6fc8 | ||
|
|
d7bac22059 | ||
|
|
8d4424061d | ||
|
|
6c692e44e5 | ||
|
|
ec071d4e8a | ||
|
|
904e10daa7 | ||
|
|
65b6da3f2a | ||
|
|
3f495b72c1 | ||
|
|
90670477c6 | ||
|
|
566ebb503f | ||
|
|
6feab51a5d | ||
|
|
15d0b44ab9 | ||
|
|
a0f20caa77 |
@@ -6,7 +6,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install Deps
|
||||
run: sudo apt install ninja-build
|
||||
@@ -22,7 +22,8 @@ jobs:
|
||||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: xsystem35-apk
|
||||
name: xsystem35-android-sdl3
|
||||
path: android/app/build/outputs/apk/release/app-release.apk
|
||||
archive: false
|
||||
|
||||
@@ -2,40 +2,49 @@ 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@v4
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup cache
|
||||
id: cache-system-libraries
|
||||
uses: actions/cache@v4
|
||||
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: mymindstorm/setup-emsdk@v14
|
||||
uses: emscripten-core/setup-emsdk@v16
|
||||
with:
|
||||
version: ${{ env.EM_VERSION }}
|
||||
actions-cache-folder: ${{env.EM_CACHE_FOLDER}}
|
||||
|
||||
- name: Work around emscripten-core/emscripten#26534
|
||||
run: npm install -g typescript@5
|
||||
|
||||
- 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@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: xsystem35-wasm
|
||||
name: xsystem35-wasm-sdl${{ matrix.sdl-version }}
|
||||
path: out/wasm/xsystem35
|
||||
|
||||
+28
-10
@@ -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@v4
|
||||
- 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,17 +9,31 @@ 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"
|
||||
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
|
||||
portmidi:p
|
||||
defaults:
|
||||
run:
|
||||
shell: msys2 {0}
|
||||
name: MSYS2 ${{ matrix.sys }}
|
||||
name: MSYS2 SDL${{ matrix.sdl-version }} ${{ matrix.sys }}
|
||||
steps:
|
||||
|
||||
- name: Set up MSYS2
|
||||
@@ -27,32 +41,31 @@ jobs:
|
||||
with:
|
||||
msystem: ${{ matrix.sys }}
|
||||
pacboy: >-
|
||||
SDL2:p
|
||||
SDL2_ttf:p
|
||||
SDL2_mixer:p
|
||||
${{ matrix.sdl-deps }}
|
||||
freetype:p
|
||||
libwebp:p
|
||||
portmidi:p
|
||||
${{ matrix.deps }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Copy licenses
|
||||
run: |
|
||||
ls /${{ matrix.sys }}/share/licenses
|
||||
mkdir dist
|
||||
cp -r /${{ matrix.sys }}/share/licenses dist/
|
||||
cp fonts/*.license dist/licenses/
|
||||
cp licenses/* dist/licenses/
|
||||
|
||||
- name: Install dev dependencies
|
||||
run: pacboy --noconfirm -S --needed gcc:p cmake:p ninja:p asciidoctor:p
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
mkdir -p out/release
|
||||
cd out/release
|
||||
cmake -GNinja -DCMAKE_BUILD_TYPE=Release -DCMAKE_COMPILE_WARNING_AS_ERROR=YES ${{ matrix.cmake_args }} ../../
|
||||
ninja
|
||||
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
|
||||
@@ -63,7 +76,7 @@ jobs:
|
||||
cp out/release/src/xsystem35.exe out/release/doc/*.html *.md COPYING xsys35rc.sample dist/
|
||||
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ matrix.package }}
|
||||
path: dist
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
out
|
||||
android/.gradle
|
||||
android/.idea
|
||||
android/app/aars
|
||||
android/app/.cxx
|
||||
android/app/.externalNativeBuild
|
||||
android/app/build
|
||||
|
||||
+59
-4
@@ -1,17 +1,72 @@
|
||||
# Changelog
|
||||
|
||||
## 2.14.3 - 2024-04-01
|
||||
## 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.
|
||||
|
||||
## 2.17.1 - 2026-01-10
|
||||
- Fixed an issue where Rance 5D could not save files on non-Windows systems.
|
||||
- Improved fast-forward behavior using the Ctrl key in Rance 5D.
|
||||
- The `SI 2` command now returns 0 if a playlist file is not provided.
|
||||
|
||||
## 2.17.0 - 2025-11-09
|
||||
- Added "Streamer Mode" to mosaic specified (NSFW) images. This is enabled via the `-censor <file>` command-line option.
|
||||
- Windows: Fixed an issue where Rance 5D could not save files if the `Save` directory did not exist.
|
||||
|
||||
## 2.16.1 - 2025-08-28
|
||||
- Fixed text rendering issue in SACT log (#71)
|
||||
- Windows: The 32-bit version no longer supports the portmidi MIDI backend
|
||||
|
||||
## 2.16.0 - 2025-06-13
|
||||
- Fixed map tile color issue in Graymerca (#67)
|
||||
- Windows: Implemented number input dialog for the `NI` command (#68)
|
||||
- Android: Added "Clear Save Files" launcher menu command
|
||||
|
||||
## 2.15.1 - 2025-05-14
|
||||
- Fixed a text rendering issue in Rance 5D (#66)
|
||||
|
||||
## 2.15.0 - 2025-04-21
|
||||
- Fixed color palette bug introduced in v2.14.2.
|
||||
- Fixed uncancellable message waiting mode.
|
||||
- Android: "Install from ZIP" button is moved to the top-right option menu.
|
||||
- Android: Improved extraction logic of track numbers from audio file names.
|
||||
|
||||
## 2.14.3 - 2025-04-01
|
||||
- Fixed build for big endian architectures.
|
||||
|
||||
## 2.14.2 - 2024-03-09
|
||||
## 2.14.2 - 2025-03-09
|
||||
- Android: Back button to exit the game (#50)
|
||||
- Improved touch input usability in ママトト (#64)
|
||||
|
||||
## 2.14.1 - 2024-02-07
|
||||
## 2.14.1 - 2025-02-07
|
||||
- Fixed font selection and text decoration not working in selection menus.
|
||||
- Fixed a graphic issue in DALK intro.
|
||||
|
||||
## 2.14.0 - 2024-01-09
|
||||
## 2.14.0 - 2025-01-09
|
||||
- New supported games: 俺の下であがけ and 楽園行.
|
||||
- Windows: On Windows 10 version 1903 or above, now xsystem35 can handle
|
||||
Japanese filenames correctly even if the system locale is not Japanese.
|
||||
|
||||
+89
-37
@@ -1,16 +1,28 @@
|
||||
cmake_minimum_required(VERSION 3.13)
|
||||
|
||||
# Rebuild external projects when download URL changes
|
||||
if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24.0")
|
||||
cmake_policy(SET CMP0135 NEW)
|
||||
endif()
|
||||
cmake_minimum_required(VERSION 3.13...4.0)
|
||||
|
||||
project(xsystem35 LANGUAGES C)
|
||||
set(CMAKE_C_STANDARD 99)
|
||||
enable_testing()
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS TRUE)
|
||||
|
||||
set(XSYSTEM35_VERSION "2.14.3")
|
||||
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)
|
||||
@@ -60,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
|
||||
@@ -67,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)
|
||||
@@ -92,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()
|
||||
|
||||
@@ -158,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})
|
||||
@@ -216,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})
|
||||
@@ -232,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.
|
||||
@@ -16,9 +16,10 @@ the following features:
|
||||
### Playing Audio Files as Virtual CD Music
|
||||
|
||||
Many System 3.x games feature music as audio tracks on the CD-ROM. xsystem35
|
||||
can play music from audio files, eliminating the need to insert CDs. To use
|
||||
ripped audio files, create a file named `playlist.txt` in the game directory
|
||||
and list the paths to your tracks, one per line. For example:
|
||||
can play music from audio files, eliminating the need to insert CDs. Supported
|
||||
audio file formats are MP3 and Ogg. To use ripped audio files, create a file
|
||||
named `playlist.txt` in the game directory and list the paths to your tracks,
|
||||
one per line. For example:
|
||||
|
||||
```
|
||||
# The first line is not used
|
||||
@@ -60,6 +61,14 @@ and examine or modify game variables. There are two ways to use the debugger:
|
||||
launch the debugger with a console interface. Type `help` to see a list of
|
||||
available commands.
|
||||
|
||||
### Streamer Mode
|
||||
|
||||
xsystem35 introduces a "Streamer Mode" to make playing games with NSFW content
|
||||
safer for streaming or public viewing. When enabled with the `-censor <file>`
|
||||
option, images specified in the provided file will be automatically mosaiced.
|
||||
|
||||
The `misc/censor/` directory contains example censor list files for some games.
|
||||
|
||||
## Installation
|
||||
|
||||
Prebuilt packages for Windows and Android can be downloaded from the
|
||||
@@ -93,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.
|
||||
|
||||
+71
-37
@@ -6,7 +6,77 @@ Minimum supported Android version: 5.0
|
||||
You can download prebuilt APKs
|
||||
[here](https://github.com/kichikuou/xsystem35-sdl2/releases).
|
||||
|
||||
## Build
|
||||
### Installing with Obtainium
|
||||
Alternatively, you can install `xsystem35-sdl2` using [Obtainium](https://github.com/ImranR98/Obtainium). Obtainium will detect new releases and allow you to update the app directly.
|
||||
|
||||
1. Install Obtainium on your Android device.
|
||||
2. Click the badge below to add the app to Obtainium:
|
||||
|
||||
[<img src="https://raw.githubusercontent.com/ImranR98/Obtainium/main/assets/graphics/badge_obtainium.png" alt="Get it on Obtainium" height="60">](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22io.github.kichikuou.xsystem35%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fkichikuou%2Fxsystem35-sdl2%22%2C%22author%22%3A%22kichikuou%22%2C%22name%22%3A%22xsystem35%22%2C%22categories%22%3A%5B%22games%22%5D%7D)
|
||||
|
||||
## 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.
|
||||
|
||||
#### 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
|
||||
others). `.EXE` and `.DLL` files are not necessary, but you can include them
|
||||
if you want.
|
||||
- Music files (`.mp3`, `.ogg`, or `.wav`) whose filenames have a number either
|
||||
at the beginning or just before the extension are recognized as BGM files.
|
||||
For example:
|
||||
- `Track2.mp3`
|
||||
- `15.ogg`
|
||||
- `02 - Title.mp3`
|
||||
- `rance4_03.wav` (Note: The filename shouldn't be `rance403.wav`, as it
|
||||
would be treated as the 403rd track.)
|
||||
|
||||
Note: This ZIP format is also compatible with
|
||||
[Kichikuou on Web](http://kichikuou.github.io/web/).
|
||||
|
||||
### Miscellaneous
|
||||
- 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.
|
||||
|
||||
## Known Issues
|
||||
- Android versions older than 7.0 cannot handle ZIP files containing Shift-JIS
|
||||
filenames. This issue occurs with some ZIP files distributed on
|
||||
[retroc.net](http://retropc.net/alice/). If you encounter the error message
|
||||
"This type of ZIP is not supported," unzip the file on your PC and re-archive
|
||||
it using modern ZIP creation software.
|
||||
|
||||
## 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.
|
||||
@@ -34,39 +104,3 @@ git clone https://github.com/kichikuou/xsystem35-sdl2.git
|
||||
cd xsystem35-sdl2/android
|
||||
./gradlew build # or ./gradlew installDebug if you have a connected device
|
||||
```
|
||||
|
||||
## Usage
|
||||
### Basic Usage
|
||||
1. Create a ZIP file containing all the game files and BGM files (see
|
||||
[below](#preparing-a-zip) for details), and transfer it to your device.
|
||||
2. Open the app. A list of installed games will be displayed. Since no games
|
||||
have been installed yet, only the "Install from ZIP" button will be visible.
|
||||
Tap it.
|
||||
3. Select the ZIP file you created in step 1.
|
||||
4. The game will start. To simulate a right-click, tap the black bars on either
|
||||
the left or right, or top or bottom of the screen.
|
||||
|
||||
### Preparing a ZIP
|
||||
- Include all files from the `GAMEDATA` folder (such as `.ALD` files and
|
||||
others). `.EXE` and `.DLL` files are not necessary, but you can include them
|
||||
if you want.
|
||||
- Music files (`.mp3`, `.ogg`, or `.wav`) whose filenames end with a number
|
||||
will be recognized as BGM files. For example:
|
||||
- `Track2.mp3`
|
||||
- `15.ogg`
|
||||
- `rance4_03.wav` (Note: The filename shouldn't be `rance403.wav`, as it
|
||||
would be treated as the 403rd track.)
|
||||
|
||||
Note: This ZIP format is also compatible with
|
||||
[Kichikuou on Web](http://kichikuou.github.io/web/).
|
||||
|
||||
### Miscellaneous
|
||||
- You can export or import save files via the game list's option menu.
|
||||
- To uninstall a game, long-tap its title in the game list.
|
||||
|
||||
## Known Issues
|
||||
- Android versions older than 7.0 cannot handle ZIP files containing Shift-JIS
|
||||
filenames. This issue occurs with some ZIP files distributed on
|
||||
[retroc.net](http://retropc.net/alice/). If you encounter the error message
|
||||
"This type of ZIP is not supported," unzip the file on your PC and re-archive
|
||||
it using modern ZIP creation software.
|
||||
|
||||
@@ -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 32
|
||||
versionName "2.14.3" + gitRevision()
|
||||
versionCode 36
|
||||
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.4/SDL2-2.32.4.tar.gz
|
||||
URL_HASH SHA1=4fb68f43891ca4def414cc7cf0f3b2a053cb6aaf
|
||||
)
|
||||
FetchContent_Declare(
|
||||
SDL_ttf
|
||||
URL https://github.com/libsdl-org/SDL_ttf/releases/download/release-2.22.0/SDL2_ttf-2.22.0.tar.gz
|
||||
URL_HASH SHA1=da5e86b601ad299a697878fab1af6f3be47b529d
|
||||
)
|
||||
FetchContent_Declare(
|
||||
SDL_mixer
|
||||
URL https://github.com/libsdl-org/SDL_mixer/releases/download/release-2.8.0/SDL2_mixer-2.8.0.tar.gz
|
||||
URL_HASH SHA1=a58c69f9d00e44833b9e00e1adb58d85759ca499
|
||||
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)
|
||||
@@ -42,12 +30,10 @@ add_subdirectory(${PROJECT_ROOT_DIR} xsystem35)
|
||||
set(ASSETS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/../src/main/assets)
|
||||
file(MAKE_DIRECTORY ${ASSETS_DIR}/licenses)
|
||||
file(COPY_FILE ${PROJECT_ROOT_DIR}/fonts/MTLc3m.ttf ${ASSETS_DIR}/MTLc3m.ttf)
|
||||
file(COPY_FILE ${PROJECT_ROOT_DIR}/fonts/mincho.otf ${ASSETS_DIR}/mincho.otf)
|
||||
file(COPY_FILE ${PROJECT_ROOT_DIR}/fonts/mincho.ttf ${ASSETS_DIR}/mincho.ttf)
|
||||
file(COPY_FILE ${PROJECT_ROOT_DIR}/COPYING ${ASSETS_DIR}/licenses/xsystem35)
|
||||
file(COPY_FILE ${PROJECT_ROOT_DIR}/fonts/MTLc3m.ttf.license ${ASSETS_DIR}/licenses/MTLc3m)
|
||||
file(COPY_FILE ${PROJECT_ROOT_DIR}/fonts/mincho.otf.license ${ASSETS_DIR}/licenses/mincho)
|
||||
file(COPY_FILE ${sdl_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL)
|
||||
file(COPY_FILE ${sdl_ttf_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL_ttf)
|
||||
file(COPY_FILE ${sdl_ttf_SOURCE_DIR}/external/freetype/docs/GPLv2.TXT ${ASSETS_DIR}/licenses/freetype)
|
||||
file(COPY_FILE ${sdl_ttf_SOURCE_DIR}/external/harfbuzz/COPYING ${ASSETS_DIR}/licenses/harfbuzz)
|
||||
file(COPY_FILE ${sdl_mixer_SOURCE_DIR}/LICENSE.txt ${ASSETS_DIR}/licenses/SDL_mixer)
|
||||
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 ${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,224 +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()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createDirForGame(): File {
|
||||
var i = 0
|
||||
while (true) {
|
||||
val f = File(rootDir, i++.toString())
|
||||
if (!f.exists() && f.mkdir()) {
|
||||
return f
|
||||
}
|
||||
}
|
||||
fun refreshGameList() {
|
||||
store.updateGameList()
|
||||
observer?.onGameListChange()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
return store.importSaveData(input)
|
||||
}
|
||||
|
||||
private fun extractFiles(input: InputStream, outDir: File, progressCallback: (String) -> Unit): File {
|
||||
val configWriter = GameConfigWriter()
|
||||
val hadDecodeError = forEachZipEntry(input) { zipEntry, zip ->
|
||||
Log.i("extractFiles", zipEntry.name)
|
||||
val path = File(outDir, zipEntry.name)
|
||||
if (zipEntry.isDirectory)
|
||||
return@forEachZipEntry
|
||||
path.parentFile?.mkdirs()
|
||||
progressCallback(zipEntry.name)
|
||||
FileOutputStream(path).buffered().use {
|
||||
zip.copyTo(it)
|
||||
}
|
||||
configWriter.maybeAdd(zipEntry.name)
|
||||
}
|
||||
if (!configWriter.ready) {
|
||||
if (hadDecodeError)
|
||||
throw InstallFailureException(R.string.unsupported_zip)
|
||||
throw InstallFailureException(R.string.cannot_find_ald)
|
||||
}
|
||||
configWriter.write(outDir)
|
||||
return configWriter.gameDir?.let { File(outDir, it) } ?: outDir
|
||||
}
|
||||
|
||||
// Xsystem35-sdl2 2.3.0 - 2.11.1 used playlist2.txt. Rename it to playlist.txt.
|
||||
private fun migratePlaylist(dir: File) {
|
||||
val oldPlaylist = File(dir, OLD_PLAYLIST_FILE)
|
||||
if (oldPlaylist.exists()) {
|
||||
oldPlaylist.renameTo(File(dir, PLAYLIST_FILE))
|
||||
}
|
||||
}
|
||||
|
||||
class InstallFailureException(val msgId: Int) : Exception()
|
||||
|
||||
// A helper class which generates GAMEDIR_FILE and PLAYLIST_FILE.
|
||||
private class GameConfigWriter {
|
||||
var ready = false
|
||||
private set
|
||||
var gameDir: String? = null
|
||||
private set
|
||||
private val aldRegex = """.*?s[a-z]\.ald""".toRegex(RegexOption.IGNORE_CASE)
|
||||
private val audioRegex = """.*?(\d+)\.(wav|mp3|ogg)""".toRegex(RegexOption.IGNORE_CASE)
|
||||
private val audioFiles: Array<String?> = arrayOfNulls(100)
|
||||
|
||||
fun maybeAdd(path: String) {
|
||||
aldRegex.matchEntire(path)?.let {
|
||||
gameDir = File(path).parent
|
||||
ready = true
|
||||
}
|
||||
audioRegex.matchEntire(path)?.let {
|
||||
val track = it.groupValues[1].toInt()
|
||||
if (0 < track && track <= audioFiles.size)
|
||||
audioFiles[track - 1] = path
|
||||
}
|
||||
}
|
||||
|
||||
fun write(outDir: File) {
|
||||
// Generate GAMEDIR_FILE
|
||||
gameDir?.let {
|
||||
File(outDir, GAMEDIR_FILE).writeText(it)
|
||||
}
|
||||
// Generate PLAYLIST_FILE
|
||||
val absGameDir = gameDir?.let { File(outDir, it) } ?: outDir
|
||||
val playlistFile = File(absGameDir, PLAYLIST_FILE)
|
||||
if (!playlistFile.exists()) {
|
||||
val prefixToRemove = gameDir?.let { "$it/" } ?: ""
|
||||
val playlist = audioFiles.joinToString("\n") {
|
||||
it?.removePrefix(prefixToRemove) ?: ""
|
||||
}.trimEnd('\n')
|
||||
playlistFile.writeText(playlist)
|
||||
}
|
||||
}
|
||||
fun clearSaveData(): Boolean {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import android.text.Html
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
@@ -32,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
|
||||
@@ -47,12 +48,12 @@ 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)
|
||||
listView.emptyView = findViewById(R.id.empty)
|
||||
findViewById<TextView>(R.id.usage).text = Html.fromHtml(getString(R.string.usage))
|
||||
listView.setOnItemClickListener { _, _, position, _ ->
|
||||
onListItemClick(position)
|
||||
}
|
||||
@@ -67,31 +68,21 @@ 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) {
|
||||
if (position < launcher.games.size) {
|
||||
startGame(launcher.games[position].path, null)
|
||||
} else {
|
||||
val i = Intent(Intent.ACTION_GET_CONTENT)
|
||||
i.type = CONTENT_TYPE_ZIP
|
||||
startActivityForResult(Intent.createChooser(i, getString(R.string.choose_a_file)), INSTALL_REQUEST)
|
||||
}
|
||||
startGame(launcher.games[position].path, null)
|
||||
}
|
||||
|
||||
private fun onItemLongClick(position: Int): Boolean {
|
||||
if (position < launcher.games.size) {
|
||||
AlertDialog.Builder(this).setTitle(R.string.uninstall_dialog_title)
|
||||
.setMessage(getString(R.string.uninstall_dialog_message, launcher.games[position].title))
|
||||
.setPositiveButton(R.string.ok) {_, _ -> uninstall(position)}
|
||||
.setNegativeButton(R.string.cancel) {_, _ -> }
|
||||
.show()
|
||||
}
|
||||
AlertDialog.Builder(this).setTitle(R.string.confirm)
|
||||
.setMessage(getString(R.string.uninstall_dialog_message, launcher.games[position].title))
|
||||
.setPositiveButton(R.string.ok) {_, _ -> uninstall(position)}
|
||||
.setNegativeButton(R.string.cancel) {_, _ -> }
|
||||
.show()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -102,6 +93,20 @@ class LauncherActivity : Activity(), LauncherObserver {
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
return when (item.itemId) {
|
||||
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)), 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 -> {
|
||||
val i = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||
i.type = CONTENT_TYPE_ZIP
|
||||
@@ -116,6 +121,27 @@ class LauncherActivity : Activity(), LauncherObserver {
|
||||
SAVEDATA_IMPORT_REQUEST)
|
||||
true
|
||||
}
|
||||
R.id.clear_savedata -> {
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(R.string.confirm)
|
||||
.setMessage(R.string.clear_save_data_confirm)
|
||||
.setPositiveButton(R.string.ok) { _, _ ->
|
||||
if (launcher.clearSaveData()) {
|
||||
Toast.makeText(this, R.string.save_data_clear_success, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
errorDialog(R.string.save_data_clear_error)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
true
|
||||
}
|
||||
R.id.help -> {
|
||||
AlertDialog.Builder(this)
|
||||
.setMessage(Html.fromHtml(getString(R.string.usage)))
|
||||
.show()
|
||||
true
|
||||
}
|
||||
R.id.licenses -> {
|
||||
val intent = Intent(this, LicensesMenuActivity::class.java)
|
||||
startActivity(intent)
|
||||
@@ -128,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) {
|
||||
@@ -143,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) {
|
||||
@@ -157,23 +190,20 @@ class LauncherActivity : Activity(), LauncherObserver {
|
||||
|
||||
override fun onGameListChange() {
|
||||
val items = launcher.titles.toMutableList()
|
||||
items.add(getString(R.string.install_from_zip))
|
||||
val listView = findViewById<ListView>(R.id.list)
|
||||
listView.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, items)
|
||||
}
|
||||
|
||||
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?) {
|
||||
@@ -189,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,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,11 +12,11 @@ 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("Source Han Serif", "mincho", "https://github.com/adobe-fonts/source-han-serif/"),
|
||||
Entry("IPA明朝", "mincho", "https://moji.or.jp/ipafont/"),
|
||||
)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -6,4 +6,12 @@
|
||||
<ListView android:id="@+id/list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
<ScrollView android:id="@+id/empty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<TextView android:id="@+id/usage"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="20dp" />
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
||||
@@ -1,12 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<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" />
|
||||
<item
|
||||
android:id="@+id/import_savedata"
|
||||
android:title="@string/action_import_save_data" />
|
||||
<item
|
||||
android:id="@+id/clear_savedata"
|
||||
android:title="@string/action_clear_save_data" />
|
||||
<item
|
||||
android:id="@+id/help"
|
||||
android:title="@string/action_help" />
|
||||
<item
|
||||
android:id="@+id/licenses"
|
||||
android:title="@string/action_licenses" />
|
||||
</menu>
|
||||
</menu>
|
||||
|
||||
@@ -2,21 +2,51 @@
|
||||
<resources>
|
||||
<string name="app_name">xsystem35</string>
|
||||
<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_from_zip">ZIP からインストール</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>
|
||||
<string name="save_data_import_success">インポートに成功しました。</string>
|
||||
<string name="save_data_clear_success">セーブデータをクリアしました。</string>
|
||||
<string name="save_data_clear_error">セーブデータのクリアに失敗しました。</string>
|
||||
<string name="uninstall_dialog_message">「%s」をアンインストールしますか?</string>
|
||||
<string name="uninstall_dialog_title">確認</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>
|
||||
</resources>
|
||||
<string name="usage">
|
||||
<![CDATA[
|
||||
<h1>ゲームのインストール方法</h1>
|
||||
<p>1. PC上で、ゲームのファイルが入ったZIPを用意します。例えば以下のようなファイルを含めます:</p>
|
||||
<div>
|
||||
・鬼畜王SA.ALD<br>
|
||||
・鬼畜王GA.ALD<br>
|
||||
・鬼畜王GB.ALD<br>
|
||||
・鬼畜王WA.ALD<br>
|
||||
・02.mp3<br>
|
||||
・03.mp3<br>
|
||||
・…<br>
|
||||
</div>
|
||||
<p>2. ZIPファイルを (例えばUSB経由で) PCからデバイスに転送します。</p>
|
||||
<p>3. この画面の右上のメニューボタンをタップして「ZIPからインストール」を選択し、転送したZIPを選びます。</p>
|
||||
]]>
|
||||
</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,22 +1,53 @@
|
||||
<resources>
|
||||
<string name="app_name">xsystem35</string>
|
||||
<string name="action_clear_save_data">Clear Save Files</string>
|
||||
<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_from_zip">Install from ZIP</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>
|
||||
<string name="save_data_clear_success">Save files cleared.</string>
|
||||
<string name="save_data_export_error">Failed to export save files.</string>
|
||||
<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="uninstall_dialog_title">Confirm</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">
|
||||
<![CDATA[
|
||||
<h1>How to install a game</h1>
|
||||
<p>1. On your PC, prepare a ZIP containing the game files. For example, it should contain the following files:</p>
|
||||
<div>
|
||||
* KICHIKUSA.ALD<br>
|
||||
* KICHIKUGA.ALD<br>
|
||||
* KICHIKUGB.ALD<br>
|
||||
* KICHIKUWA.ALD<br>
|
||||
* System39.ain<br>
|
||||
* 02.mp3<br>
|
||||
* 03.mp3<br>
|
||||
* …<br>
|
||||
</div>
|
||||
<p>2. Transfer the ZIP file from your PC to the device (e.g. via USB).</p>
|
||||
<p>3. Tap the menu button at the upper-right of this screen, select "Install from ZIP," and choose the ZIP you transferred.</p>
|
||||
]]>
|
||||
</string>
|
||||
</resources>
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
+5
-5
@@ -1,21 +1,21 @@
|
||||
#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@
|
||||
#cmakedefine HAVE_WEBP @HAVE_WEBP@
|
||||
|
||||
#define DEFAULT_GOTHIC_TTF "@DEFAULT_FONT_PATH@MTLc3m.ttf"
|
||||
#define DEFAULT_MINCHO_TTF "@DEFAULT_FONT_PATH@mincho.otf"
|
||||
#define DEFAULT_MINCHO_TTF "@DEFAULT_FONT_PATH@mincho.ttf"
|
||||
|
||||
+10
-1
@@ -30,7 +30,8 @@ finds System 3.x game files (*.ALD) from the current directory.
|
||||
*-game* _game_::
|
||||
Enables game-specific hacks. Usually auto-detected, but must be specified for
|
||||
games with modified / translated titles. Possible values are: `toushin2`,
|
||||
`rance3_eng`, `rance4_eng`, `rance4_v2`, `persiom`, `agake`.
|
||||
`rance2`, `rance3`, `rance3_eng`, `rance4_eng`, `rance4_v2`, `persiom`,
|
||||
`agake`.
|
||||
|
||||
*-savedir* _dir_::
|
||||
Directory to save game state files. If _dir_ begins with '~', it is expanded
|
||||
@@ -48,6 +49,11 @@ finds System 3.x game files (*.ALD) from the current directory.
|
||||
Suppress text hook on specified scenario pages. _list_ is a comma-separated
|
||||
list of page numbers. Used to suppress system messages, etc.
|
||||
|
||||
*-censor* _file_::
|
||||
Enable Streamer Mode, which mosaics specific images to make them safe for
|
||||
streaming. The _file_ is a text file containing a list of image numbers
|
||||
to censor, one per line. Lines starting with '#' are treated as comments.
|
||||
|
||||
*-renderer* _name_::
|
||||
Use the SDL rendering driver named _name_.
|
||||
|
||||
@@ -92,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.
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
install(FILES
|
||||
MTLc3m.ttf
|
||||
MTLc3m.ttf.license
|
||||
mincho.otf
|
||||
mincho.otf.license
|
||||
mincho.ttf
|
||||
DESTINATION share/xsystem35/fonts)
|
||||
|
||||
# pyftsubset is included in fonttools.
|
||||
@@ -11,8 +9,9 @@ find_program(PYFTSUBSET_FOUND pyftsubset)
|
||||
if (PYFTSUBSET_FOUND)
|
||||
add_custom_target(fonts
|
||||
COMMAND uchars > uchars.txt
|
||||
COMMAND wget -O SourceHanSerifJP-Regular.otf https://github.com/adobe-fonts/source-han-serif/blob/release/SubsetOTF/JP/SourceHanSerifJP-Regular.otf?raw=true
|
||||
COMMAND pyftsubset SourceHanSerifJP-Regular.otf --text-file=uchars.txt --output-file=subset.otf
|
||||
COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/adjust_metrics.py subset.otf ${CMAKE_CURRENT_SOURCE_DIR}/mincho.otf
|
||||
COMMAND wget -O ipam.zip https://moji.or.jp/wp-content/ipafont/IPAfont/ipam00303.zip
|
||||
COMMAND unzip -o -j ipam.zip ipam00303/ipam.ttf
|
||||
COMMAND pyftsubset ipam.ttf --text-file=uchars.txt --output-file=subset.ttf
|
||||
COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/rename_font.py subset.ttf ${CMAKE_CURRENT_SOURCE_DIR}/mincho.ttf
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
if len(sys.argv) != 3:
|
||||
print('Usage: adjust_metrics.py infile outfile')
|
||||
exit(1)
|
||||
|
||||
font = TTFont(sys.argv[1])
|
||||
|
||||
hhea = font['hhea']
|
||||
os2 = font['OS/2']
|
||||
|
||||
if hhea.ascent != os2.sTypoAscender:
|
||||
print(f'Changing hhea.ascent from {hhea.ascent} to {os2.sTypoAscender}')
|
||||
hhea.ascent = os2.sTypoAscender
|
||||
|
||||
if hhea.descent != os2.sTypoDescender:
|
||||
print(f'Changing hhea.descent from {hhea.descent} to {os2.sTypoDescender}')
|
||||
hhea.descent = os2.sTypoDescender
|
||||
|
||||
font.save(sys.argv[2])
|
||||
Binary file not shown.
@@ -1,92 +0,0 @@
|
||||
This Font Software is licensed under the SIL Open Font License,
|
||||
Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font
|
||||
creation efforts of academic and linguistic communities, and to
|
||||
provide a free and open framework in which fonts may be shared and
|
||||
improved in partnership with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply to
|
||||
any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software
|
||||
components as distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to,
|
||||
deleting, or substituting -- in part or in whole -- any of the
|
||||
components of the Original Version, by changing formats or by porting
|
||||
the Font Software to a new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed,
|
||||
modify, redistribute, and sell modified and unmodified copies of the
|
||||
Font Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components, in
|
||||
Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the
|
||||
corresponding Copyright Holder. This restriction only applies to the
|
||||
primary font name as presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created using
|
||||
the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3
|
||||
# Rename the font's internal name records.
|
||||
#
|
||||
# IPA明朝 is distributed under the IPA Font License v1.0. A subset of it is a
|
||||
# "Derived Program" under that license, and Article 3.1(4) forbids using or
|
||||
# including the Licensed Program's name ("IPA"/"IPAMincho") as the program,
|
||||
# font, or file name of the Derived Program. pyftsubset preserves the original
|
||||
# name table, so rename it here to a name that does not contain "IPA".
|
||||
import sys
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
if len(sys.argv) != 3:
|
||||
print('Usage: rename_font.py infile outfile')
|
||||
exit(1)
|
||||
|
||||
FAMILY = 'xsystem35 Mincho'
|
||||
PSNAME = 'xsystem35-Mincho'
|
||||
|
||||
font = TTFont(sys.argv[1])
|
||||
name = font['name']
|
||||
|
||||
# Rewrite the family / full / typographic-family / PostScript / unique-ID
|
||||
# records in place, preserving each record's platform, encoding and language.
|
||||
for rec in list(name.names):
|
||||
if rec.nameID in (1, 4, 16):
|
||||
value = FAMILY
|
||||
elif rec.nameID in (3, 6):
|
||||
value = PSNAME
|
||||
else:
|
||||
continue
|
||||
name.setName(value, rec.nameID, rec.platformID, rec.platEncID, rec.langID)
|
||||
|
||||
font.save(sys.argv[2])
|
||||
@@ -42,7 +42,7 @@ Game Compatibility
|
||||
| これD・P・S? | Supported | From 20世紀アリス |
|
||||
| 夜が来る! | Unsupported | |
|
||||
| Only you ~リ・クルス~ | Supported | |
|
||||
| 大悪司 | Supported | Opening demo is not supported |
|
||||
| 大悪司 | Supported | |
|
||||
| 王子さまLv1 | Supported | |
|
||||
| 王子さま1ヵ月後 | Supported | From 王子さまLV1.5 |
|
||||
| 白鳳の子分叩き | Supported | From 王子さまLV1.5 |
|
||||
@@ -62,7 +62,7 @@ Game Compatibility
|
||||
| 翡翠の眸 | Unsupported | |
|
||||
| 学園KING | Supported | From アリスCD |
|
||||
| 学園KING 番外編 | Supported | From アリスCD |
|
||||
| 蒼海に墜ちて | Supported | From アリスCD |
|
||||
| 蒼海に堕ちて… | Supported | From アリスCD |
|
||||
| ぶろぶろ | Supported | From アリスCD |
|
||||
| 裸ぶろぶろ狂 | Supported | From アリスCD |
|
||||
| メイドのススメ | Supported | From アリスCD |
|
||||
|
||||
@@ -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.
|
||||
@@ -0,0 +1,117 @@
|
||||
--------------------------------------------------
|
||||
IPA Font License Agreement v1.0 <Japanese/English>
|
||||
--------------------------------------------------
|
||||
|
||||
IPAフォントライセンスv1.0
|
||||
|
||||
許諾者は、この使用許諾(以下「本契約」といいます。)に定める条件の下で、許諾プログラム(1条に定義するところによります。)を提供します。受領者(1条に定義するところによります。)が、許諾プログラムを使用し、複製し、または頒布する行為、その他、本契約に定める権利の利用を行った場合、受領者は本契約に同意したものと見なします。
|
||||
|
||||
|
||||
第1条 用語の定義
|
||||
|
||||
本契約において、次の各号に掲げる用語は、当該各号に定めるところによります。
|
||||
|
||||
1.「デジタル・フォント・プログラム」とは、フォントを含み、レンダリングしまたは表示するために用いられるコンピュータ・プログラムをいいます。
|
||||
2.「許諾プログラム」とは、許諾者が本契約の下で許諾するデジタル・フォント・プログラムをいいます。
|
||||
3.「派生プログラム」とは、許諾プログラムの一部または全部を、改変し、加除修正等し、入れ替え、その他翻案したデジタル・フォント・プログラムをいい、許諾プログラムの一部もしくは全部から文字情報を取り出し、またはデジタル・ドキュメント・ファイルからエンベッドされたフォントを取り出し、取り出された文字情報をそのまま、または改変をなして新たなデジタル・フォント・プログラムとして製作されたものを含みます。
|
||||
4.「デジタル・コンテンツ」とは、デジタル・データ形式によってエンド・ユーザに提供される制作物のことをいい、動画・静止画等の映像コンテンツおよびテレビ番組等の放送コンテンツ、ならびに文字テキスト、画像、図形等を含んで構成された制作物を含みます。
|
||||
5.「デジタル・ドキュメント・ファイル」とは、PDFファイルその他、各種ソフトウェア・プログラムによって製作されたデジタル・コンテンツであって、その中にフォントを表示するために許諾プログラムの全部または一部が埋め込まれた(エンベッドされた)ものをいいます。フォントが「エンベッドされた」とは、当該フォントが埋め込まれた特定の「デジタル・ドキュメント・ファイル」においてのみ表示されるために使用されている状態を指し、その特定の「デジタル・ドキュメント・ファイル」以外でフォントを表示するために使用できるデジタル・フォント・プログラムに含まれている場合と区別されます。
|
||||
6.「コンピュータ」とは、本契約においては、サーバを含みます。
|
||||
7.「複製その他の利用」とは、複製、譲渡、頒布、貸与、公衆送信、上映、展示、翻案その他の利用をいいます。
|
||||
8.「受領者」とは、許諾プログラムを本契約の下で受領した人をいい、受領者から許諾プログラムを受領した人を含みます。
|
||||
|
||||
第2条 使用許諾の付与
|
||||
|
||||
許諾者は受領者に対し、本契約の条項に従い、すべての国で、許諾プログラムを使用することを許諾します。ただし、許諾プログラムに存在する一切の権利はすべて許諾者が保有しています。本契約は、本契約で明示的に定められている場合を除き、いかなる意味においても、許諾者が保有する許諾プログラムに関する一切の権利および、いかなる商標、商号、もしくはサービス・マークに関する権利をも受領者に移転するものではありません。
|
||||
|
||||
1.受領者は本契約に定める条件に従い、許諾プログラムを任意の数のコンピュータにインストールし、当該コンピュータで使用することができます。
|
||||
2.受領者はコンピュータにインストールされた許諾プログラムをそのまま、または改変を行ったうえで、印刷物およびデジタル・コンテンツにおいて、文字テキスト表現等として使用することができます。
|
||||
3.受領者は前項の定めに従い作成した印刷物およびデジタル・コンテンツにつき、その商用・非商用の別、および放送、通信、各種記録メディアなどの媒体の形式を問わず、複製その他の利用をすることができます。
|
||||
4.受領者がデジタル・ドキュメント・ファイルからエンベッドされたフォントを取り出して派生プログラムを作成した場合には、かかる派生プログラムは本契約に定める条件に従う必要があります。
|
||||
5.許諾プログラムのエンベッドされたフォントがデジタル・ドキュメント・ファイル内のデジタル・コンテンツをレンダリングするためにのみ使用される場合において、受領者が当該デジタル・ドキュメント・ファイルを複製その他の利用をする場合には、受領者はかかる行為に関しては本契約の下ではいかなる義務をも負いません。
|
||||
6.受領者は、3条2項の定めに従い、商用・非商用を問わず、許諾プログラムをそのままの状態で改変することなく複製して第三者への譲渡し、公衆送信し、その他の方法で再配布することができます(以下、「再配布」といいます。)。
|
||||
7.受領者は、上記の許諾プログラムについて定められた条件と同様の条件に従って、派生プログラムを作成し、使用し、複製し、再配布することができます。ただし、受領者が派生プログラムを再配布する場合には、3条1項の定めに従うものとします。
|
||||
|
||||
第3条 制限
|
||||
|
||||
前条により付与された使用許諾は、以下の制限に服します。
|
||||
|
||||
1.派生プログラムが前条4項及び7項に基づき再配布される場合には、以下の全ての条件を満たさなければなりません。
|
||||
(1)派生プログラムを再配布する際には、下記もまた、当該派生プログラムと一緒に再配布され、オンラインで提供され、または、郵送費・媒体及び取扱手数料の合計を超えない実費と引き換えに媒体を郵送する方法により提供されなければなりません。
|
||||
(a)派生プログラムの写し; および
|
||||
(b)派生プログラムを作成する過程でフォント開発プログラムによって作成された追加のファイルであって派生プログラムをさらに加工するにあたって利用できるファイルが存在すれば、当該ファイル
|
||||
(2)派生プログラムの受領者が、派生プログラムを、このライセンスの下で最初にリリースされた許諾プログラム(以下、「オリジナル・プログラム」といいます。)に置き換えることができる方法を再配布するものとします。かかる方法は、オリジナル・ファイルからの差分ファイルの提供、または、派生プログラムをオリジナル・プログラムに置き換える方法を示す指示の提供などが考えられます。
|
||||
(3)派生プログラムを、本契約書に定められた条件の下でライセンスしなければなりません。
|
||||
(4)派生プログラムのプログラム名、フォント名またはファイル名として、許諾プログラムが用いているのと同一の名称、またはこれを含む名称を使用してはなりません。
|
||||
(5)本項の要件を満たすためにオンラインで提供し、または媒体を郵送する方法で提供されるものは、その提供を希望するいかなる者によっても提供が可能です。
|
||||
2.受領者が前条6項に基づき許諾プログラムを再配布する場合には、以下の全ての条件を満たさなければなりません。
|
||||
(1)許諾プログラムの名称を変更してはなりません。
|
||||
(2)許諾プログラムに加工その他の改変を加えてはなりません。
|
||||
(3)本契約の写しを許諾プログラムに添付しなければなりません。
|
||||
3.許諾プログラムは、現状有姿で提供されており、許諾プログラムまたは派生プログラムについて、許諾者は一切の明示または黙示の保証(権利の所在、非侵害、商品性、特定目的への適合性を含むがこれに限られません)を行いません。いかなる場合にも、その原因を問わず、契約上の責任か厳格責任か過失その他の不法行為責任かにかかわらず、また事前に通知されたか否かにかかわらず、許諾者は、許諾プログラムまたは派生プログラムのインストール、使用、複製その他の利用または本契約上の権利の行使によって生じた一切の損害(直接・間接・付随的・特別・拡大・懲罰的または結果的損害)(商品またはサービスの代替品の調達、システム障害から生じた損害、現存するデータまたはプログラムの紛失または破損、逸失利益を含むがこれに限られません)について責任を負いません。
|
||||
4.許諾プログラムまたは派生プログラムのインストール、使用、複製その他の利用に関して、許諾者は技術的な質問や問い合わせ等に対する対応その他、いかなるユーザ・サポートをも行う義務を負いません。
|
||||
|
||||
第4条 契約の終了
|
||||
|
||||
1.本契約の有効期間は、受領者が許諾プログラムを受領した時に開始し、受領者が許諾プログラムを何らかの方法で保持する限り続くものとします。
|
||||
2.前項の定めにかかわらず、受領者が本契約に定める各条項に違反したときは、本契約は、何らの催告を要することなく、自動的に終了し、当該受領者はそれ以後、許諾プログラムおよび派生プログラムを一切使用しまたは複製その他の利用をすることができないものとします。ただし、かかる契約の終了は、当該違反した受領者から許諾プログラムまたは派生プログラムの配布を受けた受領者の権利に影響を及ぼすものではありません。
|
||||
|
||||
第5条 準拠法
|
||||
|
||||
1.IPAは、本契約の変更バージョンまたは新しいバージョンを公表することができます。その場合には、受領者は、許諾プログラムまたは派生プログラムの使用、複製その他の利用または再配布にあたり、本契約または変更後の契約のいずれかを選択することができます。その他、上記に記載されていない条項に関しては日本の著作権法および関連法規に従うものとします。
|
||||
2.本契約は、日本法に基づき解釈されます。
|
||||
|
||||
|
||||
----------
|
||||
|
||||
IPA Font License Agreement v1.0
|
||||
|
||||
The Licensor provides the Licensed Program (as defined in Article 1 below) under the terms of this license agreement (“Agreement”). Any use, reproduction or distribution of the Licensed Program, or any exercise of rights under this Agreement by a Recipient (as defined in Article 1 below) constitutes the Recipient's acceptance of this Agreement.
|
||||
|
||||
Article 1 (Definitions)
|
||||
1.“Digital Font Program” shall mean a computer program containing, or used to render or display fonts.
|
||||
2.“Licensed Program” shall mean a Digital Font Program licensed by the Licensor under this Agreement.
|
||||
3.“Derived Program” shall mean a Digital Font Program created as a result of a modification, addition, deletion, replacement or any other adaptation to or of a part or all of the Licensed Program, and includes a case where a Digital Font Program newly created by retrieving font information from a part or all of the Licensed Program or Embedded Fonts from a Digital Document File with or without modification of the retrieved font information.
|
||||
4.“Digital Content” shall mean products provided to end users in the form of digital data, including video content, motion and/or still pictures, TV programs or other broadcasting content and products consisting of character text, pictures, photographic images, graphic symbols and/or the like.
|
||||
5.“Digital Document File” shall mean a PDF file or other Digital Content created by various software programs in which a part or all of the Licensed Program becomes embedded or contained in the file for the display of the font (“Embedded Fonts”). Embedded Fonts are used only in the display of characters in the particular Digital Document File within which they are embedded, and shall be distinguished from those in any Digital Font Program, which may be used for display of characters outside that particular Digital Document File.
|
||||
6.“Computer” shall include a server in this Agreement.
|
||||
7.“Reproduction and Other Exploitation” shall mean reproduction, transfer, distribution, lease, public transmission, presentation, exhibition, adaptation and any other exploitation.
|
||||
8.“Recipient” shall mean anyone who receives the Licensed Program under this Agreement, including one that receives the Licensed Program from a Recipient.
|
||||
|
||||
Article 2 (Grant of License)
|
||||
The Licensor grants to the Recipient a license to use the Licensed Program in any and all countries in accordance with each of the provisions set forth in this Agreement. However, any and all rights underlying in the Licensed Program shall be held by the Licensor. In no sense is this Agreement intended to transfer any right relating to the Licensed Program held by the Licensor except as specifically set forth herein or any right relating to any trademark, trade name, or service mark to the Recipient.
|
||||
|
||||
1.The Recipient may install the Licensed Program on any number of Computers and use the same in accordance with the provisions set forth in this Agreement.
|
||||
2.The Recipient may use the Licensed Program, with or without modification in printed materials or in Digital Content as an expression of character texts or the like.
|
||||
3.The Recipient may conduct Reproduction and Other Exploitation of the printed materials and Digital Content created in accordance with the preceding Paragraph, for commercial or non-commercial purposes and in any form of media including but not limited to broadcasting, communication and various recording media.
|
||||
4.If any Recipient extracts Embedded Fonts from a Digital Document File to create a Derived Program, such Derived Program shall be subject to the terms of this agreement.
|
||||
5.If any Recipient performs Reproduction or Other Exploitation of a Digital Document File in which Embedded Fonts of the Licensed Program are used only for rendering the Digital Content within such Digital Document File then such Recipient shall have no further obligations under this Agreement in relation to such actions.
|
||||
6.The Recipient may reproduce the Licensed Program as is without modification and transfer such copies, publicly transmit or otherwise redistribute the Licensed Program to a third party for commercial or non-commercial purposes (“Redistribute”), in accordance with the provisions set forth in Article 3 Paragraph 2.
|
||||
7.The Recipient may create, use, reproduce and/or Redistribute a Derived Program under the terms stated above for the Licensed Program: provided, that the Recipient shall follow the provisions set forth in Article 3 Paragraph 1 when Redistributing the Derived Program.
|
||||
|
||||
Article 3 (Restriction)
|
||||
The license granted in the preceding Article shall be subject to the following restrictions:
|
||||
|
||||
1.If a Derived Program is Redistributed pursuant to Paragraph 4 and 7 of the preceding Article, the following conditions must be met :
|
||||
(1)The following must be also Redistributed together with the Derived Program, or be made available online or by means of mailing mechanisms in exchange for a cost which does not exceed the total costs of postage, storage medium and handling fees:
|
||||
(a)a copy of the Derived Program; and
|
||||
(b)any additional file created by the font developing program in the course of creating the Derived Program that can be used for further modification of the Derived Program, if any.
|
||||
(2)It is required to also Redistribute means to enable recipients of the Derived Program to replace the Derived Program with the Licensed Program first released under this License (the “Original Program”). Such means may be to provide a difference file from the Original Program, or instructions setting out a method to replace the Derived Program with the Original Program.
|
||||
(3)The Recipient must license the Derived Program under the terms and conditions of this Agreement.
|
||||
(4)No one may use or include the name of the Licensed Program as a program name, font name or file name of the Derived Program.
|
||||
(5)Any material to be made available online or by means of mailing a medium to satisfy the requirements of this paragraph may be provided, verbatim, by any party wishing to do so.
|
||||
2.If the Recipient Redistributes the Licensed Program pursuant to Paragraph 6 of the preceding Article, the Recipient shall meet all of the following conditions:
|
||||
(1)The Recipient may not change the name of the Licensed Program.
|
||||
(2)The Recipient may not alter or otherwise modify the Licensed Program.
|
||||
(3)The Recipient must attach a copy of this Agreement to the Licensed Program.
|
||||
3.THIS LICENSED PROGRAM IS PROVIDED BY THE LICENSOR “AS IS” AND ANY EXPRESSED OR IMPLIED WARRANTY AS TO THE LICENSED PROGRAM OR ANY DERIVED PROGRAM, INCLUDING, BUT NOT LIMITED TO, WARRANTIES OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE, ARE DISCLAIMED. IN NO EVENT SHALL THE LICENSOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXTENDED, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO; PROCUREMENT OF SUBSTITUTED GOODS OR SERVICE; DAMAGES ARISING FROM SYSTEM FAILURE; LOSS OR CORRUPTION OF EXISTING DATA OR PROGRAM; LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE INSTALLATION, USE, THE REPRODUCTION OR OTHER EXPLOITATION OF THE LICENSED PROGRAM OR ANY DERIVED PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
|
||||
4.The Licensor is under no obligation to respond to any technical questions or inquiries, or provide any other user support in connection with the installation, use or the Reproduction and Other Exploitation of the Licensed Program or Derived Programs thereof.
|
||||
|
||||
Article 4 (Termination of Agreement)
|
||||
1.The term of this Agreement shall begin from the time of receipt of the Licensed Program by the Recipient and shall continue as long as the Recipient retains any such Licensed Program in any way.
|
||||
2.Notwithstanding the provision set forth in the preceding Paragraph, in the event of the breach of any of the provisions set forth in this Agreement by the Recipient, this Agreement shall automatically terminate without any notice. In the case of such termination, the Recipient may not use or conduct Reproduction and Other Exploitation of the Licensed Program or a Derived Program: provided that such termination shall not affect any rights of any other Recipient receiving the Licensed Program or the Derived Program from such Recipient who breached this Agreement.
|
||||
|
||||
Article 5 (Governing Law)
|
||||
1.IPA may publish revised and/or new versions of this License. In such an event, the Recipient may select either this Agreement or any subsequent version of the Agreement in using, conducting the Reproduction and Other Exploitation of, or Redistributing the Licensed Program or a Derived Program. Other matters not specified above shall be subject to the Copyright Law of Japan and other related laws and regulations of Japan.
|
||||
2.This Agreement shall be construed under the laws of Japan.
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// NanoJPEG -- KeyJ's Tiny Baseline JPEG Decoder
|
||||
// version 1.3.5 (2016-11-14)
|
||||
// Copyright (c) 2009-2016 Martin J. Fiedler <martin.fiedler@gmx.net>
|
||||
// published under the terms of the MIT license
|
||||
//
|
||||
// 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.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Censor Lists for Streamer Mode
|
||||
|
||||
This directory contains sample censor lists for various games, intended for use
|
||||
with xsystem35's Streamer Mode.
|
||||
|
||||
## Usage
|
||||
|
||||
To use a censor list, specify its path with the `-censor` command-line option
|
||||
or in your `.xsys35rc` file. For example:
|
||||
|
||||
```bash
|
||||
xsystem35 -censor misc/censor/kichikuou.txt
|
||||
```
|
||||
|
||||
The list files contain image numbers (integers), one per line. A `#` starts a
|
||||
comment that continues to the end of the line.
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions of censor lists for other games! If you have created
|
||||
a censor list that you'd like to share, please open a Pull Request.
|
||||
@@ -0,0 +1,164 @@
|
||||
# Censor list for 鬼畜王ランス (Kichikuou Rance)
|
||||
27
|
||||
70
|
||||
93
|
||||
101
|
||||
102
|
||||
103
|
||||
106
|
||||
107
|
||||
108
|
||||
109
|
||||
110
|
||||
111
|
||||
112
|
||||
113
|
||||
114
|
||||
115
|
||||
116
|
||||
117
|
||||
118
|
||||
119
|
||||
120
|
||||
121
|
||||
123
|
||||
124
|
||||
125
|
||||
126
|
||||
127
|
||||
128
|
||||
129
|
||||
130
|
||||
131
|
||||
132
|
||||
133
|
||||
134
|
||||
136
|
||||
137
|
||||
138
|
||||
139
|
||||
140
|
||||
141
|
||||
142
|
||||
143
|
||||
144
|
||||
145
|
||||
146
|
||||
147
|
||||
148
|
||||
149
|
||||
150
|
||||
151
|
||||
152
|
||||
153
|
||||
154
|
||||
155
|
||||
156
|
||||
157
|
||||
158
|
||||
160
|
||||
161
|
||||
162
|
||||
163
|
||||
164
|
||||
165
|
||||
166
|
||||
167
|
||||
168
|
||||
169
|
||||
171
|
||||
172
|
||||
174
|
||||
175
|
||||
176
|
||||
177
|
||||
178
|
||||
179
|
||||
180
|
||||
181
|
||||
182
|
||||
183
|
||||
184
|
||||
185
|
||||
186
|
||||
187
|
||||
188
|
||||
189
|
||||
190
|
||||
191
|
||||
193
|
||||
195
|
||||
196
|
||||
197
|
||||
198
|
||||
199
|
||||
200
|
||||
201
|
||||
203
|
||||
204
|
||||
205
|
||||
206
|
||||
207
|
||||
208
|
||||
209
|
||||
210
|
||||
214
|
||||
215
|
||||
216
|
||||
217
|
||||
218
|
||||
219
|
||||
221
|
||||
222
|
||||
224
|
||||
225
|
||||
226
|
||||
227
|
||||
228
|
||||
229
|
||||
230
|
||||
231
|
||||
232
|
||||
233
|
||||
234
|
||||
235
|
||||
239
|
||||
299
|
||||
300
|
||||
361
|
||||
362
|
||||
363
|
||||
375
|
||||
376
|
||||
377
|
||||
393
|
||||
422
|
||||
461
|
||||
466
|
||||
500
|
||||
501
|
||||
502
|
||||
503
|
||||
504
|
||||
505
|
||||
506
|
||||
507
|
||||
508
|
||||
509
|
||||
510
|
||||
511
|
||||
512
|
||||
513
|
||||
514
|
||||
515
|
||||
517
|
||||
844
|
||||
845
|
||||
846
|
||||
847
|
||||
848
|
||||
851
|
||||
852
|
||||
853
|
||||
854
|
||||
855
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ add_library(modules STATIC
|
||||
MsgSkip/MsgSkip.c
|
||||
NIGHTDLL/NIGHTDLL.c
|
||||
NIGHTDLL/nt_scenario.c
|
||||
NIGHTDLL/sactcg.c
|
||||
NIGHTDLL/sactstring.c
|
||||
NIGHTDLL/sprite.c
|
||||
NIGHTDLL/sprite_draw.c
|
||||
@@ -30,7 +29,6 @@ add_library(modules STATIC
|
||||
RandMT/RandMT.c
|
||||
S3xMusic/S3xMusic.c
|
||||
SACT/SACT.c
|
||||
SACT/sactcg.c
|
||||
SACT/sactsound.c
|
||||
SACT/sacttimer.c
|
||||
SACT/sactstring.c
|
||||
@@ -68,6 +66,7 @@ add_library(modules STATIC
|
||||
lib/drawtext.c
|
||||
lib/list.c
|
||||
lib/graph_blend_amap.c
|
||||
lib/sactcg.c
|
||||
nDEMO/nDEMO.c
|
||||
nDEMOE/nDEMOE.c
|
||||
oDEMO/oDEMO.c
|
||||
@@ -85,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);
|
||||
|
||||
|
||||
+4
-4
@@ -33,7 +33,7 @@
|
||||
#include "ags.h"
|
||||
#include "pms.h"
|
||||
#include "qnt.h"
|
||||
#include "sdl_core.h"
|
||||
#include "gfx.h"
|
||||
#include "ngraph.h"
|
||||
#include "dri.h"
|
||||
#include "ald_manager.h"
|
||||
@@ -73,21 +73,21 @@ static surface_t *sf_getcg(void *b, size_t size) {
|
||||
cg->alpha = cg->pic;
|
||||
cg->pic = NULL;
|
||||
sf = sf_create_alpha(cg->width, cg->height);
|
||||
sdl_drawImageAlphaMap(cg, sf, 0, 0);
|
||||
gfx_drawImageAlphaMap(cg, sf, 0, 0);
|
||||
break;
|
||||
case ALCG_PMS16:
|
||||
cg = pms64k_extract(b);
|
||||
sf = cg->alpha
|
||||
? sf_create_surface(cg->width, cg->height)
|
||||
: sf_create_pixel(cg->width, cg->height);
|
||||
sdl_drawImage16(cg, sf, 0, 0, 255, false);
|
||||
gfx_drawImage16(cg, sf, 0, 0, 255, false);
|
||||
break;
|
||||
case ALCG_QNT:
|
||||
cg = qnt_extract(b);
|
||||
sf = cg->alpha
|
||||
? sf_create_surface(cg->width, cg->height)
|
||||
: sf_create_pixel(cg->width, cg->height);
|
||||
sdl_drawImage24(cg, sf, 0, 0, 255);
|
||||
gfx_drawImage24(cg, sf, 0, 0, 255);
|
||||
break;
|
||||
default:
|
||||
WARNING("Unknown Cg Type");
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
#include "surface.h"
|
||||
#include "sdl_core.h"
|
||||
#include "effect.h"
|
||||
#include "ags.h"
|
||||
#include "input.h"
|
||||
#include "graph.h"
|
||||
@@ -17,24 +17,24 @@ 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;
|
||||
|
||||
*endtype = 0;
|
||||
|
||||
enum sdl_effect_type type = from_sact_effect(no);
|
||||
enum effect_type type = from_sact_effect(no);
|
||||
if (type == EFFECT_INVALID) {
|
||||
WARNING("Unimplemented effect %d", no);
|
||||
type = EFFECT_CROSSFADE;
|
||||
}
|
||||
SDL_Rect rect = { wx, wy, width, height };
|
||||
struct sdl_effect *eff = sdl_effect_init(&rect, dst, dx, dy, src, sx, sy, type);
|
||||
struct effect *eff = effect_init(&rect, dst, dx, dy, src, sx, sy, type);
|
||||
if (!time)
|
||||
time = (no == SACT_EFFECT_CROSSFADE_DOWN || no == SACT_EFFECT_CROSSFADE_UP) ? 1150 : 2700;
|
||||
ags_runEffect(time, false, (ags_EffectStepFunc)sdl_effect_step, eff);
|
||||
sdl_effect_finish(eff);
|
||||
ags_runEffect(time, false, (ags_EffectStepFunc)effect_step, eff);
|
||||
effect_finish(eff);
|
||||
|
||||
switch (no) {
|
||||
case SACT_EFFECT_FADEOUT:
|
||||
|
||||
@@ -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__ */
|
||||
|
||||
+13
-12
@@ -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"
|
||||
@@ -11,7 +11,7 @@
|
||||
#include "graph.h"
|
||||
#include "ngraph.h"
|
||||
#include "ags.h"
|
||||
#include "sdl_core.h"
|
||||
#include "gfx.h"
|
||||
|
||||
/**
|
||||
* surface から surface にコピーなどをする際に、転送元と転送先のsurface
|
||||
@@ -33,8 +33,8 @@
|
||||
*/
|
||||
bool gr_clip(surface_t *ss, int *sx, int *sy, int *sw, int *sh, surface_t *ds, int *dx, int *dy) {
|
||||
if (!ss || !ds) return false;
|
||||
MyRectangle src_window = { 0, 0, ss->width, ss->height };
|
||||
MyRectangle dst_window = { 0, 0, ds->width, ds->height };
|
||||
SDL_Rect src_window = { 0, 0, ss->width, ss->height };
|
||||
SDL_Rect dst_window = { 0, 0, ds->width, ds->height };
|
||||
return ags_clipCopyRect(&src_window, &dst_window, sx, sy, dx, dy, sw, sh);
|
||||
}
|
||||
|
||||
@@ -75,13 +75,14 @@ void gr_copy(surface_t *dst, int dx, int dy, surface_t *src, int sx, int sy, int
|
||||
if (dst == src) {
|
||||
SDL_Rect r;
|
||||
if (SDL_IntersectRect(&src_rect, &dst_rect, &r)) {
|
||||
SDL_Surface *view = sdl_createSurfaceView(src->sdl_surface, sx, sy, sw, sh);
|
||||
SDL_Surface *tmp = SDL_ConvertSurface(view, dst->sdl_surface->format, 0);
|
||||
SDL_Surface *view = gfx_createSurfaceView(src->sdl_surface, sx, sy, sw, sh);
|
||||
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;
|
||||
|
||||
|
||||
+26
-27
@@ -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 */
|
||||
@@ -46,7 +46,7 @@ static void NIGHTDLL_reset(void) {
|
||||
if (night.sp[i])
|
||||
nt_sp_free(night.sp[i]);
|
||||
}
|
||||
nt_scg_freeall();
|
||||
scg_freeall();
|
||||
memset(&night, 0, sizeof(night));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
#include "config.h"
|
||||
#include "portab.h"
|
||||
#include "graphics.h"
|
||||
#include "ags.h"
|
||||
#include "sprite.h"
|
||||
|
||||
@@ -38,11 +37,7 @@ struct _night {
|
||||
int waitskiplv;
|
||||
int waitkey;
|
||||
|
||||
|
||||
// graph
|
||||
// cginfo_t *cg[CGMAX];
|
||||
|
||||
MyRectangle updaterect;
|
||||
SDL_Rect updaterect;
|
||||
|
||||
FontType fonttype;
|
||||
int fontsize;
|
||||
|
||||
@@ -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"
|
||||
@@ -53,8 +53,8 @@ void nt_gr_set_wallpaper(int no) {
|
||||
sp = nt_sp_new(SPNO_WALL, no, 0, 0, SPRITE_WP);
|
||||
nt_sp_add_updatelist(sp);
|
||||
if (no == 0) {
|
||||
sp->cursize.width = main_surface->w;
|
||||
sp->cursize.height = main_surface->h;
|
||||
sp->width = main_surface->w;
|
||||
sp->height = main_surface->h;
|
||||
sp->update = nt_sp_draw_wall;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+19
-20
@@ -3,11 +3,10 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
#include "sdl_core.h"
|
||||
#include "utfsjis.h"
|
||||
#include "ags.h"
|
||||
#include "nact.h"
|
||||
@@ -83,7 +82,7 @@ void ntmsg_init() {
|
||||
nt_sp_add_updatelist(sp);
|
||||
|
||||
// 文字背景CG作成
|
||||
nt_scg_create(CGNO_MSGFR_BG,
|
||||
scg_create(CGNO_MSGFR_BG,
|
||||
main_surface->w, main_surface->h,
|
||||
0, 0, 0, 255);
|
||||
// night.cg[CGNO_MSGFR_BG]->refcnt++;
|
||||
@@ -93,9 +92,9 @@ void ntmsg_init() {
|
||||
nt_sp_add_updatelist(sp);
|
||||
|
||||
// キー入力アニメーションCG作成
|
||||
nt_scg_cut(CGNO_MSGHAK_1, CGNO_MSGHAK_LCG, 0, 0, 9, 9);
|
||||
scg_cut(CGNO_MSGHAK_1, CGNO_MSGHAK_LCG, 0, 0, 9, 9);
|
||||
//night.cg[CGNO_MSGHAK_1]->refcnt++;
|
||||
nt_scg_cut(CGNO_MSGHAK_2, CGNO_MSGHAK_LCG, 0, 9, 9, 9);
|
||||
scg_cut(CGNO_MSGHAK_2, CGNO_MSGHAK_LCG, 0, 9, 9, 9);
|
||||
//night.cg[CGNO_MSGHAK_2]->refcnt++;
|
||||
sp = night.sp[SPNO_MSG_KEYANIM] = nt_sp_new(SPNO_MSG_KEYANIM, CGNO_MSGHAK_1, CGNO_MSGHAK_2, 0, SPRITE_ANIME);
|
||||
sp->u.anime.interval = HAKANIM_INTERVAL;
|
||||
@@ -143,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;
|
||||
@@ -152,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);
|
||||
|
||||
@@ -217,7 +216,7 @@ static void ntmsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB
|
||||
char *msg;
|
||||
sprite_t *sp;
|
||||
bool needupdate = false;
|
||||
MyRectangle uparea = {0,0,0,0};
|
||||
SDL_Rect uparea = {0,0,0,0};
|
||||
int savex;
|
||||
|
||||
if (night.msgbuf[0] == '\0') return;
|
||||
@@ -236,7 +235,7 @@ static void ntmsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB
|
||||
char mbuf[20];
|
||||
int cw, delta, wcnt;
|
||||
|
||||
wcnt = sdl_getTicks();
|
||||
wcnt = sys_get_ticks();
|
||||
|
||||
mbuf[0] = '\0';
|
||||
msg = get_char(msg, mbuf, sizeof(mbuf) -1);
|
||||
@@ -267,7 +266,7 @@ static void ntmsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB
|
||||
needupdate = false;
|
||||
|
||||
// keywait
|
||||
delta = sdl_getTicks() - wcnt;
|
||||
delta = sys_get_ticks() - wcnt;
|
||||
if (delta < wSpeed) {
|
||||
if (sys_keywait(wSpeed - delta, KEYWAIT_NONCANCELABLE)) {
|
||||
|
||||
@@ -284,8 +283,8 @@ static void ntmsg_out(int wNum, int wSize, int wColorR, int wColorG, int wColorB
|
||||
|
||||
// Waitなしの出力は最後にupdate
|
||||
if (needupdate) {
|
||||
uparea.w = sp->cursize.width;
|
||||
uparea.h = min(sp->cursize.height, uparea.y - sp->u.msg.dspcur.y + wLineSpace + wLineSpace);
|
||||
uparea.w = sp->width;
|
||||
uparea.h = min(sp->height, uparea.y - sp->u.msg.dspcur.y + wLineSpace + wLineSpace);
|
||||
nt_sp_updateme_part(sp, uparea.x, uparea.y, uparea.w, uparea.h);
|
||||
}
|
||||
|
||||
@@ -311,14 +310,14 @@ static int ntmsg_keywait() {
|
||||
night.waitkey = -1;
|
||||
|
||||
while (night.waitkey == -1 && !nact->is_quit) {
|
||||
int st = sdl_getTicks();
|
||||
int st = sys_get_ticks();
|
||||
int interval = 25;
|
||||
|
||||
if (!night.zhiding) {
|
||||
interval = night.sp[SPNO_MSG_KEYANIM]->u.anime.interval;
|
||||
hakanim(i++);
|
||||
}
|
||||
sys_keywait(interval - (sdl_getTicks() - st), KEYWAIT_NONCANCELABLE);
|
||||
sys_keywait(interval - (sys_get_ticks() - st), KEYWAIT_NONCANCELABLE);
|
||||
}
|
||||
|
||||
night.waittype = KEYWAIT_NONE;
|
||||
@@ -355,8 +354,8 @@ static void set_align(char *msg, sprite_t *sp, int wSize) {
|
||||
|
||||
maxchar = max(maxchar, c);
|
||||
line++;
|
||||
sp->u.msg.dspcur.x = (sp->cursize.width - (maxchar * wSize/2)) /2;
|
||||
sp->u.msg.dspcur.y = (sp->cursize.height - (line * (wSize+2))) /2;
|
||||
sp->u.msg.dspcur.x = (sp->width - (maxchar * wSize/2)) /2;
|
||||
sp->u.msg.dspcur.y = (sp->height - (line * (wSize+2))) /2;
|
||||
break;
|
||||
case 2:
|
||||
sp->u.msg.dspcur.x = msgframe_2.x;
|
||||
@@ -436,14 +435,14 @@ static void hakanim(int i) {
|
||||
sp->show = show;
|
||||
}
|
||||
|
||||
void ntmsg_update(sprite_t *sp, MyRectangle *r) {
|
||||
SDL_Rect sp_rect = {0, 0, sp->cursize.width, sp->cursize.height};
|
||||
void ntmsg_update(sprite_t *sp, SDL_Rect *r) {
|
||||
SDL_Rect sp_rect = {0, 0, sp->width, sp->height};
|
||||
int sx = 0;
|
||||
int sy = 0;
|
||||
int dx = sp->cur.x;
|
||||
int dy = sp->cur.y;
|
||||
int w = sp->cursize.width;
|
||||
int h = sp->cursize.height;
|
||||
int w = sp->width;
|
||||
int h = sp->height;
|
||||
if (!ags_clipCopyRect(&sp_rect, r, &sx, &sy, &dx, &dy, &w, &h)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,6 @@ void ntmsg_set_place(int type);
|
||||
void ntmsg_newline();
|
||||
void ntmsg_add(const char *msg);
|
||||
int ntmsg_ana(void);
|
||||
void ntmsg_update(sprite_t *sp, MyRectangle *r);
|
||||
void ntmsg_update(sprite_t *sp, SDL_Rect *r);
|
||||
|
||||
#endif /* __NT_MSG_H__ */
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
#include "input.h"
|
||||
#include "sdl_core.h"
|
||||
#include "scheduler.h"
|
||||
#include "xsystem35.h"
|
||||
#include "nact.h"
|
||||
@@ -56,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,146 +0,0 @@
|
||||
/*
|
||||
* sactcg.c: CG作成
|
||||
*
|
||||
* Copyright (C) 1997-1998 Masaki Chikama (Wren) <chikama@kasumi.ipl.mech.nagoya-u.ac.jp>
|
||||
* 1998- <masaki-c@is.aist-nara.ac.jp>
|
||||
*
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
/* $Id: sactcg.c,v 1.2 2003/11/16 15:29:52 chikama Exp $ */
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <SDL.h>
|
||||
|
||||
#include "portab.h"
|
||||
#include "nact.h"
|
||||
#include "system.h"
|
||||
#include "ags.h"
|
||||
#include "cg.h"
|
||||
#include "sactcg.h"
|
||||
|
||||
#define CGMAX 65536
|
||||
static cginfo_t *cgs[CGMAX];
|
||||
|
||||
#define SPCG_ASSERT_NO(no) \
|
||||
if ((no) > (CGMAX -1)) { \
|
||||
WARNING("no is too large (should be %d < %d)", (no), CGMAX); \
|
||||
return; \
|
||||
} \
|
||||
|
||||
static cginfo_t *nt_scg_new(enum cgtype type, int no, SDL_Surface *sf) {
|
||||
cginfo_t *info = calloc(1, sizeof(cginfo_t));
|
||||
info->type = type;
|
||||
info->no = no;
|
||||
info->sf = sf;
|
||||
info->refcnt = 1;
|
||||
|
||||
nt_scg_free(no);
|
||||
cgs[no] = info;
|
||||
|
||||
return info;
|
||||
}
|
||||
|
||||
/*
|
||||
cgの読み込み
|
||||
|
||||
指定の番号のCGをリンクファイルから読み込んだり、
|
||||
CG_xxxで作成したCGを参照する
|
||||
|
||||
@param no: 読み込むCG番号
|
||||
*/
|
||||
static cginfo_t *nt_scg_get(int no) {
|
||||
if (no >= (CGMAX -1)) {
|
||||
WARNING("no is too large (should be %d < %d)", (no), CGMAX);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (cgs[no] != NULL)
|
||||
return cgs[no];
|
||||
|
||||
SDL_Surface *sf = cg_load_as_sdlsurface(no - 1);
|
||||
if (!sf) {
|
||||
WARNING("load fail (%d)", no -1);
|
||||
return NULL;
|
||||
}
|
||||
return nt_scg_new(CG_LINKED, no, sf);
|
||||
}
|
||||
|
||||
cginfo_t *nt_scg_addref(int no) {
|
||||
cginfo_t *info = nt_scg_get(no);
|
||||
if (info)
|
||||
info->refcnt++;
|
||||
return info;
|
||||
}
|
||||
|
||||
void nt_scg_deref(cginfo_t *cg) {
|
||||
if (--cg->refcnt > 0)
|
||||
return;
|
||||
|
||||
if (cg->sf)
|
||||
SDL_FreeSurface(cg->sf);
|
||||
free(cg);
|
||||
}
|
||||
|
||||
// 指定の大きさ、色の矩形の CG を作成
|
||||
void nt_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));
|
||||
nt_scg_new(CG_SET, wNumCG, sf);
|
||||
}
|
||||
|
||||
// CGの一部を切りぬいたCGを作成
|
||||
void nt_scg_cut(int wNumDstCG, int wNumSrcCG, int wX, int wY, int wWidth, int wHeight) {
|
||||
SPCG_ASSERT_NO(wNumDstCG);
|
||||
SPCG_ASSERT_NO(wNumSrcCG);
|
||||
|
||||
cginfo_t *src = nt_scg_get(wNumSrcCG);
|
||||
if (!src)
|
||||
return;
|
||||
|
||||
SDL_Surface *sf = SDL_CreateRGBSurfaceWithFormat(0, wWidth, wHeight, src->sf->format->BitsPerPixel, src->sf->format->format);
|
||||
SDL_BlitSurface(src->sf, &(SDL_Rect){wX, wY, wWidth, wHeight}, sf, NULL);
|
||||
|
||||
nt_scg_new(CG_SET, wNumDstCG, sf);
|
||||
}
|
||||
|
||||
// 全てのCGの開放
|
||||
void nt_scg_freeall(void) {
|
||||
int i;
|
||||
|
||||
for (i = 1; i < CGMAX; i++) {
|
||||
nt_scg_free(i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定の番号の CG をオブジェクトリストから消し、オブジェクトがどこからも参照
|
||||
* されていない(参照数が0の)場合のみ、オブジェクトを削除
|
||||
*/
|
||||
void nt_scg_free(int no) {
|
||||
SPCG_ASSERT_NO(no);
|
||||
|
||||
cginfo_t *cg = cgs[no];
|
||||
if (!cg) return;
|
||||
|
||||
nt_scg_deref(cg);
|
||||
cgs[no] = NULL;
|
||||
}
|
||||
+22
-21
@@ -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"
|
||||
@@ -16,9 +16,9 @@ sprite_t *nt_sp_new(int no, int cg1, int cg2, int cg3, int type) {
|
||||
sp->no = no;
|
||||
sp->type = type;
|
||||
|
||||
sp->cg1 = cg1 ? nt_scg_addref(cg1) : NULL;
|
||||
sp->cg2 = cg2 ? nt_scg_addref(cg2) : NULL;
|
||||
sp->cg3 = cg3 ? nt_scg_addref(cg3) : NULL;
|
||||
sp->cg1 = cg1 ? scg_addref(cg1) : NULL;
|
||||
sp->cg2 = cg2 ? scg_addref(cg2) : NULL;
|
||||
sp->cg3 = cg3 ? scg_addref(cg3) : NULL;
|
||||
|
||||
sp->curcg = sp->cg1;
|
||||
sp->show = true;
|
||||
@@ -28,11 +28,11 @@ sprite_t *nt_sp_new(int no, int cg1, int cg2, int cg3, int type) {
|
||||
sp->cur = sp->loc;
|
||||
|
||||
if (sp->curcg == NULL) {
|
||||
sp->cursize.width = 0;
|
||||
sp->cursize.height = 0;
|
||||
sp->width = 0;
|
||||
sp->height = 0;
|
||||
} else {
|
||||
sp->cursize.width = sp->curcg->sf->w;
|
||||
sp->cursize.height = sp->curcg->sf->h;
|
||||
sp->width = sp->curcg->sf->w;
|
||||
sp->height = sp->curcg->sf->h;
|
||||
}
|
||||
sp->update = DEFAULT_UPDATE;
|
||||
|
||||
@@ -58,11 +58,12 @@ sprite_t *nt_sp_msg_new(int no, int x, int y, int width, int height) {
|
||||
sp->loc.x = x;
|
||||
sp->loc.y = y;
|
||||
sp->cur = sp->loc;
|
||||
sp->cursize.width = width;
|
||||
sp->cursize.height = height;
|
||||
sp->width = width;
|
||||
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;
|
||||
@@ -71,12 +72,12 @@ sprite_t *nt_sp_msg_new(int no, int x, int y, int width, int height) {
|
||||
void nt_sp_free(sprite_t *sp) {
|
||||
if (sp == NULL) return;
|
||||
|
||||
if (sp->cg1) nt_scg_deref(sp->cg1);
|
||||
if (sp->cg2) nt_scg_deref(sp->cg2);
|
||||
if (sp->cg3) nt_scg_deref(sp->cg3);
|
||||
if (sp->cg1) scg_deref(sp->cg1);
|
||||
if (sp->cg2) scg_deref(sp->cg2);
|
||||
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);
|
||||
@@ -102,16 +103,16 @@ void nt_sp_set_cg(sprite_t *sp, int no) {
|
||||
if (sp == NULL) return;
|
||||
|
||||
if (sp->curcg)
|
||||
nt_scg_deref(sp->curcg);
|
||||
scg_deref(sp->curcg);
|
||||
|
||||
cg = no ? nt_scg_addref(no) : NULL;
|
||||
cg = no ? scg_addref(no) : NULL;
|
||||
|
||||
if (cg == NULL) {
|
||||
sp->cursize.width = 0;
|
||||
sp->cursize.height = 0;
|
||||
sp->width = 0;
|
||||
sp->height = 0;
|
||||
} else {
|
||||
sp->cursize.width = cg->sf->w;
|
||||
sp->cursize.height = cg->sf->h;
|
||||
sp->width = cg->sf->w;
|
||||
sp->height = cg->sf->h;
|
||||
}
|
||||
|
||||
sp->curcg = cg;
|
||||
|
||||
+11
-29
@@ -3,7 +3,8 @@
|
||||
#define __SPRITE_H__
|
||||
|
||||
#include "portab.h"
|
||||
#include "graphics.h"
|
||||
#include "sdl_compat.h"
|
||||
#include "sactcg.h"
|
||||
|
||||
#define DEFAULT_UPDATE nt_sp_draw
|
||||
|
||||
@@ -21,7 +22,6 @@
|
||||
#define SPNO_MSG_KEYANIM 12 // メッセージキー入力アニメーション
|
||||
#define SPNO_MSG_ICON_MUTE 13 // メッセージウィンドアイコン
|
||||
|
||||
#define CGMAX 65536
|
||||
// 0-9999: reserve for Link CG
|
||||
// 2051: メッセージウィンド枠CG
|
||||
#define CGNO_MSGFRAME_LCG 2051
|
||||
@@ -40,25 +40,6 @@
|
||||
#define CGNO_MSGFR_BG 10002
|
||||
|
||||
|
||||
// CG_XX で作るCGの種類
|
||||
enum cgtype {
|
||||
CG_NOTUSED = 0,
|
||||
CG_LINKED = 1,
|
||||
CG_SET = 2,
|
||||
CG_REVERSE = 3,
|
||||
CG_STRETCH = 4
|
||||
};
|
||||
|
||||
// cgに関する情報
|
||||
struct _cginfo {
|
||||
enum cgtype type; // CGの種類, 0: 未使用, 1:リンクされている, ...
|
||||
int no; // CGの番号
|
||||
struct SDL_Surface *sf;
|
||||
int refcnt; // 参照カウンタ。0になったら開放してもよい。
|
||||
};
|
||||
typedef struct _cginfo cginfo_t;
|
||||
|
||||
|
||||
enum spritetype {
|
||||
SPRITE_NORMAL = 0,
|
||||
SPRITE_ANIME = 5,
|
||||
@@ -72,7 +53,8 @@ struct _sprite {
|
||||
|
||||
int no;
|
||||
|
||||
MyDimension cursize;
|
||||
int width;
|
||||
int height;
|
||||
|
||||
cginfo_t *curcg;
|
||||
cginfo_t *cg1, *cg2, *cg3;
|
||||
@@ -81,11 +63,11 @@ struct _sprite {
|
||||
|
||||
int blendrate;
|
||||
|
||||
MyPoint loc;
|
||||
SDL_Point loc;
|
||||
|
||||
MyPoint cur;
|
||||
SDL_Point cur;
|
||||
|
||||
void (* update)(struct _sprite *sp, MyRectangle *updatearea);
|
||||
void (* update)(struct _sprite *sp, SDL_Rect *updatearea);
|
||||
|
||||
union {
|
||||
struct {
|
||||
@@ -97,7 +79,7 @@ struct _sprite {
|
||||
|
||||
struct {
|
||||
struct SDL_Surface *canvas;
|
||||
MyPoint dspcur;
|
||||
SDL_Point dspcur;
|
||||
} msg;
|
||||
} u;
|
||||
};
|
||||
@@ -120,12 +102,12 @@ void nt_sp_updateme(sprite_t *sp);
|
||||
void nt_sp_updateme_part(sprite_t *sp, int x, int y, int w, int h);
|
||||
void nt_sp_add_updatelist(sprite_t *sp);
|
||||
void nt_sp_remove_updatelist(sprite_t *sp);
|
||||
void nt_sp_draw_wall(sprite_t *sp, MyRectangle *r);
|
||||
void nt_sp_draw_wall(sprite_t *sp, SDL_Rect *r);
|
||||
void nt_sp_clear_updatelist(void);
|
||||
|
||||
/* in nt_sprite_draw.c */
|
||||
void nt_sp_draw(sprite_t *sp, MyRectangle *r);
|
||||
void nt_sp_draw_scg(sprite_t *sp, MyRectangle *r);
|
||||
void nt_sp_draw(sprite_t *sp, SDL_Rect *r);
|
||||
void nt_sp_draw_scg(sprite_t *sp, SDL_Rect *r);
|
||||
|
||||
/* in nt_sprite_eupdate.c */
|
||||
void nt_sp_eupdate(int type, int time, int cancel);
|
||||
|
||||
@@ -24,12 +24,11 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
#include "ags.h"
|
||||
#include "graphics.h"
|
||||
#include "sprite.h"
|
||||
|
||||
/*
|
||||
@@ -37,7 +36,7 @@
|
||||
@param sp: 描画するスプライト
|
||||
@param r : 再描画する領域
|
||||
*/
|
||||
void nt_sp_draw(sprite_t *sp, MyRectangle *r) {
|
||||
void nt_sp_draw(sprite_t *sp, SDL_Rect *r) {
|
||||
if (sp == NULL) return;
|
||||
cginfo_t *cg = sp->curcg;
|
||||
if (cg == NULL) return;
|
||||
@@ -56,7 +55,7 @@ void nt_sp_draw(sprite_t *sp, MyRectangle *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 {
|
||||
@@ -69,7 +68,7 @@ void nt_sp_draw(sprite_t *sp, MyRectangle *r) {
|
||||
}
|
||||
|
||||
// BlendScreenによる描画
|
||||
void nt_sp_draw_scg(sprite_t *sp, MyRectangle *r) {
|
||||
void nt_sp_draw_scg(sprite_t *sp, SDL_Rect *r) {
|
||||
if (sp == NULL) return;
|
||||
cginfo_t *cg = sp->curcg;
|
||||
if (cg == NULL) return;
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
#include "ags.h"
|
||||
#include "input.h"
|
||||
#include "sprite.h"
|
||||
#include "sdl_core.h"
|
||||
#include "gfx.h"
|
||||
#include "effect.h"
|
||||
|
||||
/*
|
||||
効果つき画面更新
|
||||
@@ -44,25 +45,25 @@ void nt_sp_eupdate(int no, int time, int cancel) {
|
||||
|
||||
nt_sp_update_all(false);
|
||||
|
||||
enum sdl_effect_type type = from_sact_effect(no - 100);
|
||||
enum effect_type type = from_sact_effect(no - 100);
|
||||
if (type == EFFECT_INVALID) {
|
||||
WARNING("Unimplemented effect %d", no);
|
||||
type = EFFECT_CROSSFADE;
|
||||
}
|
||||
SDL_Rect rect = { 0, 0, main_surface->w, main_surface->h };
|
||||
struct sdl_effect *eff = sdl_effect_init(&rect, NULL, 0, 0, sdl_getDIB(), 0, 0, type);
|
||||
struct effect *eff = effect_init(&rect, NULL, 0, 0, gfx_getDIB(), 0, 0, type);
|
||||
|
||||
int sttime, curtime;
|
||||
sttime = curtime = sdl_getTicks();
|
||||
sttime = curtime = sys_get_ticks();
|
||||
int edtime = curtime + time;
|
||||
|
||||
while ((curtime = sdl_getTicks()) < edtime) {
|
||||
sdl_effect_step(eff, (float)(curtime - sttime) / (edtime - sttime));
|
||||
int rest = 16 - (sdl_getTicks() - curtime);
|
||||
while ((curtime = sys_get_ticks()) < edtime) {
|
||||
effect_step(eff, (float)(curtime - sttime) / (edtime - sttime));
|
||||
int rest = 16 - (sys_get_ticks() - curtime);
|
||||
int key = sys_keywait(rest, cancel ? KEYWAIT_CANCELABLE : KEYWAIT_NONCANCELABLE);
|
||||
if (cancel && key)
|
||||
break;
|
||||
}
|
||||
sdl_effect_finish(eff);
|
||||
effect_finish(eff);
|
||||
ags_updateFull();
|
||||
}
|
||||
|
||||
@@ -25,13 +25,12 @@
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
#include "list.h"
|
||||
#include "ags.h"
|
||||
#include "graphics.h"
|
||||
#include "sprite.h"
|
||||
|
||||
// スプライト再描画間の間に変更のあったスプライトの領域の和
|
||||
@@ -41,22 +40,22 @@ static SList *updatearea;
|
||||
static SList *updatelist;
|
||||
|
||||
static void disjunction(void* region, void* data);
|
||||
static MyRectangle get_updatearea();
|
||||
static SDL_Rect get_updatearea();
|
||||
static void do_update_each(void* data, void* userdata);
|
||||
|
||||
// 領域1と領域2をすべて含む矩形領域を計算
|
||||
static void disjunction(void* region, void* data) {
|
||||
MyRectangle *r1 = (MyRectangle *)region;
|
||||
MyRectangle *r2 = (MyRectangle *)data;
|
||||
SDL_Rect *r1 = (SDL_Rect *)region;
|
||||
SDL_Rect *r2 = (SDL_Rect *)data;
|
||||
SDL_UnionRect(r1, r2, r2);
|
||||
free(r1);
|
||||
}
|
||||
|
||||
// 更新の必要なスプライトの領域の和をとってクリッピングする
|
||||
static MyRectangle get_updatearea() {
|
||||
MyRectangle clip = {0, 0, 0, 0};
|
||||
MyRectangle screen = {0, 0, main_surface->w, main_surface->h};
|
||||
MyRectangle result;
|
||||
static SDL_Rect get_updatearea() {
|
||||
SDL_Rect clip = {0, 0, 0, 0};
|
||||
SDL_Rect screen = {0, 0, main_surface->w, main_surface->h};
|
||||
SDL_Rect result;
|
||||
|
||||
slist_foreach(updatearea, disjunction, &clip);
|
||||
|
||||
@@ -75,7 +74,7 @@ static MyRectangle get_updatearea() {
|
||||
// updatelist に登録してあるすべてのスプライトを更新
|
||||
static void do_update_each(void* data, void* userdata) {
|
||||
sprite_t *sp = (sprite_t *)data;
|
||||
MyRectangle *r = (MyRectangle *)userdata;
|
||||
SDL_Rect *r = (SDL_Rect *)userdata;
|
||||
|
||||
// 非表示の場合はなにもしない
|
||||
if (!sp->show) return;
|
||||
@@ -92,7 +91,7 @@ static void do_update_each(void* data, void* userdata) {
|
||||
*/
|
||||
void nt_sp_update_all(bool syncscreen) {
|
||||
// 画面全体を更新領域に
|
||||
MyRectangle r = {0, 0, main_surface->w, main_surface->h };
|
||||
SDL_Rect r = {0, 0, main_surface->w, main_surface->h };
|
||||
|
||||
// updatelistに登録してあるスプライトを再描画
|
||||
// updatelistはスプライトの番号順に並んでいる
|
||||
@@ -111,7 +110,7 @@ void nt_sp_update_all(bool syncscreen) {
|
||||
updateme(_part)で登録した更新が必要なspriteの和の領域をupdate
|
||||
*/
|
||||
void nt_sp_update_clipped() {
|
||||
MyRectangle r;
|
||||
SDL_Rect r;
|
||||
|
||||
// 更新領域の確定
|
||||
r = get_updatearea();
|
||||
@@ -131,16 +130,16 @@ void nt_sp_update_clipped() {
|
||||
@param sp: 更新するスプライト
|
||||
*/
|
||||
void nt_sp_updateme(sprite_t *sp) {
|
||||
MyRectangle *r;
|
||||
SDL_Rect *r;
|
||||
|
||||
if (sp == NULL) return;
|
||||
if (sp->cursize.width == 0 || sp->cursize.height == 0) return;
|
||||
if (sp->width == 0 || sp->height == 0) return;
|
||||
|
||||
r = malloc(sizeof(MyRectangle));
|
||||
r = malloc(sizeof(SDL_Rect));
|
||||
r->x = sp->cur.x;
|
||||
r->y = sp->cur.y;
|
||||
r->w = sp->cursize.width;
|
||||
r->h = sp->cursize.height;
|
||||
r->w = sp->width;
|
||||
r->h = sp->height;
|
||||
|
||||
updatearea = slist_append(updatearea, r);
|
||||
|
||||
@@ -157,12 +156,12 @@ void nt_sp_updateme(sprite_t *sp) {
|
||||
@param h: 更新領域高さ
|
||||
*/
|
||||
void nt_sp_updateme_part(sprite_t *sp, int x, int y, int w, int h) {
|
||||
MyRectangle *r;
|
||||
SDL_Rect *r;
|
||||
|
||||
if (sp == NULL) return;
|
||||
if (w == 0 || h == 0) return;
|
||||
|
||||
r = malloc(sizeof(MyRectangle));
|
||||
r = malloc(sizeof(SDL_Rect));
|
||||
r->x = sp->cur.x + x;
|
||||
r->y = sp->cur.y + y;
|
||||
r->w = w;
|
||||
@@ -204,6 +203,6 @@ void nt_sp_clear_updatelist(void) {
|
||||
}
|
||||
|
||||
// デフォルトの壁紙update
|
||||
void nt_sp_draw_wall(sprite_t *sp, MyRectangle *area) {
|
||||
SDL_FillRect(main_surface, area, SDL_MapRGB(main_surface->format, 0, 0, 0));
|
||||
void nt_sp_draw_wall(sprite_t *sp, SDL_Rect *area) {
|
||||
SDL_FillRect(main_surface, area, sdl_map_rgb(main_surface, 0, 0, 0));
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
#include "nact.h"
|
||||
#include "alk.h"
|
||||
#include "music.h"
|
||||
#include "sdl_core.h"
|
||||
#include "gfx.h"
|
||||
#include "input.h"
|
||||
#include "cg.h"
|
||||
#include "jpeg.h"
|
||||
@@ -52,29 +52,30 @@ static void ndd_run(int demonum) {
|
||||
if (mus)
|
||||
musbgm_play(mus, 0, 100, 0);
|
||||
|
||||
uint32_t start = sdl_getTicks();
|
||||
uint32_t start = sys_get_ticks();
|
||||
while (!nact->is_quit) {
|
||||
int i = (sdl_getTicks() - start) / (1000 / FPS);
|
||||
int i = (sys_get_ticks() - start) / (1000 / FPS);
|
||||
if (i >= alk->datanum)
|
||||
break;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
int wait_ms = (i + 1) * (1000 / FPS) - (sdl_getTicks() - start);
|
||||
int wait_ms = (i + 1) * (1000 / FPS) - (sys_get_ticks() - start);
|
||||
if (wait_ms > 0) {
|
||||
if (sys_keywait(wait_ms, KEYWAIT_CANCELABLE))
|
||||
break;
|
||||
} else {
|
||||
sdl_updateScreen();
|
||||
gfx_updateScreen();
|
||||
if (sys_getInputInfo())
|
||||
break;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+78
-68
@@ -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);
|
||||
|
||||
@@ -1613,10 +1622,10 @@ static void TimerWait() {
|
||||
int wTimerID = getCaliValue();
|
||||
int wCount = getCaliValue();
|
||||
|
||||
while (wCount > stimer_get(wTimerID) && !nact->is_quit) {
|
||||
sys_keywait(10, KEYWAIT_NONCANCELABLE);
|
||||
}
|
||||
|
||||
int msec = (wCount - stimer_get(wTimerID)) * 10;
|
||||
if (msec > 0)
|
||||
sys_keywait(msec, KEYWAIT_CTRL_CANCELABLE | KEYWAIT_SKIPPABLE);
|
||||
|
||||
TRACE("SACT.TimerWait %d,%d:", wTimerID, wCount);
|
||||
}
|
||||
|
||||
@@ -1627,9 +1636,9 @@ static void TimerWait() {
|
||||
*/
|
||||
static void Wait() {
|
||||
int wCount = getCaliValue();
|
||||
|
||||
sys_keywait(wCount*10, KEYWAIT_NONCANCELABLE);
|
||||
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
||||
+14
-40
@@ -25,20 +25,16 @@
|
||||
#define __SACT_H__
|
||||
|
||||
#include "config.h"
|
||||
|
||||
#include "sdl_compat.h"
|
||||
#include "portab.h"
|
||||
#include "list.h"
|
||||
#include "graphics.h"
|
||||
#include "ags.h"
|
||||
#include "sacttimer.h"
|
||||
#include "variable.h"
|
||||
#include "sactcg.h"
|
||||
|
||||
// スプライトの最大数
|
||||
#define SPRITEMAX 21845
|
||||
|
||||
// CGの最大数
|
||||
#define CGMAX 63336
|
||||
|
||||
// メッセージの最大長さ
|
||||
#define MSGBUFMAX 257*10
|
||||
|
||||
@@ -60,24 +56,6 @@ typedef struct {
|
||||
char *dst; // 置き換え文字列
|
||||
} strexchange_t;
|
||||
|
||||
// CG_XX で作るCGの種類
|
||||
enum cgtype {
|
||||
CG_NOTUSED = 0,
|
||||
CG_LINKED = 1,
|
||||
CG_SET = 2,
|
||||
CG_REVERSE = 3,
|
||||
CG_STRETCH = 4
|
||||
};
|
||||
|
||||
// cgに関する情報
|
||||
struct _cginfo {
|
||||
enum cgtype type; // CGの種類, 0: 未使用, 1:リンクされている, ...
|
||||
int no; // CGの番号
|
||||
struct SDL_Surface *sf;
|
||||
int refcnt; // 参照カウンタ。0になったら開放してもよい。
|
||||
};
|
||||
typedef struct _cginfo cginfo_t;
|
||||
|
||||
// スプライトのタイプ
|
||||
enum spritetype {
|
||||
SPRITE_NORMAL = 0,
|
||||
@@ -107,7 +85,8 @@ struct _sprite {
|
||||
int numsound1, numsound2, numsound3;
|
||||
|
||||
// 初期 sprite の大きさ(cg1の大きさ)
|
||||
MyDimension cursize;
|
||||
int width;
|
||||
int height;
|
||||
|
||||
// それぞれの状態で表示する CG
|
||||
cginfo_t *cg1, *cg2, *cg3;
|
||||
@@ -126,10 +105,10 @@ struct _sprite {
|
||||
int freezed_state;
|
||||
|
||||
// 表示位置 (SetPos)
|
||||
MyPoint loc;
|
||||
SDL_Point loc;
|
||||
|
||||
// 現在のスプライトの表示位置
|
||||
MyPoint cur;
|
||||
SDL_Point cur;
|
||||
|
||||
// event callback
|
||||
int (* eventcb)(struct _sprite *sp, agsevent_t *e); // for key/mouse
|
||||
@@ -146,7 +125,7 @@ struct _sprite {
|
||||
|
||||
// move command 用パラメータ
|
||||
struct {
|
||||
MyPoint to; // 移動先
|
||||
SDL_Point to; // 移動先
|
||||
int time; // 移動完了時間
|
||||
int speed; // 移動速度
|
||||
int starttime; // 移動開始時刻
|
||||
@@ -157,7 +136,7 @@ struct _sprite {
|
||||
// SACT.Numeral用パラメータ
|
||||
struct {
|
||||
int cg[10];
|
||||
MyPoint pos;
|
||||
SDL_Point pos;
|
||||
int span;
|
||||
} numeral;
|
||||
|
||||
@@ -171,7 +150,7 @@ struct _sprite {
|
||||
// ゲットスプライト
|
||||
struct {
|
||||
bool dragging; // ドラッグ中
|
||||
MyPoint dragstart; // ドラッグ開始位置
|
||||
SDL_Point dragstart; // ドラッグ開始位置
|
||||
} get;
|
||||
|
||||
// プットスプライト
|
||||
@@ -191,7 +170,7 @@ struct _sprite {
|
||||
struct {
|
||||
SList *buf; // 表示する文字のリスト
|
||||
struct SDL_Surface *canvas;
|
||||
MyPoint dspcur; // 現在の表示位置
|
||||
SDL_Point dspcur; // 現在の表示位置
|
||||
} msg;
|
||||
} u;
|
||||
};
|
||||
@@ -209,11 +188,9 @@ struct _sact {
|
||||
SList *sp_quake; // Quakeで揺らすスプライトのリスト
|
||||
|
||||
SList *updatelist; // 再描画するスプライトのリスト
|
||||
|
||||
cginfo_t *cg[CGMAX]; // cgまたはCG_xxで作った CG
|
||||
|
||||
|
||||
// 座標系の原点
|
||||
MyPoint origin;
|
||||
SDL_Point origin;
|
||||
|
||||
// 文字列 replce 用
|
||||
SList *strreplace;
|
||||
@@ -247,10 +224,7 @@ struct _sact {
|
||||
int movestarttime; // 一斉に移動を開始するための開始時間
|
||||
int movecurtime;
|
||||
|
||||
MyRectangle updaterect; // 更新が必要なspriteの領域の和
|
||||
|
||||
// sact timer
|
||||
stimer_t timer[65536];
|
||||
SDL_Rect updaterect; // 更新が必要なspriteの領域の和
|
||||
|
||||
// DnDに関するもの
|
||||
sprite_t *draggedsp; // drag中のスプライト
|
||||
@@ -281,7 +255,7 @@ struct _sact {
|
||||
|
||||
// バックログ
|
||||
bool logging;
|
||||
List *log;
|
||||
List *log; // UTF-8 strings
|
||||
};
|
||||
typedef struct _sact sact_t;
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <SDL.h>
|
||||
#include "sdl_compat.h"
|
||||
|
||||
#include "portab.h"
|
||||
#include "system.h"
|
||||
@@ -34,7 +34,7 @@
|
||||
#include "sact.h"
|
||||
#include "pms.h"
|
||||
#include "sprite.h"
|
||||
#include "sdl_core.h"
|
||||
#include "effect.h"
|
||||
#include "mmap.h"
|
||||
|
||||
// SACTEFAM を使ったマスク
|
||||
@@ -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);
|
||||
sp_update_all(false); // old = sdl_texture, new = main_surface
|
||||
struct sdl_effect *eff = sdl_effect_sactamask_init(mask_sf);
|
||||
ags_runEffect(time * 10, cancel, (ags_EffectStepFunc)sdl_effect_step, eff);
|
||||
sdl_effect_finish(eff);
|
||||
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);
|
||||
|
||||
+26
-27
@@ -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"
|
||||
@@ -36,12 +36,13 @@
|
||||
#include "nact.h"
|
||||
#include "sact.h"
|
||||
#include "sprite.h"
|
||||
#include "drawtext.h"
|
||||
#include "utfsjis.h"
|
||||
#include "gfx.h"
|
||||
#include "ags.h"
|
||||
#include "font.h"
|
||||
|
||||
#define LOGMSG_LINES 6
|
||||
|
||||
static const char *logmsg_utf8[LOGMSG_LINES] = {
|
||||
static char *logmsg[LOGMSG_LINES] = {
|
||||
"\n",
|
||||
"※バックログ操作方法※",
|
||||
"[ESC]またはマウス右クリックでゲームに戻る",
|
||||
@@ -56,7 +57,7 @@ static const char *logmsg_utf8[LOGMSG_LINES] = {
|
||||
PageUP/Downで1ページ送り
|
||||
*/
|
||||
|
||||
#define FONTSIZEINDEX 10
|
||||
#define INDEX_FONTSIZE 10
|
||||
#define FONTSIZE 20
|
||||
#define LOGLINENUM (main_surface->h / FONTSIZE)
|
||||
static int curline;
|
||||
@@ -68,8 +69,13 @@ static void draw_log() {
|
||||
char pinfo[256];
|
||||
List *node;
|
||||
|
||||
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));
|
||||
#ifdef __EMSCRIPTEN__
|
||||
load_mincho_font();
|
||||
#endif
|
||||
|
||||
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);
|
||||
|
||||
@@ -80,8 +86,8 @@ static void draw_log() {
|
||||
// ページ位置情報
|
||||
len = snprintf(pinfo, sizeof(pinfo) -1, "%d/%d", curline, list_length(sact.log));
|
||||
|
||||
dt_setfont(FONT_GOTHIC, FONTSIZEINDEX);
|
||||
dt_drawtext_col(main_surface, main_surface->w - FONTSIZEINDEX *len /2, 0, pinfo, 255, 255, 255);
|
||||
gfx_drawString(main_surface->w - INDEX_FONTSIZE * len / 2, 0, pinfo, 255,
|
||||
(FontSpec){ .size = INDEX_FONTSIZE });
|
||||
|
||||
// 表示始め位置
|
||||
node = list_nth(sact.log, list_length(sact.log) - curline);
|
||||
@@ -92,12 +98,12 @@ 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 {
|
||||
if (cur < 6) {
|
||||
dt_setfont(FONT_MINCHO, FONTSIZE);
|
||||
} else {
|
||||
dt_setfont(FONT_GOTHIC, FONTSIZE);
|
||||
}
|
||||
dt_drawtext_col(main_surface, 0, y, str, 255, 255, 255);
|
||||
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;
|
||||
cur--;
|
||||
@@ -113,17 +119,11 @@ bool sblog_start(void) {
|
||||
if (sact.version < 120)
|
||||
return false;
|
||||
|
||||
static char *logmsg[LOGMSG_LINES];
|
||||
if (!logmsg[0]) {
|
||||
for (int i = 0; i < LOGMSG_LINES; i++)
|
||||
logmsg[i] = fromUTF8(logmsg_utf8[i]);
|
||||
}
|
||||
|
||||
// 説明文章を追加
|
||||
// Append instruction text
|
||||
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;
|
||||
@@ -136,12 +136,11 @@ 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++) {
|
||||
node = list_last(sact.log);
|
||||
sact.log = list_remove(sact.log, node->data);
|
||||
sact.log = list_remove(sact.log, logmsg[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);
|
||||
|
||||
+20
-21
@@ -22,36 +22,35 @@
|
||||
/* $Id: sacttimer.c,v 1.1 2003/04/22 16:29:52 chikama Exp $ */
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "portab.h"
|
||||
#include "sact.h"
|
||||
#include "sacttimer.h"
|
||||
#include "system.h"
|
||||
|
||||
#define MAX_TIMER 128
|
||||
#define TICKS_PER_CENTISECOND 10
|
||||
|
||||
uint32_t ticks_base[MAX_TIMER];
|
||||
|
||||
/*
|
||||
sact timer subsystem 初期化
|
||||
*/
|
||||
void stimer_init(void) {
|
||||
stimer_reset(0, 0);
|
||||
memset(ticks_base, 0, sizeof(ticks_base));
|
||||
}
|
||||
|
||||
// 指定IDのタイマーのリセット
|
||||
void stimer_reset(int id, int val) {
|
||||
gettimeofday(&(sact.timer[id].tv_base), NULL);
|
||||
sact.timer[id].val = val;
|
||||
if (id < 0 || id >= MAX_TIMER) {
|
||||
WARNING("Invalid timer ID: %d", id);
|
||||
return;
|
||||
}
|
||||
ticks_base[id] = sys_get_ticks() - (val * TICKS_PER_CENTISECOND);
|
||||
}
|
||||
|
||||
// 指定IDのタイマーの取得
|
||||
int stimer_get(int id) {
|
||||
long sec, usec, usec2;
|
||||
struct timeval tv;
|
||||
struct timeval tv_base = sact.timer[id].tv_base;
|
||||
int division = 10;
|
||||
|
||||
gettimeofday(&tv, NULL);
|
||||
sec = tv.tv_sec - tv_base.tv_sec;
|
||||
usec = tv.tv_usec - tv_base.tv_usec;
|
||||
usec2 = sec * (1000l/division)+ usec / 1000l /division;
|
||||
return sact.timer[id].val + usec2;
|
||||
if (id < 0 || id >= MAX_TIMER) {
|
||||
WARNING("Invalid timer ID: %d", id);
|
||||
return 0;
|
||||
}
|
||||
uint32_t ticks = sys_get_ticks();
|
||||
uint32_t diff = ticks - ticks_base[id];
|
||||
return diff / TICKS_PER_CENTISECOND;
|
||||
}
|
||||
|
||||
@@ -24,16 +24,6 @@
|
||||
#ifndef __SACTTIMER_H__
|
||||
#define __SACTTIMER_H__
|
||||
|
||||
#include <sys/time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
struct _stimer {
|
||||
int val;
|
||||
struct timeval tv_base;
|
||||
};
|
||||
typedef struct _stimer stimer_t;
|
||||
|
||||
|
||||
void stimer_init(void);
|
||||
void stimer_reset(int id, int val);
|
||||
int stimer_get(int id);
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
#include "input.h"
|
||||
#include "sact.h"
|
||||
#include "sprite.h"
|
||||
#include "sdl_core.h"
|
||||
#include "randMT.h"
|
||||
|
||||
#define M_PIf ((float)M_PI)
|
||||
@@ -78,9 +77,9 @@ void sp_quake_screen(int type, int p1, int p2, int time, int cancel) {
|
||||
|
||||
if (type > 1) return;
|
||||
|
||||
sttime = sdl_getTicks();
|
||||
sttime = sys_get_ticks();
|
||||
edtime = time * 10 + sttime;
|
||||
while ((curtime = sdl_getTicks()) < edtime) {
|
||||
while ((curtime = sys_get_ticks()) < edtime) {
|
||||
int adjx, adjy;
|
||||
|
||||
cb[type]((float)(curtime - sttime)/(edtime - sttime), p1, p2, &adjx, &adjy);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user