mirror of https://git.h3cjp.net/H3cJP/citra.git
Compare commits
228 Commits
Author | SHA1 | Date |
---|---|---|
PabloMK7 | b5126f979c | |
PabloMK7 | a8e601ae7d | |
PabloMK7 | d063f26efc | |
PabloMK7 | b1e5485058 | |
Reg Tiangha | de65b15dde | |
Malachi | e26ceabfd1 | |
GPUCode | 9dfe3eb4bc | |
GPUCode | f5cf180cee | |
GPUCode | db3da09d85 | |
GPUCode | 5691cf7e91 | |
PabloMK7 | 0c2f076dc4 | |
PabloMK7 | 775ceac27d | |
Reg Tiangha | 7fc382479d | |
Miguel | 8e35df1a6c | |
GPUCode | a442389a60 | |
Reg Tiangha | 12b30be45e | |
Reg Tiangha | b90569700c | |
Reg Tiangha | 0500b993e0 | |
Reg Tiangha | 69eba0d295 | |
GPUCode | 8433057909 | |
PabloMK7 | c4c90f0190 | |
PabloMK7 | 813d0c2a30 | |
PabloMK7 | f112d975b9 | |
PabloMK7 | bf0127d0ae | |
PabloMK7 | c710c0009f | |
PabloMK7 | f5316532e6 | |
PabloMK7 | cb75e1d637 | |
PabloMK7 | 06b3bed49a | |
Tobias | 0ff3440232 | |
liushuyu | 69e758d738 | |
Steveice10 | f4768cd26c | |
Théo B | e0d2c1308e | |
Steveice10 | 4f9fc88bb3 | |
GPUCode | d857743075 | |
kylon | b5042a5257 | |
Wunk | e524542a40 | |
Steveice10 | 3a4ebb1413 | |
Steveice10 | cbe8987036 | |
Charles Lombardo | da5aa70fc9 | |
Castor215 | 749a721aa2 | |
SachinVin | bb003c2bd4 | |
Tobias | 7638f87f74 | |
Steveice10 | aa6809e2a8 | |
Steveice10 | 5e02be75a3 | |
Tobias | b9c9beeee5 | |
GPUCode | de993dcfbd | |
oltolm | 3c9157b1ec | |
Ishan09811 | 0c40c10022 | |
Daniel López Guimaraes | 2766118e33 | |
Steveice10 | 06b26691ba | |
PabloMK7 | d41ce64f7b | |
Tobias | 1165a708d5 | |
Steveice10 | 19784355f9 | |
SachinVin | aa6a29d7e1 | |
GPUCode | 106364e01e | |
GPUCode | d5a1bd07f3 | |
Steveice10 | 8afa27718c | |
zhaobot | 8e2415f455 | |
Steveice10 | c978c074db | |
Steveice10 | cb92ec278e | |
Steveice10 | 9f5d5c6ddd | |
GPUCode | 480604ec72 | |
merry | 63feac6bb3 | |
Steveice10 | 469f76b075 | |
SachinVin | 7a4854c519 | |
Steveice10 | d1e3dddf6a | |
Charles Lombardo | 265e8193b9 | |
Amanda Watson | e8c20fa782 | |
PabloMK7 | 95ae46f6a8 | |
Steveice10 | 41fe75acb7 | |
Tobias | 1744537d85 | |
GPUCode | bea863efff | |
Daniel López Guimaraes | 89e13a85a7 | |
GPUCode | 549fdd0736 | |
GPUCode | eddc4a029c | |
Steveice10 | 82294425e3 | |
Charles Lombardo | 77fce3cf82 | |
GPUCode | 8d82adb3d3 | |
SachinVin | 228f26d1e4 | |
GPUCode | 789654d7da | |
GPUCode | ca3b2306d5 | |
GPUCode | 8e87bd606c | |
Steveice10 | f26044bb88 | |
Daniel López Guimaraes | c59ef7d793 | |
PabloMK7 | 6a7841d4b0 | |
Steveice10 | a2d1c4a94c | |
SachinVin | 9c84721d84 | |
Steveice10 | cca8c08a9a | |
PabloMK7 | 72c1075402 | |
Steveice10 | 30c53c9509 | |
xperia64 | da9f382d2c | |
PabloMK7 | a177769c3b | |
James Forward | f346949989 | |
Steveice10 | 37f0a7484f | |
PabloMK7 | 19d5695aa3 | |
Steveice10 | 6cbdc73f53 | |
Steveice10 | 81ee7ad893 | |
Steveice10 | 2ce0a9e899 | |
Steveice10 | 015e42be05 | |
Steveice10 | 57696b2c11 | |
Vitor K | c8c2beaeff | |
Steveice10 | 6069fac76d | |
Steveice10 | 7bacb78ce3 | |
Steveice10 | 0165012ba4 | |
Steveice10 | 96aa1b3a08 | |
Steveice10 | b2c740ee0e | |
Steveice10 | bc352e8168 | |
Steveice10 | 4f00eb20db | |
Steveice10 | 8b6a9b0dd8 | |
GPUCode | 62409f8139 | |
Steveice10 | 0df72f3873 | |
Steveice10 | f2ee9baec7 | |
Steveice10 | 8e2037b3ff | |
Steveice10 | c6bcbc02de | |
Steveice10 | 36db566428 | |
SachinVin | 9b147d3f9c | |
Steveice10 | 7dd9174d31 | |
GPUCode | 5a7f615da1 | |
Steveice10 | 811303ea54 | |
Steveice10 | 5bcdcffd96 | |
GPUCode | 2bb7f89c30 | |
Steveice10 | 602f4f60d8 | |
Steveice10 | 3113ae6616 | |
Steveice10 | bd4ec251cd | |
Daniel López Guimaraes | b6b98af105 | |
James Forward | 60a280af24 | |
Steveice10 | 178e602589 | |
Daniel López Guimaraes | dccb8f6b17 | |
Steveice10 | f177433d41 | |
Charles Lombardo | 71b88c4c1f | |
Tobias | c7e9f8449e | |
Steveice10 | 2e369c03b8 | |
PabloMK7 | a47d8a7b4d | |
CasualPokePlayer | 02ba5c652b | |
Charles Lombardo | 762ddfd07b | |
PabloMK7 | d680b79725 | |
GPUCode | 2b20082581 | |
Tobias | 15ea0c6336 | |
Steveice10 | 9a6d15ab74 | |
Steveice10 | 60584e861d | |
Steveice10 | 070853b465 | |
Steveice10 | 24b5ffbfca | |
Wunk | 4d9eedd0d8 | |
GPUCode | 59df319f48 | |
Steveice10 | 875f5eaad5 | |
Wunk | ea9f522c0c | |
zhaobot | 55e0b02863 | |
Charles Lombardo | 59beeac4c7 | |
Steveice10 | 0ed909e782 | |
Steveice10 | 9da78f6126 | |
Steveice10 | 0842ee6d7b | |
GPUCode | 6ec079ede8 | |
Wunk | 83b329f6e1 | |
GPUCode | db7b929e47 | |
Steveice10 | dc8425a986 | |
Steveice10 | 670e9936a4 | |
Steveice10 | c0ecdb689d | |
Wunk | 68e6a2185d | |
Steveice10 | 09b36c589b | |
GPUCode | 1dc0fa7bb5 | |
GPUCode | 85bd1be852 | |
Charles Lombardo | c17ec1d1aa | |
Steveice10 | 33a1f27a99 | |
GPUCode | 5733c8681e | |
PabloMK7 | f8ae41dfe3 | |
shinra-electric | 52254537b7 | |
SuperSamus | 98f17f8f04 | |
Steveice10 | ca6dae1744 | |
PabloMK7 | b6acebcb11 | |
Castor215 | ba702043f0 | |
SuperSamus | 2a4c60c1dd | |
Vitor K | a1532f813b | |
GPUCode | 26d5727b19 | |
PabloMK7 | 680e132318 | |
Wunk | 90a5d989e7 | |
PabloMK7 | de40153fa4 | |
PabloMK7 | e9936e01c2 | |
GPUCode | e28c2a390c | |
PabloMK7 | 63d1830429 | |
Steveice10 | 88cc6acb4d | |
PabloMK7 | 3b31720c4d | |
Steveice10 | f9bbae81aa | |
PabloMK7 | 1c793deece | |
GPUCode | d5b50a9fc0 | |
GPUCode | 168f168c33 | |
Wunk | 312068eebf | |
Steveice10 | 5118798c30 | |
Wunk | 831c9c4a38 | |
Steveice10 | 23ca10472a | |
Castor215 | 6f05dd9d1d | |
Steveice10 | 19cc8e626b | |
SachinVin | ceeda05798 | |
Vitor K | 222b1cc0d7 | |
Castor215 | b74c91457e | |
Steveice10 | 1c75d895fc | |
merry | 271218b733 | |
merry | 80213bf88f | |
Charles Lombardo | fa08df21a5 | |
Castor215 | 80ac6c03b5 | |
Steveice10 | d4f31bc617 | |
Steveice10 | 13d02c14e0 | |
Steveice10 | 84f9e9a10f | |
Castor215 | fcc0fd671a | |
Wunk | ee372572a6 | |
GPUCode | 7930e1ea86 | |
Steveice10 | 4dd6e12e46 | |
Wunk | 1d4d421097 | |
JosJuice | 3f4b57635e | |
Steveice10 | 86566f1c14 | |
GPUCode | 3f1f0aa7c2 | |
Wunk | 8fe147b8f9 | |
Steveice10 | 5193a5d222 | |
GPUCode | 1f6393e7d5 | |
Steveice10 | 9b2a5926a6 | |
Wunk | e13735b624 | |
Wunk | 3218af38d0 | |
Wunk | 1cf64ffaef | |
GPUCode | 998b9a9525 | |
Steveice10 | 27bad3a699 | |
Tobias | 1570aeffcb | |
Steveice10 | 09ee80f590 | |
Wunk | b10f3d96f5 | |
Steveice10 | b5d744bcae | |
Castor215 | 89d5d4a2b6 | |
PabloMK7 | 4284893044 | |
Steveice10 | 79ea06b226 | |
Castor215 | 8d811913a5 | |
Wunk | ac9d72a95c |
14
.ci/linux.sh
14
.ci/linux.sh
|
@ -1,13 +1,19 @@
|
|||
#!/bin/sh -ex
|
||||
#!/bin/bash -ex
|
||||
|
||||
if [ "$TARGET" = "appimage" ]; then
|
||||
# Compile the AppImage we distribute with Clang.
|
||||
export EXTRA_CMAKE_FLAGS=(-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DCMAKE_LINKER=/etc/bin/ld.lld)
|
||||
else
|
||||
# For the linux-fresh verification target, verify compilation without PCH as well.
|
||||
export EXTRA_CMAKE_FLAGS=(-DCITRA_USE_PRECOMPILED_HEADERS=OFF)
|
||||
fi
|
||||
|
||||
mkdir build && cd build
|
||||
cmake .. -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||
-DCMAKE_CXX_COMPILER=clang++ \
|
||||
-DCMAKE_C_COMPILER=clang \
|
||||
-DCMAKE_LINKER=/etc/bin/ld.lld \
|
||||
"${EXTRA_CMAKE_FLAGS[@]}" \
|
||||
-DENABLE_QT_TRANSLATION=ON \
|
||||
-DCITRA_ENABLE_COMPATIBILITY_REPORTING=ON \
|
||||
-DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON \
|
||||
|
|
16
.ci/pack.sh
16
.ci/pack.sh
|
@ -61,12 +61,20 @@ function pack_artifacts() {
|
|||
fi
|
||||
}
|
||||
|
||||
if [ -z "$PACK_INDIVIDUALLY" ]; then
|
||||
# Pack all of the artifacts at once.
|
||||
pack_artifacts build/bundle
|
||||
else
|
||||
if [ -n "$UNPACKED" ]; then
|
||||
# Copy the artifacts to be uploaded unpacked.
|
||||
for ARTIFACT in build/bundle/*; do
|
||||
FILENAME=$(basename "$ARTIFACT")
|
||||
EXTENSION="${FILENAME##*.}"
|
||||
|
||||
mv "$ARTIFACT" "artifacts/$REV_NAME.$EXTENSION"
|
||||
done
|
||||
elif [ -n "$PACK_INDIVIDUALLY" ]; then
|
||||
# Pack and upload the artifacts one-by-one.
|
||||
for ARTIFACT in build/bundle/*; do
|
||||
pack_artifacts "$ARTIFACT"
|
||||
done
|
||||
else
|
||||
# Pack all of the artifacts into a single archive.
|
||||
pack_artifacts build/bundle
|
||||
fi
|
||||
|
|
|
@ -9,7 +9,7 @@ COMPAT_LIST='dist/compatibility_list/compatibility_list.json'
|
|||
mkdir artifacts
|
||||
|
||||
pip3 install git-archive-all
|
||||
wget -q https://api.citra-emu.org/gamedb -O "${COMPAT_LIST}"
|
||||
touch "${COMPAT_LIST}"
|
||||
git describe --abbrev=0 --always HEAD > GIT-COMMIT
|
||||
git describe --tags HEAD > GIT-TAG || echo 'unknown' > GIT-TAG
|
||||
git archive-all --include "${COMPAT_LIST}" --include GIT-COMMIT --include GIT-TAG --force-submodules artifacts/"${REV_NAME}.tar"
|
||||
|
|
|
@ -12,13 +12,13 @@ jobs:
|
|||
if: ${{ !github.head_ref }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Pack
|
||||
run: ./.ci/source.sh
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: source
|
||||
path: artifacts/
|
||||
|
@ -28,20 +28,20 @@ jobs:
|
|||
matrix:
|
||||
target: ["appimage", "fresh"]
|
||||
container:
|
||||
image: citraemu/build-environments:linux-${{ matrix.target }}
|
||||
image: pablomk7/build-environments:linux-${{ matrix.target }}
|
||||
options: -u 1001
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_COMPILERCHECK: content
|
||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
||||
CCACHE_SLOPPINESS: time_macros
|
||||
OS: linux
|
||||
TARGET: ${{ matrix.target }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.CCACHE_DIR }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
|
||||
|
@ -53,60 +53,60 @@ jobs:
|
|||
run: ./.ci/pack.sh
|
||||
if: ${{ matrix.target == 'appimage' }}
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ matrix.target == 'appimage' }}
|
||||
with:
|
||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
path: artifacts/
|
||||
macos:
|
||||
runs-on: macos-latest
|
||||
runs-on: ${{ (matrix.target == 'x86_64' && 'macos-13') || 'macos-14' }}
|
||||
strategy:
|
||||
matrix:
|
||||
target: ["x86_64", "arm64"]
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_COMPILERCHECK: content
|
||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
||||
CCACHE_SLOPPINESS: time_macros
|
||||
OS: macos
|
||||
TARGET: ${{ matrix.target }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.CCACHE_DIR }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ matrix.target }}-
|
||||
- name: Install tools
|
||||
run: brew install ccache glslang ninja
|
||||
run: brew install ccache ninja
|
||||
- name: Build
|
||||
run: ./.ci/macos.sh
|
||||
- name: Prepare outputs for caching
|
||||
run: mv build/bundle $OS-$TARGET
|
||||
- name: Cache outputs for universal build
|
||||
uses: actions/cache/save@v3
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: ${{ env.OS }}-${{ env.TARGET }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
macos-universal:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-14
|
||||
needs: macos
|
||||
env:
|
||||
OS: macos
|
||||
TARGET: universal
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Download x86_64 build from cache
|
||||
uses: actions/cache/restore@v3
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: ${{ env.OS }}-x86_64
|
||||
key: ${{ runner.os }}-x86_64-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
fail-on-cache-miss: true
|
||||
- name: Download ARM64 build from cache
|
||||
uses: actions/cache/restore@v3
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: ${{ env.OS }}-arm64
|
||||
key: ${{ runner.os }}-arm64-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||
|
@ -118,7 +118,7 @@ jobs:
|
|||
- name: Pack
|
||||
run: ./.ci/pack.sh
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
path: artifacts/
|
||||
|
@ -133,15 +133,15 @@ jobs:
|
|||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_COMPILERCHECK: content
|
||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
||||
CCACHE_SLOPPINESS: time_macros
|
||||
OS: windows
|
||||
TARGET: ${{ matrix.target }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.CCACHE_DIR }}
|
||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
|
||||
|
@ -153,13 +153,6 @@ jobs:
|
|||
- name: Install extra tools (MSVC)
|
||||
run: choco install ccache ninja wget
|
||||
if: ${{ matrix.target == 'msvc' }}
|
||||
- name: Set up Vulkan SDK (MSVC)
|
||||
uses: humbletim/setup-vulkan-sdk@v1.2.0
|
||||
if: ${{ matrix.target == 'msvc' }}
|
||||
with:
|
||||
vulkan-query-version: latest
|
||||
vulkan-components: SPIRV-Tools, Glslang
|
||||
vulkan-use-cache: true
|
||||
- name: Set up MSYS2
|
||||
uses: msys2/setup-msys2@v2
|
||||
if: ${{ matrix.target == 'msys2' }}
|
||||
|
@ -168,10 +161,8 @@ jobs:
|
|||
update: true
|
||||
install: git make p7zip
|
||||
pacboy: >-
|
||||
toolchain:p ccache:p cmake:p ninja:p glslang:p
|
||||
toolchain:p ccache:p cmake:p ninja:p
|
||||
qt6-base:p qt6-multimedia:p qt6-multimedia-wmf:p qt6-tools:p qt6-translations:p
|
||||
- name: Test glslang
|
||||
run: glslang --version || glslangValidator --version
|
||||
- name: Disable line ending translation
|
||||
run: git config --global core.autocrlf input
|
||||
- name: Build
|
||||
|
@ -179,7 +170,7 @@ jobs:
|
|||
- name: Pack
|
||||
run: ./.ci/pack.sh
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
path: artifacts/
|
||||
|
@ -188,15 +179,15 @@ jobs:
|
|||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_COMPILERCHECK: content
|
||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
||||
CCACHE_SLOPPINESS: time_macros
|
||||
OS: android
|
||||
TARGET: universal
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
|
@ -215,7 +206,7 @@ jobs:
|
|||
run: |
|
||||
sudo add-apt-repository -y ppa:theofficialgman/gpu-tools
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install ccache glslang-dev glslang-tools apksigner -y
|
||||
sudo apt-get install ccache apksigner -y
|
||||
- name: Build
|
||||
run: JAVA_HOME=$JAVA_HOME_17_X64 ./.ci/android.sh
|
||||
env:
|
||||
|
@ -226,35 +217,34 @@ jobs:
|
|||
run: ../../../.ci/pack.sh
|
||||
working-directory: src/android/app
|
||||
env:
|
||||
PACK_INDIVIDUALLY: 1
|
||||
SKIP_7Z: 1
|
||||
UNPACKED: 1
|
||||
- name: Upload
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||
path: src/android/app/artifacts/
|
||||
ios:
|
||||
runs-on: macos-latest
|
||||
runs-on: macos-14
|
||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||
env:
|
||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||
CCACHE_COMPILERCHECK: content
|
||||
CCACHE_SLOPPINESS: pch_defines,time_macros
|
||||
CCACHE_SLOPPINESS: time_macros
|
||||
OS: ios
|
||||
TARGET: arm64
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Set up cache
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ env.CCACHE_DIR }}
|
||||
key: ${{ runner.os }}-ios-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-ios-
|
||||
- name: Install tools
|
||||
run: brew install ccache glslang ninja
|
||||
run: brew install ccache ninja
|
||||
- name: Build
|
||||
run: ./.ci/ios.sh
|
||||
release:
|
||||
|
@ -262,7 +252,7 @@ jobs:
|
|||
needs: [windows, linux, macos-universal, android, source]
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||
steps:
|
||||
- uses: actions/download-artifact@v3
|
||||
- uses: actions/download-artifact@v4
|
||||
- name: Create release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
|
|
|
@ -200,7 +200,7 @@ async function mergebot(github, context, execa) {
|
|||
}
|
||||
console.info("The following pull requests will be merged:");
|
||||
console.table(displayList);
|
||||
await fetchPullRequests(pulls, "https://github.com/citra-emu/citra", execa);
|
||||
await fetchPullRequests(pulls, "https://github.com/PabloMK7/citra", execa);
|
||||
const mergeResults = await mergePullRequests(pulls, execa);
|
||||
await generateReadme(pulls, context, mergeResults, execa);
|
||||
await tagAndPush(github, context.repo.owner, `${context.repo.repo}-canary`, execa, true);
|
||||
|
|
|
@ -10,10 +10,10 @@ jobs:
|
|||
clang-format:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: citraemu/build-environments:linux-fresh
|
||||
image: pablomk7/build-environments:linux-fresh
|
||||
options: -u 1001
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Build
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
name: citra-publish
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '7 0 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
nightly:
|
||||
description: 'Whether to trigger a nightly build (true/false/auto)'
|
||||
required: false
|
||||
default: 'true'
|
||||
canary:
|
||||
description: 'Whether to trigger a canary build (true/false/auto)'
|
||||
required: false
|
||||
default: 'true'
|
||||
|
||||
jobs:
|
||||
nightly:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.inputs.nightly != 'false' && github.repository == 'citra-emu/citra' }}
|
||||
steps:
|
||||
# this checkout is required to make sure the GitHub Actions scripts are available
|
||||
- uses: actions/checkout@v3
|
||||
name: Pre-checkout
|
||||
with:
|
||||
submodules: false
|
||||
- uses: actions/github-script@v6
|
||||
id: check-changes
|
||||
name: 'Check for new changes'
|
||||
env:
|
||||
# 24 hours
|
||||
DETECTION_TIME_FRAME: 86400000
|
||||
with:
|
||||
result-encoding: string
|
||||
script: |
|
||||
if (context.payload.inputs && context.payload.inputs.nightly === 'true') return true;
|
||||
const checkBaseChanges = require('./.github/workflows/ci-merge.js').checkBaseChanges;
|
||||
return checkBaseChanges(github, context);
|
||||
- run: npm install execa@5
|
||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||
- uses: actions/checkout@v3
|
||||
name: Checkout
|
||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||
with:
|
||||
path: 'citra-merge'
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
token: ${{ secrets.ALT_GITHUB_TOKEN }}
|
||||
- uses: actions/github-script@v6
|
||||
name: 'Update and tag new commits'
|
||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||
env:
|
||||
ALT_GITHUB_TOKEN: ${{ secrets.ALT_GITHUB_TOKEN }}
|
||||
with:
|
||||
script: |
|
||||
const execa = require("execa");
|
||||
const tagAndPush = require('./.github/workflows/ci-merge.js').tagAndPush;
|
||||
process.chdir('${{ github.workspace }}/citra-merge');
|
||||
tagAndPush(github, context.repo.owner, `${context.repo.repo}-nightly`, execa);
|
||||
canary:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.inputs.canary != 'false' && github.repository == 'citra-emu/citra' }}
|
||||
steps:
|
||||
# this checkout is required to make sure the GitHub Actions scripts are available
|
||||
- uses: actions/checkout@v3
|
||||
name: Pre-checkout
|
||||
with:
|
||||
submodules: false
|
||||
- uses: actions/github-script@v6
|
||||
id: check-changes
|
||||
name: 'Check for new changes'
|
||||
env:
|
||||
# 24 hours
|
||||
DETECTION_TIME_FRAME: 86400000
|
||||
with:
|
||||
script: |
|
||||
if (context.payload.inputs && context.payload.inputs.canary === 'true') return true;
|
||||
const checkCanaryChanges = require('./.github/workflows/ci-merge.js').checkCanaryChanges;
|
||||
return checkCanaryChanges(github, context);
|
||||
- run: npm install execa@5
|
||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||
- uses: actions/checkout@v3
|
||||
name: Checkout
|
||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||
with:
|
||||
path: 'citra-merge'
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
token: ${{ secrets.ALT_GITHUB_TOKEN }}
|
||||
- uses: actions/github-script@v6
|
||||
name: 'Check and merge canary changes'
|
||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||
env:
|
||||
ALT_GITHUB_TOKEN: ${{ secrets.ALT_GITHUB_TOKEN }}
|
||||
with:
|
||||
script: |
|
||||
const execa = require("execa");
|
||||
const mergebot = require('./.github/workflows/ci-merge.js').mergebot;
|
||||
process.chdir('${{ github.workspace }}/citra-merge');
|
||||
mergebot(github, context, execa);
|
|
@ -7,10 +7,10 @@ on:
|
|||
jobs:
|
||||
transifex:
|
||||
runs-on: ubuntu-latest
|
||||
container: citraemu/build-environments:linux-fresh
|
||||
container: pablomk7/build-environments:linux-fresh
|
||||
if: ${{ github.repository == 'citra-emu/citra' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[submodule "boost"]
|
||||
path = externals/boost
|
||||
url = https://github.com/citra-emu/ext-boost.git
|
||||
url = https://github.com/PabloMK7/ext-boost.git
|
||||
[submodule "nihstro"]
|
||||
path = externals/nihstro
|
||||
url = https://github.com/neobrain/nihstro.git
|
||||
|
@ -12,7 +12,7 @@
|
|||
url = https://github.com/catchorg/Catch2
|
||||
[submodule "dynarmic"]
|
||||
path = externals/dynarmic
|
||||
url = https://github.com/merryhime/dynarmic.git
|
||||
url = https://github.com/PabloMK7/dynarmic.git
|
||||
[submodule "xbyak"]
|
||||
path = externals/xbyak
|
||||
url = https://github.com/herumi/xbyak.git
|
||||
|
@ -27,7 +27,7 @@
|
|||
url = https://github.com/benhoyt/inih.git
|
||||
[submodule "libressl"]
|
||||
path = externals/libressl
|
||||
url = https://github.com/citra-emu/ext-libressl-portable.git
|
||||
url = https://github.com/PabloMK7/ext-libressl-portable.git
|
||||
[submodule "libusb"]
|
||||
path = externals/libusb/libusb
|
||||
url = https://github.com/libusb/libusb.git
|
||||
|
@ -36,7 +36,7 @@
|
|||
url = https://github.com/mozilla/cubeb
|
||||
[submodule "discord-rpc"]
|
||||
path = externals/discord-rpc
|
||||
url = https://github.com/yuzu-emu/discord-rpc.git
|
||||
url = https://github.com/PabloMK7/discord-rpc.git
|
||||
[submodule "cpp-jwt"]
|
||||
path = externals/cpp-jwt
|
||||
url = https://github.com/arun11299/cpp-jwt.git
|
||||
|
@ -78,10 +78,16 @@
|
|||
url = https://github.com/KhronosGroup/Vulkan-Headers
|
||||
[submodule "sirit"]
|
||||
path = externals/sirit
|
||||
url = https://github.com/yuzu-emu/sirit
|
||||
url = https://github.com/PabloMK7/sirit
|
||||
[submodule "faad2"]
|
||||
path = externals/faad2/faad2
|
||||
url = https://github.com/knik0/faad2
|
||||
[submodule "library-headers"]
|
||||
path = externals/library-headers
|
||||
url = https://github.com/citra-emu/ext-library-headers.git
|
||||
url = https://github.com/PabloMK7/ext-library-headers.git
|
||||
[submodule "libadrenotools"]
|
||||
path = externals/libadrenotools
|
||||
url = https://github.com/bylaws/libadrenotools
|
||||
[submodule "oaknut"]
|
||||
path = externals/oaknut
|
||||
url = https://github.com/merryhime/oaknut.git
|
||||
|
|
|
@ -79,13 +79,12 @@ option(ENABLE_OPENAL "Enables the OpenAL audio backend" ON)
|
|||
|
||||
CMAKE_DEPENDENT_OPTION(ENABLE_LIBUSB "Enable libusb for GameCube Adapter support" ON "NOT IOS" OFF)
|
||||
|
||||
CMAKE_DEPENDENT_OPTION(ENABLE_SOFTWARE_RENDERER "Enables the software renderer" ON "NOT ANDROID" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(ENABLE_OPENGL "Enables the OpenGL renderer" ON "NOT APPLE" OFF)
|
||||
option(ENABLE_VULKAN "Enables the Vulkan renderer" ON)
|
||||
|
||||
option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
|
||||
|
||||
CMAKE_DEPENDENT_OPTION(ENABLE_MF "Use Media Foundation decoder (preferred over FFmpeg)" ON "WIN32" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(ENABLE_AUDIOTOOLBOX "Use AudioToolbox decoder (preferred over FFmpeg)" ON "APPLE" OFF)
|
||||
|
||||
CMAKE_DEPENDENT_OPTION(CITRA_ENABLE_BUNDLE_TARGET "Enable the distribution bundling target." ON "NOT ANDROID AND NOT IOS" OFF)
|
||||
|
||||
# Compile options
|
||||
CMAKE_DEPENDENT_OPTION(COMPILE_WITH_DWARF "Add DWARF debugging information" ${IS_DEBUG_BUILD} "MINGW" OFF)
|
||||
option(ENABLE_LTO "Enable link time optimization" ${DEFAULT_ENABLE_LTO})
|
||||
|
@ -236,7 +235,7 @@ find_package(Threads REQUIRED)
|
|||
|
||||
if (ENABLE_QT)
|
||||
if (NOT USE_SYSTEM_QT)
|
||||
download_qt(6.5.1)
|
||||
download_qt(6.6.0)
|
||||
endif()
|
||||
|
||||
find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia Concurrent)
|
||||
|
@ -248,6 +247,26 @@ if (ENABLE_QT)
|
|||
if (ENABLE_QT_TRANSLATION)
|
||||
find_package(Qt6 REQUIRED COMPONENTS LinguistTools)
|
||||
endif()
|
||||
|
||||
if (NOT DEFINED QT_TARGET_PATH)
|
||||
# Determine the location of the compile target's Qt.
|
||||
get_target_property(qtcore_path Qt6::Core LOCATION_Release)
|
||||
string(FIND "${qtcore_path}" "/bin/" qtcore_path_bin_pos REVERSE)
|
||||
string(FIND "${qtcore_path}" "/lib/" qtcore_path_lib_pos REVERSE)
|
||||
if (qtcore_path_bin_pos GREATER qtcore_path_lib_pos)
|
||||
string(SUBSTRING "${qtcore_path}" 0 ${qtcore_path_bin_pos} QT_TARGET_PATH)
|
||||
else()
|
||||
string(SUBSTRING "${qtcore_path}" 0 ${qtcore_path_lib_pos} QT_TARGET_PATH)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if (NOT DEFINED QT_HOST_PATH)
|
||||
# Use the same for host Qt if none is defined.
|
||||
set(QT_HOST_PATH "${QT_TARGET_PATH}")
|
||||
endif()
|
||||
|
||||
message(STATUS "Using target Qt at ${QT_TARGET_PATH}")
|
||||
message(STATUS "Using host Qt at ${QT_HOST_PATH}")
|
||||
endif()
|
||||
|
||||
# Use system tsl::robin_map if available (otherwise we fallback to version bundled with dynarmic)
|
||||
|
@ -257,20 +276,22 @@ find_package(tsl-robin-map QUIET)
|
|||
# ======================================
|
||||
|
||||
if (APPLE)
|
||||
if (NOT USE_SYSTEM_MOLTENVK)
|
||||
download_moltenvk()
|
||||
endif()
|
||||
find_library(MOLTENVK_LIBRARY MoltenVK REQUIRED)
|
||||
message(STATUS "Using MoltenVK at ${MOLTENVK_LIBRARY}.")
|
||||
|
||||
if (NOT IOS)
|
||||
# Umbrella framework for everything GUI-related
|
||||
find_library(COCOA_LIBRARY Cocoa REQUIRED)
|
||||
endif()
|
||||
|
||||
find_library(AVFOUNDATION_LIBRARY AVFoundation REQUIRED)
|
||||
find_library(IOSURFACE_LIBRARY IOSurface REQUIRED)
|
||||
set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${AVFOUNDATION_LIBRARY} ${IOSURFACE_LIBRARY} ${MOLTENVK_LIBRARY})
|
||||
|
||||
if (ENABLE_VULKAN)
|
||||
if (NOT USE_SYSTEM_MOLTENVK)
|
||||
download_moltenvk()
|
||||
endif()
|
||||
find_library(MOLTENVK_LIBRARY MoltenVK REQUIRED)
|
||||
message(STATUS "Using MoltenVK at ${MOLTENVK_LIBRARY}.")
|
||||
set(PLATFORM_LIBRARIES ${PLATFORM_LIBRARIES} ${MOLTENVK_LIBRARY})
|
||||
endif()
|
||||
elseif (WIN32)
|
||||
set(PLATFORM_LIBRARIES winmm ws2_32)
|
||||
if (MINGW)
|
||||
|
@ -421,7 +442,8 @@ else()
|
|||
endif()
|
||||
|
||||
# Create target for outputting distributable bundles.
|
||||
if (CITRA_ENABLE_BUNDLE_TARGET)
|
||||
# Not supported for mobile platforms as distributables are built differently.
|
||||
if (NOT ANDROID AND NOT IOS)
|
||||
include(BundleTarget)
|
||||
if (ENABLE_SDL2_FRONTEND)
|
||||
bundle_target(citra)
|
||||
|
|
|
@ -2,37 +2,104 @@
|
|||
if (BUNDLE_TARGET_EXECUTE)
|
||||
# --- Bundling method logic ---
|
||||
|
||||
function(symlink_safe_copy from to)
|
||||
if (WIN32)
|
||||
# Use cmake copy for maximum compatibility.
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${from}" "${to}"
|
||||
RESULT_VARIABLE cp_result)
|
||||
else()
|
||||
# Use native copy to turn symlinks into normal files.
|
||||
execute_process(COMMAND cp -L "${from}" "${to}"
|
||||
RESULT_VARIABLE cp_result)
|
||||
endif()
|
||||
if (NOT cp_result EQUAL "0")
|
||||
message(FATAL_ERROR "cp \"${from}\" \"${to}\" failed: ${cp_result}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(bundle_qt executable_path)
|
||||
if (WIN32)
|
||||
# Perform standalone bundling first to copy over all used libraries, as windeployqt does not do this.
|
||||
bundle_standalone("${executable_path}" "${EXECUTABLE_PATH}" "${BUNDLE_LIBRARY_PATHS}")
|
||||
|
||||
get_filename_component(executable_parent_dir "${executable_path}" DIRECTORY)
|
||||
find_program(windeployqt_executable windeployqt6)
|
||||
|
||||
# Create a qt.conf file pointing to the app directory.
|
||||
# This ensures Qt can find its plugins.
|
||||
file(WRITE "${executable_parent_dir}/qt.conf" "[Paths]\nprefix = .")
|
||||
file(WRITE "${executable_parent_dir}/qt.conf" "[Paths]\nPrefix = .")
|
||||
|
||||
find_program(windeployqt_executable windeployqt6 PATHS "${QT_HOST_PATH}/bin")
|
||||
find_program(qtpaths_executable qtpaths6 PATHS "${QT_HOST_PATH}/bin")
|
||||
|
||||
# TODO: Hack around windeployqt's poor cross-compilation support by
|
||||
# TODO: making a local copy with a prefix pointing to the target Qt.
|
||||
if (NOT "${QT_HOST_PATH}" STREQUAL "${QT_TARGET_PATH}")
|
||||
set(windeployqt_dir "${BINARY_PATH}/windeployqt_copy")
|
||||
file(MAKE_DIRECTORY "${windeployqt_dir}")
|
||||
symlink_safe_copy("${windeployqt_executable}" "${windeployqt_dir}/windeployqt.exe")
|
||||
symlink_safe_copy("${qtpaths_executable}" "${windeployqt_dir}/qtpaths.exe")
|
||||
symlink_safe_copy("${QT_HOST_PATH}/bin/Qt6Core.dll" "${windeployqt_dir}")
|
||||
|
||||
if (EXISTS "${QT_TARGET_PATH}/share")
|
||||
# Unix-style Qt; we need to wire up the paths manually.
|
||||
file(WRITE "${windeployqt_dir}/qt.conf" "\
|
||||
[Paths]\n
|
||||
Prefix = ${QT_TARGET_PATH}\n \
|
||||
ArchData = ${QT_TARGET_PATH}/share/qt6\n \
|
||||
Binaries = ${QT_TARGET_PATH}/bin\n \
|
||||
Data = ${QT_TARGET_PATH}/share/qt6\n \
|
||||
Documentation = ${QT_TARGET_PATH}/share/qt6/doc\n \
|
||||
Headers = ${QT_TARGET_PATH}/include/qt6\n \
|
||||
Libraries = ${QT_TARGET_PATH}/lib\n \
|
||||
LibraryExecutables = ${QT_TARGET_PATH}/share/qt6/bin\n \
|
||||
Plugins = ${QT_TARGET_PATH}/share/qt6/plugins\n \
|
||||
QmlImports = ${QT_TARGET_PATH}/share/qt6/qml\n \
|
||||
Translations = ${QT_TARGET_PATH}/share/qt6/translations\n \
|
||||
")
|
||||
else()
|
||||
# Windows-style Qt; the defaults should suffice.
|
||||
file(WRITE "${windeployqt_dir}/qt.conf" "[Paths]\nPrefix = ${QT_TARGET_PATH}")
|
||||
endif()
|
||||
|
||||
set(windeployqt_executable "${windeployqt_dir}/windeployqt.exe")
|
||||
set(qtpaths_executable "${windeployqt_dir}/qtpaths.exe")
|
||||
endif()
|
||||
|
||||
message(STATUS "Executing windeployqt for executable ${executable_path}")
|
||||
execute_process(COMMAND "${windeployqt_executable}" "${executable_path}"
|
||||
--qtpaths "${qtpaths_executable}"
|
||||
--no-compiler-runtime --no-system-d3d-compiler --no-opengl-sw --no-translations
|
||||
--plugindir "${executable_parent_dir}/plugins")
|
||||
--plugindir "${executable_parent_dir}/plugins"
|
||||
RESULT_VARIABLE windeployqt_result)
|
||||
if (NOT windeployqt_result EQUAL "0")
|
||||
message(FATAL_ERROR "windeployqt failed: ${windeployqt_result}")
|
||||
endif()
|
||||
|
||||
# Remove the FFmpeg multimedia plugin as we don't include FFmpeg.
|
||||
# We want to use the Windows media plugin instead, which is also included.
|
||||
file(REMOVE "${executable_parent_dir}/plugins/multimedia/ffmpegmediaplugin.dll")
|
||||
elseif (APPLE)
|
||||
get_filename_component(executable_name "${executable_path}" NAME_WE)
|
||||
find_program(MACDEPLOYQT_EXECUTABLE macdeployqt6)
|
||||
find_program(macdeployqt_executable macdeployqt6 PATHS "${QT_HOST_PATH}/bin")
|
||||
|
||||
message(STATUS "Executing macdeployqt for executable ${executable_path}")
|
||||
message(STATUS "Executing macdeployqt at \"${macdeployqt_executable}\" for executable \"${executable_path}\"")
|
||||
execute_process(
|
||||
COMMAND "${MACDEPLOYQT_EXECUTABLE}"
|
||||
COMMAND "${macdeployqt_executable}"
|
||||
"${executable_path}"
|
||||
"-executable=${executable_path}/Contents/MacOS/${executable_name}"
|
||||
-always-overwrite)
|
||||
-always-overwrite
|
||||
RESULT_VARIABLE macdeployqt_result)
|
||||
if (NOT macdeployqt_result EQUAL "0")
|
||||
message(FATAL_ERROR "macdeployqt failed: ${macdeployqt_result}")
|
||||
endif()
|
||||
|
||||
# Bundling libraries can rewrite path information and break code signatures of system libraries.
|
||||
# Perform an ad-hoc re-signing on the whole app bundle to fix this.
|
||||
execute_process(COMMAND codesign --deep -fs - "${executable_path}")
|
||||
execute_process(COMMAND codesign --deep -fs - "${executable_path}"
|
||||
RESULT_VARIABLE codesign_result)
|
||||
if (NOT codesign_result EQUAL "0")
|
||||
message(FATAL_ERROR "codesign failed: ${codesign_result}")
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "Unsupported OS for Qt bundling.")
|
||||
endif()
|
||||
|
@ -44,9 +111,9 @@ if (BUNDLE_TARGET_EXECUTE)
|
|||
|
||||
if (enable_qt)
|
||||
# Find qmake to make sure the plugin uses the right version of Qt.
|
||||
find_program(QMAKE_EXECUTABLE qmake6)
|
||||
find_program(qmake_executable qmake6 PATHS "${QT_HOST_PATH}/bin")
|
||||
|
||||
set(extra_linuxdeploy_env "QMAKE=${QMAKE_EXECUTABLE}")
|
||||
set(extra_linuxdeploy_env "QMAKE=${qmake_executable}")
|
||||
set(extra_linuxdeploy_args --plugin qt)
|
||||
endif()
|
||||
|
||||
|
@ -59,7 +126,11 @@ if (BUNDLE_TARGET_EXECUTE)
|
|||
--executable "${executable_path}"
|
||||
--icon-file "${source_path}/dist/citra.svg"
|
||||
--desktop-file "${source_path}/dist/${executable_name}.desktop"
|
||||
--appdir "${appdir_path}")
|
||||
--appdir "${appdir_path}"
|
||||
RESULT_VARIABLE linuxdeploy_appdir_result)
|
||||
if (NOT linuxdeploy_appdir_result EQUAL "0")
|
||||
message(FATAL_ERROR "linuxdeploy failed to create AppDir: ${linuxdeploy_appdir_result}")
|
||||
endif()
|
||||
|
||||
if (enable_qt)
|
||||
set(qt_hook_file "${appdir_path}/apprun-hooks/linuxdeploy-plugin-qt-hook.sh")
|
||||
|
@ -82,7 +153,11 @@ if (BUNDLE_TARGET_EXECUTE)
|
|||
"OUTPUT=${bundle_dir}/${executable_name}.AppImage"
|
||||
"${linuxdeploy_executable}"
|
||||
--output appimage
|
||||
--appdir "${appdir_path}")
|
||||
--appdir "${appdir_path}"
|
||||
RESULT_VARIABLE linuxdeploy_appimage_result)
|
||||
if (NOT linuxdeploy_appimage_result EQUAL "0")
|
||||
message(FATAL_ERROR "linuxdeploy failed to create AppImage: ${linuxdeploy_appimage_result}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(bundle_standalone executable_path original_executable_path bundle_library_paths)
|
||||
|
@ -109,16 +184,23 @@ if (BUNDLE_TARGET_EXECUTE)
|
|||
file(MAKE_DIRECTORY ${lib_dir})
|
||||
foreach (lib_file IN LISTS resolved_deps)
|
||||
message(STATUS "Bundling library ${lib_file}")
|
||||
# Use native copy to turn symlinks into normal files.
|
||||
execute_process(COMMAND cp -L "${lib_file}" "${lib_dir}")
|
||||
symlink_safe_copy("${lib_file}" "${lib_dir}")
|
||||
endforeach()
|
||||
endif()
|
||||
|
||||
# Add libs directory to executable rpath where applicable.
|
||||
if (APPLE)
|
||||
execute_process(COMMAND install_name_tool -add_rpath "@loader_path/libs" "${executable_path}")
|
||||
execute_process(COMMAND install_name_tool -add_rpath "@loader_path/libs" "${executable_path}"
|
||||
RESULT_VARIABLE install_name_tool_result)
|
||||
if (NOT install_name_tool_result EQUAL "0")
|
||||
message(FATAL_ERROR "install_name_tool failed: ${install_name_tool_result}")
|
||||
endif()
|
||||
elseif (UNIX)
|
||||
execute_process(COMMAND patchelf --set-rpath '$ORIGIN/../libs' "${executable_path}")
|
||||
execute_process(COMMAND patchelf --set-rpath '$ORIGIN/../libs' "${executable_path}"
|
||||
RESULT_VARIABLE patchelf_result)
|
||||
if (NOT patchelf_result EQUAL "0")
|
||||
message(FATAL_ERROR "patchelf failed: ${patchelf_result}")
|
||||
endif()
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
|
@ -127,7 +209,7 @@ if (BUNDLE_TARGET_EXECUTE)
|
|||
set(bundle_dir ${BINARY_PATH}/bundle)
|
||||
|
||||
# On Linux, always bundle an AppImage.
|
||||
if (DEFINED LINUXDEPLOY)
|
||||
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
|
||||
if (IN_PLACE)
|
||||
message(FATAL_ERROR "Cannot bundle for Linux in-place.")
|
||||
endif()
|
||||
|
@ -146,14 +228,12 @@ if (BUNDLE_TARGET_EXECUTE)
|
|||
|
||||
if (BUNDLE_QT)
|
||||
bundle_qt("${bundled_executable_path}")
|
||||
endif()
|
||||
|
||||
if (WIN32 OR NOT BUNDLE_QT)
|
||||
else()
|
||||
bundle_standalone("${bundled_executable_path}" "${EXECUTABLE_PATH}" "${BUNDLE_LIBRARY_PATHS}")
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
# --- Bundling target creation logic ---
|
||||
elseif (BUNDLE_TARGET_DOWNLOAD_LINUXDEPLOY)
|
||||
# --- linuxdeploy download logic ---
|
||||
|
||||
# Downloads and extracts a linuxdeploy component.
|
||||
function(download_linuxdeploy_component base_dir name executable_name)
|
||||
|
@ -161,7 +241,7 @@ else()
|
|||
if (NOT EXISTS "${executable_file}")
|
||||
message(STATUS "Downloading ${executable_name}")
|
||||
file(DOWNLOAD
|
||||
"https://github.com/linuxdeploy/${name}/releases/download/continuous/${executable_name}"
|
||||
"https://github.com/${name}/releases/download/continuous/${executable_name}"
|
||||
"${executable_file}" SHOW_PROGRESS)
|
||||
file(CHMOD "${executable_file}" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE)
|
||||
|
||||
|
@ -170,7 +250,11 @@ else()
|
|||
message(STATUS "Extracting ${executable_name}")
|
||||
execute_process(
|
||||
COMMAND "${executable_file}" --appimage-extract
|
||||
WORKING_DIRECTORY "${base_dir}")
|
||||
WORKING_DIRECTORY "${base_dir}"
|
||||
RESULT_VARIABLE extract_result)
|
||||
if (NOT extract_result EQUAL "0")
|
||||
message(FATAL_ERROR "AppImage extract failed: ${extract_result}")
|
||||
endif()
|
||||
else()
|
||||
message(STATUS "Copying ${executable_name}")
|
||||
file(COPY "${executable_file}" DESTINATION "${base_dir}/squashfs-root/usr/bin/")
|
||||
|
@ -178,89 +262,102 @@ else()
|
|||
endif()
|
||||
endfunction()
|
||||
|
||||
# Download plugins first so they don't overwrite linuxdeploy's AppRun file.
|
||||
download_linuxdeploy_component("${LINUXDEPLOY_PATH}" "linuxdeploy/linuxdeploy-plugin-qt" "linuxdeploy-plugin-qt-${LINUXDEPLOY_ARCH}.AppImage")
|
||||
download_linuxdeploy_component("${LINUXDEPLOY_PATH}" "darealshinji/linuxdeploy-plugin-checkrt" "linuxdeploy-plugin-checkrt.sh")
|
||||
download_linuxdeploy_component("${LINUXDEPLOY_PATH}" "linuxdeploy/linuxdeploy" "linuxdeploy-${LINUXDEPLOY_ARCH}.AppImage")
|
||||
else()
|
||||
# --- Bundling target creation logic ---
|
||||
|
||||
# Creates the base bundle target with common files and pre-bundle steps.
|
||||
function(create_base_bundle_target)
|
||||
message(STATUS "Creating base bundle target")
|
||||
|
||||
add_custom_target(bundle)
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bundle/")
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bundle/dist/")
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dist/icon.png" "${CMAKE_BINARY_DIR}/bundle/dist/citra.png")
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/license.txt" "${CMAKE_BINARY_DIR}/bundle/")
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/README.md" "${CMAKE_BINARY_DIR}/bundle/")
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/dist/scripting" "${CMAKE_BINARY_DIR}/bundle/scripting")
|
||||
|
||||
# On Linux, add a command to prepare linuxdeploy and any required plugins before any bundling occurs.
|
||||
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
"-DBUNDLE_TARGET_DOWNLOAD_LINUXDEPLOY=1"
|
||||
"-DLINUXDEPLOY_PATH=${CMAKE_BINARY_DIR}/externals/linuxdeploy"
|
||||
"-DLINUXDEPLOY_ARCH=${CMAKE_HOST_SYSTEM_PROCESSOR}"
|
||||
-P "${CMAKE_SOURCE_DIR}/CMakeModules/BundleTarget.cmake"
|
||||
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Adds a target to the bundle target, packing in required libraries.
|
||||
# If in_place is true, the bundling will be done in-place as part of the specified target.
|
||||
function(bundle_target_internal target_name in_place)
|
||||
# Create base bundle target if it does not exist.
|
||||
if (NOT in_place AND NOT TARGET bundle)
|
||||
message(STATUS "Creating base bundle target")
|
||||
|
||||
add_custom_target(bundle)
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bundle/")
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bundle/dist/")
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dist/icon.png" "${CMAKE_BINARY_DIR}/bundle/dist/citra.png")
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/license.txt" "${CMAKE_BINARY_DIR}/bundle/")
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/README.md" "${CMAKE_BINARY_DIR}/bundle/")
|
||||
add_custom_command(
|
||||
TARGET bundle
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/dist/scripting" "${CMAKE_BINARY_DIR}/bundle/scripting")
|
||||
create_base_bundle_target()
|
||||
endif()
|
||||
|
||||
set(BUNDLE_EXECUTABLE_PATH "$<TARGET_FILE:${target_name}>")
|
||||
set(bundle_executable_path "$<TARGET_FILE:${target_name}>")
|
||||
if (target_name MATCHES ".*qt")
|
||||
set(BUNDLE_QT ON)
|
||||
set(bundle_qt ON)
|
||||
if (APPLE)
|
||||
# For Qt targets on Apple, expect an app bundle.
|
||||
set(BUNDLE_EXECUTABLE_PATH "$<TARGET_BUNDLE_DIR:${target_name}>")
|
||||
set(bundle_executable_path "$<TARGET_BUNDLE_DIR:${target_name}>")
|
||||
endif()
|
||||
else()
|
||||
set(BUNDLE_QT OFF)
|
||||
set(bundle_qt OFF)
|
||||
endif()
|
||||
|
||||
# Build a list of library search paths from prefix paths.
|
||||
foreach(prefix_path IN LISTS CMAKE_PREFIX_PATH CMAKE_SYSTEM_PREFIX_PATH)
|
||||
foreach(prefix_path IN LISTS CMAKE_FIND_ROOT_PATH CMAKE_PREFIX_PATH CMAKE_SYSTEM_PREFIX_PATH)
|
||||
if (WIN32)
|
||||
list(APPEND BUNDLE_LIBRARY_PATHS "${prefix_path}/bin")
|
||||
list(APPEND bundle_library_paths "${prefix_path}/bin")
|
||||
endif()
|
||||
list(APPEND BUNDLE_LIBRARY_PATHS "${prefix_path}/lib")
|
||||
list(APPEND bundle_library_paths "${prefix_path}/lib")
|
||||
endforeach()
|
||||
foreach(library_path IN LISTS CMAKE_SYSTEM_LIBRARY_PATH)
|
||||
list(APPEND BUNDLE_LIBRARY_PATHS "${library_path}")
|
||||
list(APPEND bundle_library_paths "${library_path}")
|
||||
endforeach()
|
||||
|
||||
# On Linux, prepare linuxdeploy and any required plugins.
|
||||
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||
set(LINUXDEPLOY_BASE "${CMAKE_BINARY_DIR}/externals/linuxdeploy")
|
||||
|
||||
# Download plugins first so they don't overwrite linuxdeploy's AppRun file.
|
||||
download_linuxdeploy_component("${LINUXDEPLOY_BASE}" "linuxdeploy-plugin-qt" "linuxdeploy-plugin-qt-x86_64.AppImage")
|
||||
download_linuxdeploy_component("${LINUXDEPLOY_BASE}" "linuxdeploy-plugin-checkrt" "linuxdeploy-plugin-checkrt-x86_64.sh")
|
||||
download_linuxdeploy_component("${LINUXDEPLOY_BASE}" "linuxdeploy" "linuxdeploy-x86_64.AppImage")
|
||||
|
||||
set(EXTRA_BUNDLE_ARGS "-DLINUXDEPLOY=${LINUXDEPLOY_BASE}/squashfs-root/AppRun")
|
||||
endif()
|
||||
|
||||
if (in_place)
|
||||
message(STATUS "Adding in-place bundling to ${target_name}")
|
||||
set(DEST_TARGET ${target_name})
|
||||
set(dest_target ${target_name})
|
||||
else()
|
||||
message(STATUS "Adding ${target_name} to bundle target")
|
||||
set(DEST_TARGET bundle)
|
||||
set(dest_target bundle)
|
||||
add_dependencies(bundle ${target_name})
|
||||
endif()
|
||||
|
||||
add_custom_command(TARGET ${DEST_TARGET} POST_BUILD
|
||||
add_custom_command(TARGET ${dest_target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
"-DCMAKE_PREFIX_PATH=\"${CMAKE_PREFIX_PATH}\""
|
||||
"-DQT_HOST_PATH=\"${QT_HOST_PATH}\""
|
||||
"-DQT_TARGET_PATH=\"${QT_TARGET_PATH}\""
|
||||
"-DBUNDLE_TARGET_EXECUTE=1"
|
||||
"-DTARGET=${target_name}"
|
||||
"-DSOURCE_PATH=${CMAKE_SOURCE_DIR}"
|
||||
"-DBINARY_PATH=${CMAKE_BINARY_DIR}"
|
||||
"-DEXECUTABLE_PATH=${BUNDLE_EXECUTABLE_PATH}"
|
||||
"-DBUNDLE_LIBRARY_PATHS=\"${BUNDLE_LIBRARY_PATHS}\""
|
||||
"-DBUNDLE_QT=${BUNDLE_QT}"
|
||||
"-DEXECUTABLE_PATH=${bundle_executable_path}"
|
||||
"-DBUNDLE_LIBRARY_PATHS=\"${bundle_library_paths}\""
|
||||
"-DBUNDLE_QT=${bundle_qt}"
|
||||
"-DIN_PLACE=${in_place}"
|
||||
${EXTRA_BUNDLE_ARGS}
|
||||
"-DLINUXDEPLOY=${CMAKE_BINARY_DIR}/externals/linuxdeploy/squashfs-root/AppRun"
|
||||
-P "${CMAKE_SOURCE_DIR}/CMakeModules/BundleTarget.cmake"
|
||||
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}")
|
||||
endfunction()
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
|
||||
# This function downloads Qt using aqt. The path of the downloaded content will be added to the CMAKE_PREFIX_PATH.
|
||||
# Params:
|
||||
# target: Qt dependency to install. Specify a version number to download Qt, or "tools_(name)" for a specific build tool.
|
||||
function(download_qt target)
|
||||
set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR})
|
||||
|
||||
# Determines parameters based on the host and target for downloading the right Qt binaries.
|
||||
function(determine_qt_parameters target host_out type_out arch_out arch_path_out host_type_out host_arch_out host_arch_path_out)
|
||||
if (target MATCHES "tools_.*")
|
||||
set(DOWNLOAD_QT_TOOL ON)
|
||||
set(tool ON)
|
||||
else()
|
||||
set(DOWNLOAD_QT_TOOL OFF)
|
||||
set(tool OFF)
|
||||
endif()
|
||||
|
||||
# Determine installation parameters for OS, architecture, and compiler
|
||||
if (WIN32)
|
||||
set(host "windows")
|
||||
set(type "desktop")
|
||||
if (NOT DOWNLOAD_QT_TOOL)
|
||||
|
||||
if (NOT tool)
|
||||
if (MINGW)
|
||||
set(arch "win64_mingw")
|
||||
set(arch_path "mingw_64")
|
||||
|
@ -26,21 +27,35 @@ function(download_qt target)
|
|||
message(FATAL_ERROR "Unsupported bundled Qt architecture. Enable USE_SYSTEM_QT and provide your own.")
|
||||
endif()
|
||||
set(arch "win64_${arch_path}")
|
||||
|
||||
# In case we're cross-compiling, prepare to also fetch the correct host Qt tools.
|
||||
if (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "AMD64")
|
||||
set(host_arch_path "msvc2019_64")
|
||||
elseif (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "ARM64")
|
||||
# TODO: msvc2019_arm64 doesn't include some of the required tools for some reason,
|
||||
# TODO: so until it does, just use msvc2019_64 under x86_64 emulation.
|
||||
# set(host_arch_path "msvc2019_arm64")
|
||||
set(host_arch_path "msvc2019_64")
|
||||
endif()
|
||||
set(host_arch "win64_${host_arch_path}")
|
||||
else()
|
||||
message(FATAL_ERROR "Unsupported bundled Qt toolchain. Enable USE_SYSTEM_QT and provide your own.")
|
||||
endif()
|
||||
endif()
|
||||
elseif (APPLE)
|
||||
set(host "mac")
|
||||
if (IOS AND NOT DOWNLOAD_QT_TOOL)
|
||||
set(type "desktop")
|
||||
set(arch "clang_64")
|
||||
set(arch_path "macos")
|
||||
|
||||
if (IOS AND NOT tool)
|
||||
set(host_type "${type}")
|
||||
set(host_arch "${arch}")
|
||||
set(host_arch_path "${arch_path}")
|
||||
|
||||
set(type "ios")
|
||||
set(arch "ios")
|
||||
set(arch_path "ios")
|
||||
set(host_arch_path "macos")
|
||||
else()
|
||||
set(type "desktop")
|
||||
set(arch "clang_64")
|
||||
set(arch_path "macos")
|
||||
endif()
|
||||
else()
|
||||
set(host "linux")
|
||||
|
@ -49,29 +64,65 @@ function(download_qt target)
|
|||
set(arch_path "linux")
|
||||
endif()
|
||||
|
||||
get_external_prefix(qt base_path)
|
||||
file(MAKE_DIRECTORY "${base_path}")
|
||||
set(${host_out} "${host}" PARENT_SCOPE)
|
||||
set(${type_out} "${type}" PARENT_SCOPE)
|
||||
set(${arch_out} "${arch}" PARENT_SCOPE)
|
||||
set(${arch_path_out} "${arch_path}" PARENT_SCOPE)
|
||||
if (DEFINED host_type)
|
||||
set(${host_type_out} "${host_type}" PARENT_SCOPE)
|
||||
else()
|
||||
set(${host_type_out} "${type}" PARENT_SCOPE)
|
||||
endif()
|
||||
if (DEFINED host_arch)
|
||||
set(${host_arch_out} "${host_arch}" PARENT_SCOPE)
|
||||
else()
|
||||
set(${host_arch_out} "${arch}" PARENT_SCOPE)
|
||||
endif()
|
||||
if (DEFINED host_arch_path)
|
||||
set(${host_arch_path_out} "${host_arch_path}" PARENT_SCOPE)
|
||||
else()
|
||||
set(${host_arch_path_out} "${arch_path}" PARENT_SCOPE)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
if (DOWNLOAD_QT_TOOL)
|
||||
# Download Qt binaries for a specifc configuration.
|
||||
function(download_qt_configuration prefix_out target host type arch arch_path base_path)
|
||||
if (target MATCHES "tools_.*")
|
||||
set(tool ON)
|
||||
else()
|
||||
set(tool OFF)
|
||||
endif()
|
||||
|
||||
set(install_args -c "${CURRENT_MODULE_DIR}/aqt_config.ini")
|
||||
if (tool)
|
||||
set(prefix "${base_path}/Tools")
|
||||
set(install_args install-tool --outputdir ${base_path} ${host} desktop ${target})
|
||||
set(install_args ${install_args} install-tool --outputdir ${base_path} ${host} desktop ${target})
|
||||
else()
|
||||
set(prefix "${base_path}/${target}/${arch_path}")
|
||||
if (host_arch_path)
|
||||
set(host_flag "--autodesktop")
|
||||
set(host_prefix "${base_path}/${target}/${host_arch_path}")
|
||||
endif()
|
||||
set(install_args install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch} ${host_flag}
|
||||
-m qtmultimedia --archives qttranslations qttools qtsvg qtbase)
|
||||
set(install_args ${install_args} install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch}
|
||||
-m qtmultimedia --archives qttranslations qttools qtsvg qtbase)
|
||||
endif()
|
||||
|
||||
if (NOT EXISTS "${prefix}")
|
||||
message(STATUS "Downloading binaries for Qt...")
|
||||
message(STATUS "Downloading Qt binaries for ${target}:${host}:${type}:${arch}:${arch_path}")
|
||||
set(AQT_PREBUILD_BASE_URL "https://github.com/miurahr/aqtinstall/releases/download/v3.1.9")
|
||||
if (WIN32)
|
||||
set(aqt_path "${base_path}/aqt.exe")
|
||||
file(DOWNLOAD
|
||||
https://github.com/miurahr/aqtinstall/releases/download/v3.1.7/aqt.exe
|
||||
${aqt_path} SHOW_PROGRESS)
|
||||
if (NOT EXISTS "${aqt_path}")
|
||||
file(DOWNLOAD
|
||||
${AQT_PREBUILD_BASE_URL}/aqt.exe
|
||||
${aqt_path} SHOW_PROGRESS)
|
||||
endif()
|
||||
execute_process(COMMAND ${aqt_path} ${install_args}
|
||||
WORKING_DIRECTORY ${base_path})
|
||||
elseif (APPLE)
|
||||
set(aqt_path "${base_path}/aqt-macos")
|
||||
if (NOT EXISTS "${aqt_path}")
|
||||
file(DOWNLOAD
|
||||
${AQT_PREBUILD_BASE_URL}/aqt-macos
|
||||
${aqt_path} SHOW_PROGRESS)
|
||||
endif()
|
||||
execute_process(COMMAND chmod +x ${aqt_path})
|
||||
execute_process(COMMAND ${aqt_path} ${install_args}
|
||||
WORKING_DIRECTORY ${base_path})
|
||||
else()
|
||||
|
@ -84,18 +135,38 @@ function(download_qt target)
|
|||
execute_process(COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${aqt_install_path} python3 -m aqt ${install_args}
|
||||
WORKING_DIRECTORY ${base_path})
|
||||
endif()
|
||||
|
||||
message(STATUS "Downloaded Qt binaries for ${target}:${host}:${type}:${arch}:${arch_path} to ${prefix}")
|
||||
endif()
|
||||
|
||||
message(STATUS "Using downloaded Qt binaries at ${prefix}")
|
||||
set(${prefix_out} "${prefix}" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Add the Qt prefix path so CMake can locate it.
|
||||
# This function downloads Qt using aqt.
|
||||
# The path of the downloaded content will be added to the CMAKE_PREFIX_PATH.
|
||||
# QT_TARGET_PATH is set to the Qt for the compile target platform.
|
||||
# QT_HOST_PATH is set to a host-compatible Qt, for running tools.
|
||||
# Params:
|
||||
# target: Qt dependency to install. Specify a version number to download Qt, or "tools_(name)" for a specific build tool.
|
||||
function(download_qt target)
|
||||
determine_qt_parameters("${target}" host type arch arch_path host_type host_arch host_arch_path)
|
||||
|
||||
get_external_prefix(qt base_path)
|
||||
file(MAKE_DIRECTORY "${base_path}")
|
||||
|
||||
download_qt_configuration(prefix "${target}" "${host}" "${type}" "${arch}" "${arch_path}" "${base_path}")
|
||||
if (DEFINED host_arch_path AND NOT "${host_arch_path}" STREQUAL "${arch_path}")
|
||||
download_qt_configuration(host_prefix "${target}" "${host}" "${host_type}" "${host_arch}" "${host_arch_path}" "${base_path}")
|
||||
else()
|
||||
set(host_prefix "${prefix}")
|
||||
endif()
|
||||
|
||||
set(QT_TARGET_PATH "${prefix}" CACHE STRING "")
|
||||
set(QT_HOST_PATH "${host_prefix}" CACHE STRING "")
|
||||
|
||||
# Add the target Qt prefix path so CMake can locate it.
|
||||
list(APPEND CMAKE_PREFIX_PATH "${prefix}")
|
||||
set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE)
|
||||
|
||||
if (DEFINED host_prefix)
|
||||
message(STATUS "Using downloaded host Qt binaries at ${host_prefix}")
|
||||
set(QT_HOST_PATH "${host_prefix}" CACHE STRING "")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(download_moltenvk)
|
||||
|
@ -109,7 +180,7 @@ function(download_moltenvk)
|
|||
set(MOLTENVK_TAR "${CMAKE_BINARY_DIR}/externals/MoltenVK.tar")
|
||||
if (NOT EXISTS ${MOLTENVK_DIR})
|
||||
if (NOT EXISTS ${MOLTENVK_TAR})
|
||||
file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/latest/download/MoltenVK-all.tar
|
||||
file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/download/v1.2.7-rc2/MoltenVK-all.tar
|
||||
${MOLTENVK_TAR} SHOW_PROGRESS)
|
||||
endif()
|
||||
|
||||
|
|
|
@ -10,28 +10,30 @@ set(HASH_FILES
|
|||
"${VIDEO_CORE}/renderer_opengl/gl_shader_util.h"
|
||||
"${VIDEO_CORE}/renderer_vulkan/vk_shader_util.cpp"
|
||||
"${VIDEO_CORE}/renderer_vulkan/vk_shader_util.h"
|
||||
"${VIDEO_CORE}/shader/generator/glsl_fs_shader_gen.cpp"
|
||||
"${VIDEO_CORE}/shader/generator/glsl_fs_shader_gen.h"
|
||||
"${VIDEO_CORE}/shader/generator/glsl_shader_decompiler.cpp"
|
||||
"${VIDEO_CORE}/shader/generator/glsl_shader_decompiler.h"
|
||||
"${VIDEO_CORE}/shader/generator/glsl_shader_gen.cpp"
|
||||
"${VIDEO_CORE}/shader/generator/glsl_shader_gen.h"
|
||||
"${VIDEO_CORE}/shader/generator/pica_fs_config.cpp"
|
||||
"${VIDEO_CORE}/shader/generator/pica_fs_config.h"
|
||||
"${VIDEO_CORE}/shader/generator/shader_gen.cpp"
|
||||
"${VIDEO_CORE}/shader/generator/shader_gen.h"
|
||||
"${VIDEO_CORE}/shader/generator/shader_uniforms.cpp"
|
||||
"${VIDEO_CORE}/shader/generator/shader_uniforms.h"
|
||||
"${VIDEO_CORE}/shader/generator/spv_shader_gen.cpp"
|
||||
"${VIDEO_CORE}/shader/generator/spv_shader_gen.h"
|
||||
"${VIDEO_CORE}/shader/generator/spv_fs_shader_gen.cpp"
|
||||
"${VIDEO_CORE}/shader/generator/spv_fs_shader_gen.h"
|
||||
"${VIDEO_CORE}/shader/shader.cpp"
|
||||
"${VIDEO_CORE}/shader/shader.h"
|
||||
"${VIDEO_CORE}/pica.cpp"
|
||||
"${VIDEO_CORE}/pica.h"
|
||||
"${VIDEO_CORE}/regs_framebuffer.h"
|
||||
"${VIDEO_CORE}/regs_lighting.h"
|
||||
"${VIDEO_CORE}/regs_pipeline.h"
|
||||
"${VIDEO_CORE}/regs_rasterizer.h"
|
||||
"${VIDEO_CORE}/regs_shader.h"
|
||||
"${VIDEO_CORE}/regs_texturing.h"
|
||||
"${VIDEO_CORE}/regs.cpp"
|
||||
"${VIDEO_CORE}/regs.h"
|
||||
"${VIDEO_CORE}/pica/regs_framebuffer.h"
|
||||
"${VIDEO_CORE}/pica/regs_lighting.h"
|
||||
"${VIDEO_CORE}/pica/regs_pipeline.h"
|
||||
"${VIDEO_CORE}/pica/regs_rasterizer.h"
|
||||
"${VIDEO_CORE}/pica/regs_shader.h"
|
||||
"${VIDEO_CORE}/pica/regs_texturing.h"
|
||||
"${VIDEO_CORE}/pica/regs_internal.cpp"
|
||||
"${VIDEO_CORE}/pica/regs_internal.h"
|
||||
)
|
||||
set(COMBINED "")
|
||||
foreach (F IN LISTS HASH_FILES)
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
[aqt]
|
||||
concurrency: 2
|
||||
|
||||
[mirrors]
|
||||
trusted_mirrors:
|
||||
https://download.qt.io
|
||||
blacklist:
|
||||
https://qt.mirror.constant.com
|
||||
https://mirrors.ocf.berkeley.edu
|
||||
https://mirrors.ustc.edu.cn
|
||||
https://mirrors.tuna.tsinghua.edu.cn
|
||||
https://mirrors.geekpie.club
|
||||
https://mirrors-wan.geekpie.club
|
||||
https://mirrors.sjtug.sjtu.edu.cn
|
||||
fallbacks:
|
||||
https://qtproject.mirror.liquidtelecom.com/
|
||||
https://mirrors.aliyun.com/qt/
|
||||
https://ftp.jaist.ac.jp/pub/qtproject/
|
||||
https://ftp.yz.yamagata-u.ac.jp/pub/qtproject/
|
||||
https://qt-mirror.dannhauer.de/
|
||||
https://ftp.fau.de/qtproject/
|
||||
https://mirror.netcologne.de/qtproject/
|
||||
https://mirrors.dotsrc.org/qtproject/
|
||||
https://www.nic.funet.fi/pub/mirrors/download.qt-project.org/
|
||||
https://master.qt.io/
|
||||
https://mirrors.ukfast.co.uk/sites/qt.io/
|
||||
https://ftp2.nluug.nl/languages/qt/
|
||||
https://ftp1.nluug.nl/languages/qt/
|
||||
|
99
README.md
99
README.md
|
@ -1,98 +1 @@
|
|||
<h1 align="center">
|
||||
<br>
|
||||
<a href="https://citra-emu.org/"><img src="https://raw.githubusercontent.com/citra-emu/citra-assets/master/Main/citra_logo.svg" alt="Citra" width="200"></a>
|
||||
<br>
|
||||
<b>Citra</b>
|
||||
<br>
|
||||
</h1>
|
||||
|
||||
<h4 align="center"><b>Citra</b> is the world's most popular, open-source, Nintendo 3DS emulator.
|
||||
<br>
|
||||
It is written in C++ with portability in mind and builds are actively maintained for Windows, Linux, Android and macOS.
|
||||
</h4>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/citra-emu/citra/actions/">
|
||||
<img src="https://github.com/citra-emu/citra/workflows/citra-ci/badge.svg"
|
||||
alt="GitHub Actions Build Status">
|
||||
</a>
|
||||
<a href="https://discord.gg/FAXfZV9">
|
||||
<img src="https://img.shields.io/discord/220740965957107713?color=%237289DA&label=Citra&logo=discord&logoColor=white"
|
||||
alt="Discord">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="#compatibility">Compatibility</a> |
|
||||
<a href="#releases">Releases</a> |
|
||||
<a href="#development">Development</a> |
|
||||
<a href="#building">Building</a> |
|
||||
<a href="#support">Support</a> |
|
||||
<a href="#license">License</a>
|
||||
</p>
|
||||
|
||||
|
||||
## Compatibility
|
||||
|
||||
The emulator is capable of running most commercial games at full speed, provided you meet the necessary hardware requirements.
|
||||
|
||||
For a full list of games Citra supports, please visit our [Compatibility page](https://citra-emu.org/game/)
|
||||
|
||||
Check out our [website](https://citra-emu.org/) for the latest news on exciting features, progress reports, and more!
|
||||
Please read the [FAQ](https://citra-emu.org/wiki/faq/) before getting started with the project.
|
||||
|
||||
Need help? Check out our [asking for help](https://citra-emu.org/help/reference/asking/) guide.
|
||||
|
||||
## Releases
|
||||
|
||||
Citra has two main release channels: Nightly and Canary.
|
||||
|
||||
The [Nightly](https://github.com/citra-emu/citra-nightly) build is based on the master branch, and contains already reviewed and tested features.
|
||||
|
||||
The [Canary](https://github.com/citra-emu/citra-canary) build is based on the master branch, but with additional features still under review. PRs tagged `canary-merge` are merged only into the Canary builds.
|
||||
|
||||
Both builds can be installed with the installer provided on the [website](https://citra-emu.org/download/), but those looking for specific versions or standalone releases can find them in the release tabs of the [Nightly](https://github.com/citra-emu/citra-nightly/releases) and [Canary](https://github.com/citra-emu/citra-canary/releases) repositories.
|
||||
|
||||
Android builds can be downloaded from the Google Play Store.
|
||||
|
||||
A Flatpak for Citra is available on [Flathub](https://flathub.org/apps/details/org.citra_emu.citra). Details on the build process can be found in [our Flathub repository](https://github.com/flathub/org.citra_emu.citra).
|
||||
|
||||
## Development
|
||||
|
||||
Most of the development happens on GitHub. It's also where [our central repository](https://github.com/citra-emu/citra) is hosted.
|
||||
For development discussion, please join us on our [Discord server](https://citra-emu.org/discord/) or at #citra-dev on libera.
|
||||
|
||||
If you want to contribute please take a look at the [Contributor's Guide](https://github.com/citra-emu/citra/wiki/Contributing) and [Developer Information](https://github.com/citra-emu/citra/wiki/Developer-Information). You can also contact any of the developers on Discord in order to know about the current state of the emulator.
|
||||
|
||||
If you want to contribute to the user interface translation, please check out the [Citra project on transifex](https://www.transifex.com/citra/citra). We centralize the translation work there, and periodically upstream translations.
|
||||
|
||||
## Building
|
||||
|
||||
* __Windows__: [Windows Build](https://github.com/citra-emu/citra/wiki/Building-For-Windows)
|
||||
* __Linux__: [Linux Build](https://github.com/citra-emu/citra/wiki/Building-For-Linux)
|
||||
* __macOS__: [macOS Build](https://github.com/citra-emu/citra/wiki/Building-for-macOS)
|
||||
* __Android__: [Android Build](https://github.com/citra-emu/citra/wiki/Building-for-Android)
|
||||
|
||||
|
||||
## Support
|
||||
|
||||
If you enjoy the project and want to support us financially, check out our Patreon!
|
||||
|
||||
<a href="https://www.patreon.com/citraemu">
|
||||
<img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" width="160">
|
||||
</a>
|
||||
|
||||
We also happily accept donated games and hardware.
|
||||
Please see our [donations page](https://citra-emu.org/donate/) for more information on how you can contribute to Citra.
|
||||
Any donations received will go towards things like:
|
||||
* 3DS consoles for developers to explore the hardware
|
||||
* 3DS games for testing
|
||||
* Any equipment required for homebrew
|
||||
* Infrastructure setup
|
||||
|
||||
We also more than gladly accept used 3DS consoles! If you would like to give yours away, don't hesitate to join our [Discord server](https://citra-emu.org/discord/) and talk to bunnei.
|
||||
|
||||
|
||||
## License
|
||||
|
||||
Citra is licensed under the GPLv2 (or any later version). Refer to the [LICENSE.txt](https://github.com/citra-emu/citra/blob/master/license.txt) file.
|
||||
ñ
|
||||
|
|
|
@ -21,9 +21,43 @@
|
|||
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
|
||||
<!-- Fixed -->
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.games</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>3ds</string>
|
||||
<string>3dsx</string>
|
||||
<string>cci</string>
|
||||
<string>cxi</string>
|
||||
<string>cia</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Nintendo 3DS File</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Default</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>elf</string>
|
||||
<string>axf</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Unix Executable and Linkable Format</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app requires camera access to emulate the 3DS's cameras.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
|
|
|
@ -287,5 +287,13 @@ dumptxt -p $[OUT] "nfcSecret1Seed=$[NFC_SEED_1]"
|
|||
dumptxt -p $[OUT] "nfcSecret1HmacKey=$[NFC_HMAC_KEY_1]"
|
||||
dumptxt -p $[OUT] "nfcIv=$[NFC_IV]"
|
||||
|
||||
# Dump seeddb.bin as well
|
||||
|
||||
set SEEDDB_IN "0:/gm9/out/seeddb.bin"
|
||||
set SEEDDB_OUT "0:/gm9/seeddb.bin"
|
||||
|
||||
sdump -w seeddb.bin
|
||||
cp -w $[SEEDDB_IN] $[SEEDDB_OUT]
|
||||
|
||||
@Exit
|
||||
|
||||
|
|
|
@ -6,5 +6,5 @@ Usage:
|
|||
1. Copy "DumpKeys.gm9" into the "gm9/scripts/" directory on your SD card.
|
||||
2. Launch GodMode9, press the HOME button, select Scripts, and select "DumpKeys" from the list of scripts that appears.
|
||||
3. Wait for the script to complete and return you to the GodMode9 main menu.
|
||||
4. Power off your system and copy the "gm9/aes_keys.txt" file off of your SD card into "(Citra directory)/sysdata/".
|
||||
4. Power off your system and copy the "gm9/aes_keys.txt" and "gm9/seeddb.bin" files off of your SD card into "(Citra directory)/sysdata/".
|
||||
|
||||
|
|
|
@ -7,3 +7,8 @@ source_file = en.ts
|
|||
source_lang = en
|
||||
type = QT
|
||||
|
||||
[o:citra:p:citra:r:android]
|
||||
file_filter = ../../src/android/app/src/main/res/values-<lang>/strings.xml
|
||||
source_file = ../../src/android/app/src/main/res/values/strings.xml
|
||||
type = ANDROID
|
||||
lang_map = es_ES:es, hu_HU:hu, ru_RU:ru, pt_BR:pt, zh_CN:zh
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,13 @@
|
|||
Name=colorful_dark
|
||||
Comment=Colorful theme (Dark style)
|
||||
Inherits=default
|
||||
Directories=16x16
|
||||
|
||||
Directories=16x16,48x48,256x256
|
||||
|
||||
[16x16]
|
||||
Size=16
|
||||
|
||||
[48x48]
|
||||
Size=48
|
||||
|
||||
[256x256]
|
||||
Size=256
|
||||
|
|
|
@ -2,7 +2,13 @@
|
|||
Name=colorful_midnight_blue
|
||||
Comment=Colorful theme (Midnight Blue style)
|
||||
Inherits=default
|
||||
Directories=16x16
|
||||
Directories=16x16,48x48,256x256
|
||||
|
||||
[16x16]
|
||||
Size=16
|
||||
|
||||
[48x48]
|
||||
Size=48
|
||||
|
||||
[256x256]
|
||||
Size=256
|
||||
|
|
|
@ -12,18 +12,19 @@ QPushButton#GraphicsAPIStatusBarButton:hover {
|
|||
border: 1px solid #76797C;
|
||||
}
|
||||
|
||||
QPushButton#3DOptionStatusBarButton {
|
||||
color: #A5A5A5;
|
||||
font-weight: bold;
|
||||
QPushButton#TogglableStatusBarButton {
|
||||
color: #959595;
|
||||
border: 1px solid transparent;
|
||||
background-color: transparent;
|
||||
padding: 0px 3px 0px 3px;
|
||||
text-align: center;
|
||||
min-width: 60px;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
QPushButton#3DOptionStatusBarButton:hover {
|
||||
QPushButton#TogglableStatusBarButton:checked {
|
||||
color: #00FF00;
|
||||
}
|
||||
|
||||
QPushButton#TogglableStatusBarButton:hover {
|
||||
border: 1px solid #76797C;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,3 @@
|
|||
QPushButton#TogglableStatusBarButton {
|
||||
color: #959595;
|
||||
border: 1px solid transparent;
|
||||
background-color: transparent;
|
||||
padding: 0px 3px 0px 3px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
QPushButton#TogglableStatusBarButton:checked {
|
||||
color: palette(text);
|
||||
}
|
||||
|
||||
QPushButton#TogglableStatusBarButton:hover {
|
||||
border: 1px solid #76797C;
|
||||
}
|
||||
|
||||
QPushButton#GraphicsAPIStatusBarButton {
|
||||
color: #656565;
|
||||
border: 1px solid transparent;
|
||||
|
@ -26,6 +10,23 @@ QPushButton#GraphicsAPIStatusBarButton:hover {
|
|||
border: 1px solid #76797C;
|
||||
}
|
||||
|
||||
QPushButton#TogglableStatusBarButton {
|
||||
min-width: 0px;
|
||||
color: #656565;
|
||||
border: 1px solid transparent;
|
||||
background-color: transparent;
|
||||
padding: 0px 3px 0px 3px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
QPushButton#TogglableStatusBarButton:checked {
|
||||
color: #00FF00;
|
||||
}
|
||||
|
||||
QPushButton#TogglableStatusBarButton:hover {
|
||||
border: 1px solid #76797C;
|
||||
}
|
||||
|
||||
QToolTip {
|
||||
border: 1px solid #76797C;
|
||||
background-color: #5A7566;
|
||||
|
|
|
@ -41,16 +41,34 @@ else()
|
|||
endif()
|
||||
|
||||
# Catch2
|
||||
set(CATCH_INSTALL_DOCS OFF CACHE BOOL "")
|
||||
set(CATCH_INSTALL_EXTRAS OFF CACHE BOOL "")
|
||||
add_subdirectory(catch2)
|
||||
add_library(catch2 INTERFACE)
|
||||
if(USE_SYSTEM_CATCH2)
|
||||
find_package(Catch2 3.0.0 REQUIRED)
|
||||
else()
|
||||
set(CATCH_INSTALL_DOCS OFF CACHE BOOL "")
|
||||
set(CATCH_INSTALL_EXTRAS OFF CACHE BOOL "")
|
||||
add_subdirectory(catch2)
|
||||
endif()
|
||||
target_link_libraries(catch2 INTERFACE Catch2::Catch2WithMain)
|
||||
|
||||
# Crypto++
|
||||
set(CRYPTOPP_BUILD_DOCUMENTATION OFF CACHE BOOL "")
|
||||
set(CRYPTOPP_BUILD_TESTING OFF CACHE BOOL "")
|
||||
set(CRYPTOPP_INSTALL OFF CACHE BOOL "")
|
||||
set(CRYPTOPP_SOURCES "${CMAKE_SOURCE_DIR}/externals/cryptopp" CACHE STRING "")
|
||||
add_subdirectory(cryptopp-cmake)
|
||||
if(USE_SYSTEM_CRYPTOPP)
|
||||
find_package(cryptopp REQUIRED)
|
||||
add_library(cryptopp INTERFACE)
|
||||
target_link_libraries(cryptopp INTERFACE cryptopp::cryptopp)
|
||||
else()
|
||||
if (WIN32 AND NOT MSVC AND "arm64" IN_LIST ARCHITECTURE)
|
||||
# TODO: CryptoPP ARM64 ASM does not seem to support Windows unless compiled with MSVC.
|
||||
# TODO: See https://github.com/weidai11/cryptopp/issues/1260
|
||||
set(CRYPTOPP_DISABLE_ASM ON CACHE BOOL "")
|
||||
endif()
|
||||
|
||||
set(CRYPTOPP_BUILD_DOCUMENTATION OFF CACHE BOOL "")
|
||||
set(CRYPTOPP_BUILD_TESTING OFF CACHE BOOL "")
|
||||
set(CRYPTOPP_INSTALL OFF CACHE BOOL "")
|
||||
set(CRYPTOPP_SOURCES "${CMAKE_SOURCE_DIR}/externals/cryptopp" CACHE STRING "")
|
||||
add_subdirectory(cryptopp-cmake)
|
||||
endif()
|
||||
|
||||
# dds-ktx
|
||||
add_library(dds-ktx INTERFACE)
|
||||
|
@ -79,6 +97,11 @@ if ("x86_64" IN_LIST ARCHITECTURE)
|
|||
endif()
|
||||
endif()
|
||||
|
||||
# Oaknut
|
||||
if ("arm64" IN_LIST ARCHITECTURE)
|
||||
add_subdirectory(oaknut EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
|
||||
# Dynarmic
|
||||
if ("x86_64" IN_LIST ARCHITECTURE OR "arm64" IN_LIST ARCHITECTURE)
|
||||
if(USE_SYSTEM_DYNARMIC)
|
||||
|
@ -103,29 +126,6 @@ if (MSVC)
|
|||
add_subdirectory(getopt)
|
||||
endif()
|
||||
|
||||
# Glad
|
||||
add_subdirectory(glad)
|
||||
|
||||
# glslang
|
||||
if(USE_SYSTEM_GLSLANG)
|
||||
find_package(glslang REQUIRED)
|
||||
add_library(glslang INTERFACE)
|
||||
add_library(SPIRV INTERFACE)
|
||||
target_link_libraries(glslang INTERFACE glslang::glslang)
|
||||
target_link_libraries(SPIRV INTERFACE glslang::SPIRV)
|
||||
# System include path is different from submodule include path
|
||||
get_target_property(GLSLANG_PREFIX glslang::SPIRV INTERFACE_INCLUDE_DIRECTORIES)
|
||||
target_include_directories(SPIRV SYSTEM INTERFACE "${GLSLANG_PREFIX}/glslang")
|
||||
else()
|
||||
set(SKIP_GLSLANG_INSTALL ON CACHE BOOL "")
|
||||
set(ENABLE_GLSLANG_BINARIES OFF CACHE BOOL "")
|
||||
set(ENABLE_SPVREMAPPER OFF CACHE BOOL "")
|
||||
set(ENABLE_CTEST OFF CACHE BOOL "")
|
||||
set(ENABLE_HLSL OFF CACHE BOOL "")
|
||||
set(BUILD_EXTERNAL OFF CACHE BOOL "")
|
||||
add_subdirectory(glslang)
|
||||
endif()
|
||||
|
||||
# inih
|
||||
if(USE_SYSTEM_INIH)
|
||||
find_package(inih REQUIRED COMPONENTS inih inir)
|
||||
|
@ -150,24 +150,12 @@ endif()
|
|||
# Open Source Archives
|
||||
add_subdirectory(open_source_archives)
|
||||
|
||||
# faad2
|
||||
add_subdirectory(faad2 EXCLUDE_FROM_ALL)
|
||||
|
||||
# Dynamic library headers
|
||||
add_library(library-headers INTERFACE)
|
||||
|
||||
if (USE_SYSTEM_FDK_AAC_HEADERS)
|
||||
find_path(SYSTEM_FDK_AAC_INCLUDES NAMES fdk-aac/aacdecoder_lib.h)
|
||||
if (SYSTEM_FDK_AAC_INCLUDES STREQUAL "SYSTEM_FDK_AAC_INCLUDES-NOTFOUND")
|
||||
message(WARNING "System fdk-aac headers not found. Falling back on bundled headers.")
|
||||
else()
|
||||
message(STATUS "Using system fdk_aac headers.")
|
||||
target_include_directories(library-headers SYSTEM INTERFACE ${SYSTEM_FDK_AAC_INCLUDES})
|
||||
set(FOUND_FDK_AAC_HEADERS ON)
|
||||
endif()
|
||||
endif()
|
||||
if (NOT FOUND_FDK_AAC_HEADERS)
|
||||
message(STATUS "Using bundled fdk_aac headers.")
|
||||
target_include_directories(library-headers SYSTEM INTERFACE ./library-headers/fdk-aac/include)
|
||||
endif()
|
||||
|
||||
if (USE_SYSTEM_FFMPEG_HEADERS)
|
||||
find_path(SYSTEM_FFMPEG_INCLUDES NAMES libavutil/avutil.h)
|
||||
if (SYSTEM_FFMPEG_INCLUDES STREQUAL "SYSTEM_FFMPEG_INCLUDES-NOTFOUND")
|
||||
|
@ -192,9 +180,6 @@ if(NOT USE_SYSTEM_SOUNDTOUCH)
|
|||
target_compile_definitions(SoundTouch PUBLIC SOUNDTOUCH_INTEGER_SAMPLES)
|
||||
endif()
|
||||
|
||||
# sirit
|
||||
add_subdirectory(sirit EXCLUDE_FROM_ALL)
|
||||
|
||||
# Teakra
|
||||
add_subdirectory(teakra EXCLUDE_FROM_ALL)
|
||||
|
||||
|
@ -239,14 +224,35 @@ endif()
|
|||
|
||||
# Cubeb
|
||||
if (ENABLE_CUBEB)
|
||||
set(BUILD_TESTS OFF CACHE BOOL "")
|
||||
set(BUILD_TOOLS OFF CACHE BOOL "")
|
||||
set(BUNDLE_SPEEX ON CACHE BOOL "")
|
||||
add_subdirectory(cubeb EXCLUDE_FROM_ALL)
|
||||
if(USE_SYSTEM_CUBEB)
|
||||
find_package(cubeb REQUIRED)
|
||||
add_library(cubeb INTERFACE)
|
||||
target_link_libraries(cubeb INTERFACE cubeb::cubeb)
|
||||
if(TARGET cubeb::cubeb)
|
||||
message(STATUS "Found system cubeb")
|
||||
endif()
|
||||
else()
|
||||
set(BUILD_TESTS OFF CACHE BOOL "")
|
||||
set(BUILD_TOOLS OFF CACHE BOOL "")
|
||||
set(BUNDLE_SPEEX ON CACHE BOOL "")
|
||||
add_subdirectory(cubeb EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# DiscordRPC
|
||||
if (USE_DISCORD_PRESENCE)
|
||||
# rapidjson used by discord-rpc is old and doesn't correctly detect endianness for some platforms.
|
||||
include(TestBigEndian)
|
||||
test_big_endian(RAPIDJSON_BIG_ENDIAN)
|
||||
if(RAPIDJSON_BIG_ENDIAN)
|
||||
add_compile_definitions(RAPIDJSON_ENDIAN=1)
|
||||
else()
|
||||
add_compile_definitions(RAPIDJSON_ENDIAN=0)
|
||||
endif()
|
||||
|
||||
# Apply a dummy CLANG_FORMAT_SUFFIX to disable discord-rpc's unnecessary automatic clang-format.
|
||||
set(CLANG_FORMAT_SUFFIX "dummy")
|
||||
|
||||
add_subdirectory(discord-rpc EXCLUDE_FROM_ALL)
|
||||
target_include_directories(discord-rpc INTERFACE ./discord-rpc/include)
|
||||
endif()
|
||||
|
@ -288,11 +294,20 @@ endif()
|
|||
add_library(httplib INTERFACE)
|
||||
if(USE_SYSTEM_CPP_HTTPLIB)
|
||||
find_package(CppHttp 0.14.1)
|
||||
if(CppHttp_FOUND)
|
||||
target_link_libraries(httplib INTERFACE httplib::httplib)
|
||||
else()
|
||||
message(STATUS "Cpp-httplib not found or not suitable version! Falling back to bundled...")
|
||||
# Detect if system cpphttplib is a shared library
|
||||
# this breaks building as Citra relies on functions that are moved
|
||||
# into the shared object.
|
||||
get_target_property(HTTP_LIBS httplib::httplib INTERFACE_LINK_LIBRARIES)
|
||||
if(HTTP_LIBS)
|
||||
message(WARNING "Shared cpp-http (${HTTP_LIBS}) not supported. Falling back to bundled...")
|
||||
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
|
||||
else()
|
||||
if(CppHttp_FOUND)
|
||||
target_link_libraries(httplib INTERFACE httplib::httplib)
|
||||
else()
|
||||
message(STATUS "Cpp-httplib not found or not suitable version! Falling back to bundled...")
|
||||
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
|
||||
endif()
|
||||
endif()
|
||||
else()
|
||||
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
|
||||
|
@ -300,9 +315,8 @@ endif()
|
|||
target_compile_options(httplib INTERFACE -DCPPHTTPLIB_OPENSSL_SUPPORT)
|
||||
target_link_libraries(httplib INTERFACE ${OPENSSL_LIBRARIES})
|
||||
|
||||
if(ANDROID)
|
||||
add_subdirectory(android-ifaddrs)
|
||||
target_link_libraries(httplib INTERFACE ifaddrs)
|
||||
if (UNIX AND NOT APPLE)
|
||||
add_subdirectory(gamemode)
|
||||
endif()
|
||||
|
||||
# cpp-jwt
|
||||
|
@ -319,7 +333,13 @@ if (ENABLE_WEB_SERVICE)
|
|||
endif()
|
||||
|
||||
# lodepng
|
||||
add_subdirectory(lodepng)
|
||||
if(USE_SYSTEM_LODEPNG)
|
||||
add_library(lodepng INTERFACE)
|
||||
find_package(lodepng REQUIRED)
|
||||
target_link_libraries(lodepng INTERFACE lodepng::lodepng)
|
||||
else()
|
||||
add_subdirectory(lodepng)
|
||||
endif()
|
||||
|
||||
# (xperia64): Only use libyuv on Android b/c of build issues on Windows and mandatory JPEG
|
||||
if(ANDROID)
|
||||
|
@ -330,29 +350,81 @@ endif()
|
|||
|
||||
# OpenAL Soft
|
||||
if (ENABLE_OPENAL)
|
||||
set(ALSOFT_EMBED_HRTF_DATA OFF CACHE BOOL "")
|
||||
set(ALSOFT_EXAMPLES OFF CACHE BOOL "")
|
||||
set(ALSOFT_INSTALL OFF CACHE BOOL "")
|
||||
set(ALSOFT_INSTALL_CONFIG OFF CACHE BOOL "")
|
||||
set(ALSOFT_INSTALL_HRTF_DATA OFF CACHE BOOL "")
|
||||
set(ALSOFT_INSTALL_AMBDEC_PRESETS OFF CACHE BOOL "")
|
||||
set(ALSOFT_UTILS OFF CACHE BOOL "")
|
||||
set(LIBTYPE "STATIC" CACHE STRING "")
|
||||
add_subdirectory(openal-soft EXCLUDE_FROM_ALL)
|
||||
if(USE_SYSTEM_OPENAL)
|
||||
add_library(OpenAL INTERFACE)
|
||||
find_package(OpenAL REQUIRED)
|
||||
target_link_libraries(OpenAL INTERFACE OpenAL::OpenAL)
|
||||
else()
|
||||
set(ALSOFT_EMBED_HRTF_DATA OFF CACHE BOOL "")
|
||||
set(ALSOFT_EXAMPLES OFF CACHE BOOL "")
|
||||
set(ALSOFT_INSTALL OFF CACHE BOOL "")
|
||||
set(ALSOFT_INSTALL_CONFIG OFF CACHE BOOL "")
|
||||
set(ALSOFT_INSTALL_HRTF_DATA OFF CACHE BOOL "")
|
||||
set(ALSOFT_INSTALL_AMBDEC_PRESETS OFF CACHE BOOL "")
|
||||
set(ALSOFT_UTILS OFF CACHE BOOL "")
|
||||
set(LIBTYPE "STATIC" CACHE STRING "")
|
||||
add_subdirectory(openal-soft EXCLUDE_FROM_ALL)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# VMA
|
||||
add_library(vma INTERFACE)
|
||||
target_include_directories(vma SYSTEM INTERFACE ./vma/include)
|
||||
|
||||
# vulkan-headers
|
||||
add_library(vulkan-headers INTERFACE)
|
||||
target_include_directories(vulkan-headers SYSTEM INTERFACE ./vulkan-headers/include)
|
||||
if (APPLE)
|
||||
target_include_directories(vulkan-headers SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/MoltenVK)
|
||||
# OpenGL dependencies
|
||||
if (ENABLE_OPENGL)
|
||||
# Glad
|
||||
add_subdirectory(glad)
|
||||
endif()
|
||||
|
||||
# adrenotools
|
||||
if (ANDROID AND "arm64" IN_LIST ARCHITECTURE)
|
||||
add_subdirectory(libadrenotools)
|
||||
# Vulkan dependencies
|
||||
if (ENABLE_VULKAN)
|
||||
# glslang
|
||||
if(USE_SYSTEM_GLSLANG)
|
||||
find_package(glslang REQUIRED)
|
||||
add_library(glslang INTERFACE)
|
||||
add_library(SPIRV INTERFACE)
|
||||
target_link_libraries(glslang INTERFACE glslang::glslang)
|
||||
target_link_libraries(SPIRV INTERFACE glslang::SPIRV)
|
||||
# System include path is different from submodule include path
|
||||
get_target_property(GLSLANG_PREFIX glslang::SPIRV INTERFACE_INCLUDE_DIRECTORIES)
|
||||
target_include_directories(SPIRV SYSTEM INTERFACE "${GLSLANG_PREFIX}/glslang")
|
||||
else()
|
||||
set(SKIP_GLSLANG_INSTALL ON CACHE BOOL "")
|
||||
set(ENABLE_GLSLANG_BINARIES OFF CACHE BOOL "")
|
||||
set(ENABLE_SPVREMAPPER OFF CACHE BOOL "")
|
||||
set(ENABLE_CTEST OFF CACHE BOOL "")
|
||||
set(ENABLE_HLSL OFF CACHE BOOL "")
|
||||
set(BUILD_EXTERNAL OFF CACHE BOOL "")
|
||||
add_subdirectory(glslang)
|
||||
endif()
|
||||
|
||||
# sirit
|
||||
add_subdirectory(sirit EXCLUDE_FROM_ALL)
|
||||
|
||||
# VMA
|
||||
if(USE_SYSTEM_VMA)
|
||||
add_library(vma INTERFACE)
|
||||
find_package(VulkanMemoryAllocator REQUIRED)
|
||||
if(TARGET GPUOpen::VulkanMemoryAllocator)
|
||||
message(STATUS "Found VulkanMemoryAllocator")
|
||||
target_link_libraries(vma INTERFACE GPUOpen::VulkanMemoryAllocator)
|
||||
endif()
|
||||
else()
|
||||
add_library(vma INTERFACE)
|
||||
target_include_directories(vma SYSTEM INTERFACE ./vma/include)
|
||||
endif()
|
||||
|
||||
# vulkan-headers
|
||||
add_library(vulkan-headers INTERFACE)
|
||||
if(USE_SYSTEM_VULKAN_HEADERS)
|
||||
find_package(Vulkan REQUIRED)
|
||||
if(TARGET Vulkan::Headers)
|
||||
message(STATUS "Found Vulkan headers")
|
||||
target_link_libraries(vulkan-headers INTERFACE Vulkan::Headers)
|
||||
endif()
|
||||
else()
|
||||
target_include_directories(vulkan-headers SYSTEM INTERFACE ./vulkan-headers/include)
|
||||
endif()
|
||||
|
||||
# adrenotools
|
||||
if (ANDROID AND "arm64" IN_LIST ARCHITECTURE)
|
||||
add_subdirectory(libadrenotools)
|
||||
endif()
|
||||
endif()
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
add_library(ifaddrs
|
||||
ifaddrs.c
|
||||
ifaddrs.h
|
||||
)
|
||||
|
||||
create_target_directory_groups(ifaddrs)
|
||||
|
||||
target_include_directories(ifaddrs INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
|
|
@ -1,600 +0,0 @@
|
|||
/*
|
||||
Copyright (c) 2013, Kenneth MacKay
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 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 USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include "ifaddrs.h"
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
#include <errno.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <net/if_arp.h>
|
||||
#include <netinet/in.h>
|
||||
#include <linux/netlink.h>
|
||||
#include <linux/rtnetlink.h>
|
||||
|
||||
typedef struct NetlinkList
|
||||
{
|
||||
struct NetlinkList *m_next;
|
||||
struct nlmsghdr *m_data;
|
||||
unsigned int m_size;
|
||||
} NetlinkList;
|
||||
|
||||
static int netlink_socket(void)
|
||||
{
|
||||
int l_socket = socket(PF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
|
||||
if(l_socket < 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct sockaddr_nl l_addr;
|
||||
memset(&l_addr, 0, sizeof(l_addr));
|
||||
l_addr.nl_family = AF_NETLINK;
|
||||
if(bind(l_socket, (struct sockaddr *)&l_addr, sizeof(l_addr)) < 0)
|
||||
{
|
||||
close(l_socket);
|
||||
return -1;
|
||||
}
|
||||
|
||||
return l_socket;
|
||||
}
|
||||
|
||||
static int netlink_send(int p_socket, int p_request)
|
||||
{
|
||||
char l_buffer[NLMSG_ALIGN(sizeof(struct nlmsghdr)) + NLMSG_ALIGN(sizeof(struct rtgenmsg))];
|
||||
memset(l_buffer, 0, sizeof(l_buffer));
|
||||
struct nlmsghdr *l_hdr = (struct nlmsghdr *)l_buffer;
|
||||
struct rtgenmsg *l_msg = (struct rtgenmsg *)NLMSG_DATA(l_hdr);
|
||||
|
||||
l_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(*l_msg));
|
||||
l_hdr->nlmsg_type = p_request;
|
||||
l_hdr->nlmsg_flags = NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST;
|
||||
l_hdr->nlmsg_pid = 0;
|
||||
l_hdr->nlmsg_seq = p_socket;
|
||||
l_msg->rtgen_family = AF_UNSPEC;
|
||||
|
||||
struct sockaddr_nl l_addr;
|
||||
memset(&l_addr, 0, sizeof(l_addr));
|
||||
l_addr.nl_family = AF_NETLINK;
|
||||
return (sendto(p_socket, l_hdr, l_hdr->nlmsg_len, 0, (struct sockaddr *)&l_addr, sizeof(l_addr)));
|
||||
}
|
||||
|
||||
static int netlink_recv(int p_socket, void *p_buffer, size_t p_len)
|
||||
{
|
||||
struct msghdr l_msg;
|
||||
struct iovec l_iov = { p_buffer, p_len };
|
||||
struct sockaddr_nl l_addr;
|
||||
int l_result;
|
||||
|
||||
for(;;)
|
||||
{
|
||||
l_msg.msg_name = (void *)&l_addr;
|
||||
l_msg.msg_namelen = sizeof(l_addr);
|
||||
l_msg.msg_iov = &l_iov;
|
||||
l_msg.msg_iovlen = 1;
|
||||
l_msg.msg_control = NULL;
|
||||
l_msg.msg_controllen = 0;
|
||||
l_msg.msg_flags = 0;
|
||||
int l_result = recvmsg(p_socket, &l_msg, 0);
|
||||
|
||||
if(l_result < 0)
|
||||
{
|
||||
if(errno == EINTR)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
|
||||
if(l_msg.msg_flags & MSG_TRUNC)
|
||||
{ // buffer was too small
|
||||
return -1;
|
||||
}
|
||||
return l_result;
|
||||
}
|
||||
}
|
||||
|
||||
static struct nlmsghdr *getNetlinkResponse(int p_socket, int *p_size, int *p_done)
|
||||
{
|
||||
size_t l_size = 4096;
|
||||
void *l_buffer = NULL;
|
||||
|
||||
for(;;)
|
||||
{
|
||||
free(l_buffer);
|
||||
l_buffer = malloc(l_size);
|
||||
|
||||
int l_read = netlink_recv(p_socket, l_buffer, l_size);
|
||||
*p_size = l_read;
|
||||
if(l_read == -2)
|
||||
{
|
||||
free(l_buffer);
|
||||
return NULL;
|
||||
}
|
||||
if(l_read >= 0)
|
||||
{
|
||||
pid_t l_pid = getpid();
|
||||
struct nlmsghdr *l_hdr;
|
||||
for(l_hdr = (struct nlmsghdr *)l_buffer; NLMSG_OK(l_hdr, (unsigned int)l_read); l_hdr = (struct nlmsghdr *)NLMSG_NEXT(l_hdr, l_read))
|
||||
{
|
||||
if((pid_t)l_hdr->nlmsg_pid != l_pid || (int)l_hdr->nlmsg_seq != p_socket)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(l_hdr->nlmsg_type == NLMSG_DONE)
|
||||
{
|
||||
*p_done = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
if(l_hdr->nlmsg_type == NLMSG_ERROR)
|
||||
{
|
||||
free(l_buffer);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
return l_buffer;
|
||||
}
|
||||
|
||||
l_size *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
static NetlinkList *newListItem(struct nlmsghdr *p_data, unsigned int p_size)
|
||||
{
|
||||
NetlinkList *l_item = malloc(sizeof(NetlinkList));
|
||||
l_item->m_next = NULL;
|
||||
l_item->m_data = p_data;
|
||||
l_item->m_size = p_size;
|
||||
return l_item;
|
||||
}
|
||||
|
||||
static void freeResultList(NetlinkList *p_list)
|
||||
{
|
||||
NetlinkList *l_cur;
|
||||
while(p_list)
|
||||
{
|
||||
l_cur = p_list;
|
||||
p_list = p_list->m_next;
|
||||
free(l_cur->m_data);
|
||||
free(l_cur);
|
||||
}
|
||||
}
|
||||
|
||||
static NetlinkList *getResultList(int p_socket, int p_request)
|
||||
{
|
||||
if(netlink_send(p_socket, p_request) < 0)
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
NetlinkList *l_list = NULL;
|
||||
NetlinkList *l_end = NULL;
|
||||
int l_size;
|
||||
int l_done = 0;
|
||||
while(!l_done)
|
||||
{
|
||||
struct nlmsghdr *l_hdr = getNetlinkResponse(p_socket, &l_size, &l_done);
|
||||
if(!l_hdr)
|
||||
{ // error
|
||||
freeResultList(l_list);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
NetlinkList *l_item = newListItem(l_hdr, l_size);
|
||||
if(!l_list)
|
||||
{
|
||||
l_list = l_item;
|
||||
}
|
||||
else
|
||||
{
|
||||
l_end->m_next = l_item;
|
||||
}
|
||||
l_end = l_item;
|
||||
}
|
||||
return l_list;
|
||||
}
|
||||
|
||||
static size_t maxSize(size_t a, size_t b)
|
||||
{
|
||||
return (a > b ? a : b);
|
||||
}
|
||||
|
||||
static size_t calcAddrLen(sa_family_t p_family, int p_dataSize)
|
||||
{
|
||||
switch(p_family)
|
||||
{
|
||||
case AF_INET:
|
||||
return sizeof(struct sockaddr_in);
|
||||
case AF_INET6:
|
||||
return sizeof(struct sockaddr_in6);
|
||||
case AF_PACKET:
|
||||
return maxSize(sizeof(struct sockaddr_ll), offsetof(struct sockaddr_ll, sll_addr) + p_dataSize);
|
||||
default:
|
||||
return maxSize(sizeof(struct sockaddr), offsetof(struct sockaddr, sa_data) + p_dataSize);
|
||||
}
|
||||
}
|
||||
|
||||
static void makeSockaddr(sa_family_t p_family, struct sockaddr *p_dest, void *p_data, size_t p_size)
|
||||
{
|
||||
switch(p_family)
|
||||
{
|
||||
case AF_INET:
|
||||
memcpy(&((struct sockaddr_in*)p_dest)->sin_addr, p_data, p_size);
|
||||
break;
|
||||
case AF_INET6:
|
||||
memcpy(&((struct sockaddr_in6*)p_dest)->sin6_addr, p_data, p_size);
|
||||
break;
|
||||
case AF_PACKET:
|
||||
memcpy(((struct sockaddr_ll*)p_dest)->sll_addr, p_data, p_size);
|
||||
((struct sockaddr_ll*)p_dest)->sll_halen = p_size;
|
||||
break;
|
||||
default:
|
||||
memcpy(p_dest->sa_data, p_data, p_size);
|
||||
break;
|
||||
}
|
||||
p_dest->sa_family = p_family;
|
||||
}
|
||||
|
||||
static void addToEnd(struct ifaddrs **p_resultList, struct ifaddrs *p_entry)
|
||||
{
|
||||
if(!*p_resultList)
|
||||
{
|
||||
*p_resultList = p_entry;
|
||||
}
|
||||
else
|
||||
{
|
||||
struct ifaddrs *l_cur = *p_resultList;
|
||||
while(l_cur->ifa_next)
|
||||
{
|
||||
l_cur = l_cur->ifa_next;
|
||||
}
|
||||
l_cur->ifa_next = p_entry;
|
||||
}
|
||||
}
|
||||
|
||||
static void interpretLink(struct nlmsghdr *p_hdr, struct ifaddrs **p_links, struct ifaddrs **p_resultList)
|
||||
{
|
||||
struct ifinfomsg *l_info = (struct ifinfomsg *)NLMSG_DATA(p_hdr);
|
||||
|
||||
size_t l_nameSize = 0;
|
||||
size_t l_addrSize = 0;
|
||||
size_t l_dataSize = 0;
|
||||
|
||||
size_t l_rtaSize = NLMSG_PAYLOAD(p_hdr, sizeof(struct ifinfomsg));
|
||||
struct rtattr *l_rta;
|
||||
for(l_rta = (struct rtattr *)(((char *)l_info) + NLMSG_ALIGN(sizeof(struct ifinfomsg))); RTA_OK(l_rta, l_rtaSize); l_rta = RTA_NEXT(l_rta, l_rtaSize))
|
||||
{
|
||||
void *l_rtaData = RTA_DATA(l_rta);
|
||||
size_t l_rtaDataSize = RTA_PAYLOAD(l_rta);
|
||||
switch(l_rta->rta_type)
|
||||
{
|
||||
case IFLA_ADDRESS:
|
||||
case IFLA_BROADCAST:
|
||||
l_addrSize += NLMSG_ALIGN(calcAddrLen(AF_PACKET, l_rtaDataSize));
|
||||
break;
|
||||
case IFLA_IFNAME:
|
||||
l_nameSize += NLMSG_ALIGN(l_rtaSize + 1);
|
||||
break;
|
||||
case IFLA_STATS:
|
||||
l_dataSize += NLMSG_ALIGN(l_rtaSize);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
struct ifaddrs *l_entry = malloc(sizeof(struct ifaddrs) + l_nameSize + l_addrSize + l_dataSize);
|
||||
memset(l_entry, 0, sizeof(struct ifaddrs));
|
||||
l_entry->ifa_name = "";
|
||||
|
||||
char *l_name = ((char *)l_entry) + sizeof(struct ifaddrs);
|
||||
char *l_addr = l_name + l_nameSize;
|
||||
char *l_data = l_addr + l_addrSize;
|
||||
|
||||
l_entry->ifa_flags = l_info->ifi_flags;
|
||||
|
||||
l_rtaSize = NLMSG_PAYLOAD(p_hdr, sizeof(struct ifinfomsg));
|
||||
for(l_rta = (struct rtattr *)(((char *)l_info) + NLMSG_ALIGN(sizeof(struct ifinfomsg))); RTA_OK(l_rta, l_rtaSize); l_rta = RTA_NEXT(l_rta, l_rtaSize))
|
||||
{
|
||||
void *l_rtaData = RTA_DATA(l_rta);
|
||||
size_t l_rtaDataSize = RTA_PAYLOAD(l_rta);
|
||||
switch(l_rta->rta_type)
|
||||
{
|
||||
case IFLA_ADDRESS:
|
||||
case IFLA_BROADCAST:
|
||||
{
|
||||
size_t l_addrLen = calcAddrLen(AF_PACKET, l_rtaDataSize);
|
||||
makeSockaddr(AF_PACKET, (struct sockaddr *)l_addr, l_rtaData, l_rtaDataSize);
|
||||
((struct sockaddr_ll *)l_addr)->sll_ifindex = l_info->ifi_index;
|
||||
((struct sockaddr_ll *)l_addr)->sll_hatype = l_info->ifi_type;
|
||||
if(l_rta->rta_type == IFLA_ADDRESS)
|
||||
{
|
||||
l_entry->ifa_addr = (struct sockaddr *)l_addr;
|
||||
}
|
||||
else
|
||||
{
|
||||
l_entry->ifa_broadaddr = (struct sockaddr *)l_addr;
|
||||
}
|
||||
l_addr += NLMSG_ALIGN(l_addrLen);
|
||||
break;
|
||||
}
|
||||
case IFLA_IFNAME:
|
||||
strncpy(l_name, l_rtaData, l_rtaDataSize);
|
||||
l_name[l_rtaDataSize] = '\0';
|
||||
l_entry->ifa_name = l_name;
|
||||
break;
|
||||
case IFLA_STATS:
|
||||
memcpy(l_data, l_rtaData, l_rtaDataSize);
|
||||
l_entry->ifa_data = l_data;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
addToEnd(p_resultList, l_entry);
|
||||
p_links[l_info->ifi_index - 1] = l_entry;
|
||||
}
|
||||
|
||||
static void interpretAddr(struct nlmsghdr *p_hdr, struct ifaddrs **p_links, struct ifaddrs **p_resultList)
|
||||
{
|
||||
struct ifaddrmsg *l_info = (struct ifaddrmsg *)NLMSG_DATA(p_hdr);
|
||||
|
||||
size_t l_nameSize = 0;
|
||||
size_t l_addrSize = 0;
|
||||
|
||||
int l_addedNetmask = 0;
|
||||
|
||||
size_t l_rtaSize = NLMSG_PAYLOAD(p_hdr, sizeof(struct ifaddrmsg));
|
||||
struct rtattr *l_rta;
|
||||
for(l_rta = (struct rtattr *)(((char *)l_info) + NLMSG_ALIGN(sizeof(struct ifaddrmsg))); RTA_OK(l_rta, l_rtaSize); l_rta = RTA_NEXT(l_rta, l_rtaSize))
|
||||
{
|
||||
void *l_rtaData = RTA_DATA(l_rta);
|
||||
size_t l_rtaDataSize = RTA_PAYLOAD(l_rta);
|
||||
if(l_info->ifa_family == AF_PACKET)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch(l_rta->rta_type)
|
||||
{
|
||||
case IFA_ADDRESS:
|
||||
case IFA_LOCAL:
|
||||
if((l_info->ifa_family == AF_INET || l_info->ifa_family == AF_INET6) && !l_addedNetmask)
|
||||
{ // make room for netmask
|
||||
l_addrSize += NLMSG_ALIGN(calcAddrLen(l_info->ifa_family, l_rtaDataSize));
|
||||
l_addedNetmask = 1;
|
||||
}
|
||||
case IFA_BROADCAST:
|
||||
l_addrSize += NLMSG_ALIGN(calcAddrLen(l_info->ifa_family, l_rtaDataSize));
|
||||
break;
|
||||
case IFA_LABEL:
|
||||
l_nameSize += NLMSG_ALIGN(l_rtaSize + 1);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
struct ifaddrs *l_entry = malloc(sizeof(struct ifaddrs) + l_nameSize + l_addrSize);
|
||||
memset(l_entry, 0, sizeof(struct ifaddrs));
|
||||
l_entry->ifa_name = p_links[l_info->ifa_index - 1]->ifa_name;
|
||||
|
||||
char *l_name = ((char *)l_entry) + sizeof(struct ifaddrs);
|
||||
char *l_addr = l_name + l_nameSize;
|
||||
|
||||
l_entry->ifa_flags = l_info->ifa_flags | p_links[l_info->ifa_index - 1]->ifa_flags;
|
||||
|
||||
l_rtaSize = NLMSG_PAYLOAD(p_hdr, sizeof(struct ifaddrmsg));
|
||||
for(l_rta = (struct rtattr *)(((char *)l_info) + NLMSG_ALIGN(sizeof(struct ifaddrmsg))); RTA_OK(l_rta, l_rtaSize); l_rta = RTA_NEXT(l_rta, l_rtaSize))
|
||||
{
|
||||
void *l_rtaData = RTA_DATA(l_rta);
|
||||
size_t l_rtaDataSize = RTA_PAYLOAD(l_rta);
|
||||
switch(l_rta->rta_type)
|
||||
{
|
||||
case IFA_ADDRESS:
|
||||
case IFA_BROADCAST:
|
||||
case IFA_LOCAL:
|
||||
{
|
||||
size_t l_addrLen = calcAddrLen(l_info->ifa_family, l_rtaDataSize);
|
||||
makeSockaddr(l_info->ifa_family, (struct sockaddr *)l_addr, l_rtaData, l_rtaDataSize);
|
||||
if(l_info->ifa_family == AF_INET6)
|
||||
{
|
||||
if(IN6_IS_ADDR_LINKLOCAL((struct in6_addr *)l_rtaData) || IN6_IS_ADDR_MC_LINKLOCAL((struct in6_addr *)l_rtaData))
|
||||
{
|
||||
((struct sockaddr_in6 *)l_addr)->sin6_scope_id = l_info->ifa_index;
|
||||
}
|
||||
}
|
||||
|
||||
if(l_rta->rta_type == IFA_ADDRESS)
|
||||
{ // apparently in a point-to-point network IFA_ADDRESS contains the dest address and IFA_LOCAL contains the local address
|
||||
if(l_entry->ifa_addr)
|
||||
{
|
||||
l_entry->ifa_dstaddr = (struct sockaddr *)l_addr;
|
||||
}
|
||||
else
|
||||
{
|
||||
l_entry->ifa_addr = (struct sockaddr *)l_addr;
|
||||
}
|
||||
}
|
||||
else if(l_rta->rta_type == IFA_LOCAL)
|
||||
{
|
||||
if(l_entry->ifa_addr)
|
||||
{
|
||||
l_entry->ifa_dstaddr = l_entry->ifa_addr;
|
||||
}
|
||||
l_entry->ifa_addr = (struct sockaddr *)l_addr;
|
||||
}
|
||||
else
|
||||
{
|
||||
l_entry->ifa_broadaddr = (struct sockaddr *)l_addr;
|
||||
}
|
||||
l_addr += NLMSG_ALIGN(l_addrLen);
|
||||
break;
|
||||
}
|
||||
case IFA_LABEL:
|
||||
strncpy(l_name, l_rtaData, l_rtaDataSize);
|
||||
l_name[l_rtaDataSize] = '\0';
|
||||
l_entry->ifa_name = l_name;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if(l_entry->ifa_addr && (l_entry->ifa_addr->sa_family == AF_INET || l_entry->ifa_addr->sa_family == AF_INET6))
|
||||
{
|
||||
unsigned l_maxPrefix = (l_entry->ifa_addr->sa_family == AF_INET ? 32 : 128);
|
||||
unsigned l_prefix = (l_info->ifa_prefixlen > l_maxPrefix ? l_maxPrefix : l_info->ifa_prefixlen);
|
||||
char l_mask[16] = {0};
|
||||
unsigned i;
|
||||
for(i=0; i<(l_prefix/8); ++i)
|
||||
{
|
||||
l_mask[i] = 0xff;
|
||||
}
|
||||
l_mask[i] = 0xff << (8 - (l_prefix % 8));
|
||||
|
||||
makeSockaddr(l_entry->ifa_addr->sa_family, (struct sockaddr *)l_addr, l_mask, l_maxPrefix / 8);
|
||||
l_entry->ifa_netmask = (struct sockaddr *)l_addr;
|
||||
}
|
||||
|
||||
addToEnd(p_resultList, l_entry);
|
||||
}
|
||||
|
||||
static void interpret(int p_socket, NetlinkList *p_netlinkList, struct ifaddrs **p_links, struct ifaddrs **p_resultList)
|
||||
{
|
||||
pid_t l_pid = getpid();
|
||||
for(; p_netlinkList; p_netlinkList = p_netlinkList->m_next)
|
||||
{
|
||||
unsigned int l_nlsize = p_netlinkList->m_size;
|
||||
struct nlmsghdr *l_hdr;
|
||||
for(l_hdr = p_netlinkList->m_data; NLMSG_OK(l_hdr, l_nlsize); l_hdr = NLMSG_NEXT(l_hdr, l_nlsize))
|
||||
{
|
||||
if((pid_t)l_hdr->nlmsg_pid != l_pid || (int)l_hdr->nlmsg_seq != p_socket)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(l_hdr->nlmsg_type == NLMSG_DONE)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if(l_hdr->nlmsg_type == RTM_NEWLINK)
|
||||
{
|
||||
interpretLink(l_hdr, p_links, p_resultList);
|
||||
}
|
||||
else if(l_hdr->nlmsg_type == RTM_NEWADDR)
|
||||
{
|
||||
interpretAddr(l_hdr, p_links, p_resultList);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static unsigned countLinks(int p_socket, NetlinkList *p_netlinkList)
|
||||
{
|
||||
unsigned l_links = 0;
|
||||
pid_t l_pid = getpid();
|
||||
for(; p_netlinkList; p_netlinkList = p_netlinkList->m_next)
|
||||
{
|
||||
unsigned int l_nlsize = p_netlinkList->m_size;
|
||||
struct nlmsghdr *l_hdr;
|
||||
for(l_hdr = p_netlinkList->m_data; NLMSG_OK(l_hdr, l_nlsize); l_hdr = NLMSG_NEXT(l_hdr, l_nlsize))
|
||||
{
|
||||
if((pid_t)l_hdr->nlmsg_pid != l_pid || (int)l_hdr->nlmsg_seq != p_socket)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(l_hdr->nlmsg_type == NLMSG_DONE)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if(l_hdr->nlmsg_type == RTM_NEWLINK)
|
||||
{
|
||||
++l_links;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return l_links;
|
||||
}
|
||||
|
||||
int getifaddrs(struct ifaddrs **ifap)
|
||||
{
|
||||
if(!ifap)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
*ifap = NULL;
|
||||
|
||||
int l_socket = netlink_socket();
|
||||
if(l_socket < 0)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
NetlinkList *l_linkResults = getResultList(l_socket, RTM_GETLINK);
|
||||
if(!l_linkResults)
|
||||
{
|
||||
close(l_socket);
|
||||
return -1;
|
||||
}
|
||||
|
||||
NetlinkList *l_addrResults = getResultList(l_socket, RTM_GETADDR);
|
||||
if(!l_addrResults)
|
||||
{
|
||||
close(l_socket);
|
||||
freeResultList(l_linkResults);
|
||||
return -1;
|
||||
}
|
||||
|
||||
unsigned l_numLinks = countLinks(l_socket, l_linkResults) + countLinks(l_socket, l_addrResults);
|
||||
struct ifaddrs *l_links[l_numLinks];
|
||||
memset(l_links, 0, l_numLinks * sizeof(struct ifaddrs *));
|
||||
|
||||
interpret(l_socket, l_linkResults, l_links, ifap);
|
||||
interpret(l_socket, l_addrResults, l_links, ifap);
|
||||
|
||||
freeResultList(l_linkResults);
|
||||
freeResultList(l_addrResults);
|
||||
close(l_socket);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void freeifaddrs(struct ifaddrs *ifa)
|
||||
{
|
||||
struct ifaddrs *l_cur;
|
||||
while(ifa)
|
||||
{
|
||||
l_cur = ifa;
|
||||
ifa = ifa->ifa_next;
|
||||
free(l_cur);
|
||||
}
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 1995, 1999
|
||||
* Berkeley Software Design, Inc. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY Berkeley Software Design, Inc. ``AS IS'' AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL Berkeley Software Design, Inc. BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* 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 USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
* SUCH DAMAGE.
|
||||
*
|
||||
* BSDI ifaddrs.h,v 2.5 2000/02/23 14:51:59 dab Exp
|
||||
*/
|
||||
|
||||
#ifndef _IFADDRS_H_
|
||||
#define _IFADDRS_H_
|
||||
|
||||
struct ifaddrs {
|
||||
struct ifaddrs *ifa_next;
|
||||
char *ifa_name;
|
||||
unsigned int ifa_flags;
|
||||
struct sockaddr *ifa_addr;
|
||||
struct sockaddr *ifa_netmask;
|
||||
struct sockaddr *ifa_dstaddr;
|
||||
void *ifa_data;
|
||||
};
|
||||
|
||||
/*
|
||||
* This may have been defined in <net/if.h>. Note that if <net/if.h> is
|
||||
* to be included it must be included before this header file.
|
||||
*/
|
||||
#ifndef ifa_broadaddr
|
||||
#define ifa_broadaddr ifa_dstaddr /* broadcast address interface */
|
||||
#endif
|
||||
|
||||
#include <sys/cdefs.h>
|
||||
|
||||
__BEGIN_DECLS
|
||||
extern int getifaddrs(struct ifaddrs **ifap);
|
||||
extern void freeifaddrs(struct ifaddrs *ifa);
|
||||
__END_DECLS
|
||||
|
||||
#endif
|
|
@ -15,11 +15,17 @@ option(USE_SYSTEM_DYNARMIC "Use the system dynarmic (instead of the bundled one)
|
|||
option(USE_SYSTEM_FMT "Use the system fmt (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_XBYAK "Use the system xbyak (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_INIH "Use the system inih (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_FDK_AAC_HEADERS "Use the system fdk-aac headers (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_FFMPEG_HEADERS "Use the system FFmpeg headers (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_GLSLANG "Use the system glslang and SPIR-V libraries (instead of the bundled ones)" OFF)
|
||||
option(USE_SYSTEM_ZSTD "Use the system Zstandard library (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_ENET "Use the system libenet (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_CRYPTOPP "Use the system cryptopp (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_CUBEB "Use the system cubeb (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_LODEPNG "Use the system lodepng (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_OPENAL "Use the system OpenAL (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_VMA "Use the system VulkanMemoryAllocator (instead of the bundled one)" OFF)
|
||||
option(USE_SYSTEM_VULKAN_HEADERS "Use the system Vulkan headers (instead of the bundled ones)" OFF)
|
||||
option(USE_SYSTEM_CATCH2 "Use the system Catch2 (instead of the bundled one)" OFF)
|
||||
|
||||
# Qt and MoltenVK are handled separately
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_SDL2 "Disable system SDL2" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
|
@ -34,11 +40,17 @@ CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_DYNARMIC "Disable system Dynarmic" OFF "US
|
|||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_FMT "Disable system fmt" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_XBYAK "Disable system xbyak" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_INIH "Disable system inih" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_FDK_AAC_HEADERS "Disable system fdk_aac" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_FFMPEG_HEADERS "Disable system ffmpeg" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_GLSLANG "Disable system glslang" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_ZSTD "Disable system Zstandard" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_ENET "Disable system libenet" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_CRYPTOPP "Disable system cryptopp" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_CUBEB "Disable system cubeb" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_LODEPNG "Disable system lodepng" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_OPENAL "Disable system OpenAL" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_VMA "Disable system VulkanMemoryAllocator" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_VULKAN_HEADERS "Disable system Vulkan headers" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_CATCH2 "Disable system Catch2" OFF "USE_SYSTEM_LIBS" OFF)
|
||||
|
||||
set(LIB_VAR_LIST
|
||||
SDL2
|
||||
|
@ -53,11 +65,17 @@ set(LIB_VAR_LIST
|
|||
FMT
|
||||
XBYAK
|
||||
INIH
|
||||
FDK_AAC_HEADERS
|
||||
FFMPEG_HEADERS
|
||||
GLSLANG
|
||||
ZSTD
|
||||
ENET
|
||||
CRYPTOPP
|
||||
CUBEB
|
||||
LODEPNG
|
||||
OPENAL
|
||||
VMA
|
||||
VULKAN_HEADERS
|
||||
CATCH2
|
||||
)
|
||||
|
||||
# First, check that USE_SYSTEM_XXX is not used with USE_SYSTEM_LIBS
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
if(NOT OPENAL_FOUND)
|
||||
pkg_check_modules(OPENAL_TMP openal)
|
||||
|
||||
find_path(OPENAL_INCLUDE_DIRS NAMES al.h
|
||||
PATHS
|
||||
${OPENAL_TMP_INCLUDE_DIRS}
|
||||
/usr/include/AL
|
||||
/usr/include
|
||||
/usr/local/include/AL
|
||||
/usr/local/include
|
||||
)
|
||||
|
||||
find_library(OPENAL_LIBRARY_DIRS NAMES openal
|
||||
PATHS
|
||||
${OPENAL_TMP_LIBRARY_DIRS}
|
||||
/usr/lib
|
||||
/usr/local/lib
|
||||
)
|
||||
|
||||
if(OPENAL_INCLUDE_DIRS AND OPENAL_LIBRARY_DIRS)
|
||||
set(OPENAL_FOUND TRUE CACHE INTERNAL "OpenAL found")
|
||||
message(STATUS "Found OpenAL: ${OPENAL_LIBRARY_DIRS}, ${OPENAL_INCLUDE_DIRS}")
|
||||
else()
|
||||
set(OPENAL_FOUND FALSE CACHE INTERNAL "OpenAL found")
|
||||
message(STATUS "OpenAL not found.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(OPENAL_FOUND AND NOT TARGET OpenAL::OpenAL)
|
||||
add_library(OpenAL::OpenAL UNKNOWN IMPORTED)
|
||||
set_target_properties(OpenAL::OpenAL PROPERTIES
|
||||
INCLUDE_DIRECTORIES ${OPENAL_INCLUDE_DIRS}
|
||||
INTERFACE_LINK_LIBRARIES ${OPENAL_LIBRARY_DIRS}
|
||||
IMPORTED_LOCATION ${OPENAL_LIBRARY_DIRS}
|
||||
)
|
||||
endif()
|
|
@ -0,0 +1,35 @@
|
|||
if(NOT CRYPTOPP_FOUND)
|
||||
pkg_search_module(CRYPTOPP_TMP crypto++ cryptopp)
|
||||
|
||||
find_path(CRYPTOPP_INCLUDE_DIRS NAMES cryptlib.h
|
||||
PATHS
|
||||
${CRYPTOPP_TMP_INCLUDE_DIRS}
|
||||
/usr/include
|
||||
/usr/local/include
|
||||
PATH_SUFFIXES crypto++ cryptopp
|
||||
)
|
||||
|
||||
find_library(CRYPTOPP_LIBRARY_DIRS NAMES crypto++ cryptopp
|
||||
PATHS
|
||||
${CRYPTOPP_TMP_LIBRARY_DIRS}
|
||||
/usr/lib
|
||||
/usr/local/lib
|
||||
)
|
||||
|
||||
if(CRYPTOPP_INCLUDE_DIRS AND CRYPTOPP_LIBRARY_DIRS)
|
||||
set(CRYPTOPP_FOUND TRUE CACHE INTERNAL "Found cryptopp")
|
||||
message(STATUS "Found cryptopp: ${CRYPTOPP_LIBRARY_DIRS}, ${CRYPTOPP_INCLUDE_DIRS}")
|
||||
else()
|
||||
set(CRYPTOPP_FOUND FALSE CACHE INTERNAL "Found cryptopp")
|
||||
message(STATUS "Cryptopp not found.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(CRYPTOPP_FOUND AND NOT TARGET cryptopp::cryptopp)
|
||||
add_library(cryptopp::cryptopp UNKNOWN IMPORTED)
|
||||
set_target_properties(cryptopp::cryptopp PROPERTIES
|
||||
INCLUDE_DIRECTORIES ${CRYPTOPP_INCLUDE_DIRS}
|
||||
INTERFACE_LINK_LIBRARIES ${CRYPTOPP_LIBRARY_DIRS}
|
||||
IMPORTED_LOCATION ${CRYPTOPP_LIBRARY_DIRS}
|
||||
)
|
||||
endif()
|
|
@ -0,0 +1,31 @@
|
|||
if(NOT LODEPNG_FOUND)
|
||||
find_path(LODEPNG_INCLUDE_DIRS NAMES lodepng.h
|
||||
PATHS
|
||||
/usr/include
|
||||
/usr/local/include
|
||||
)
|
||||
|
||||
find_library(LODEPNG_LIBRARY_DIRS NAMES lodepng
|
||||
PATHS
|
||||
/usr/lib
|
||||
/usr/local/lib
|
||||
)
|
||||
|
||||
if(LODEPNG_INCLUDE_DIRS AND LODEPNG_LIBRARY_DIRS)
|
||||
set(LODEPNG_FOUND TRUE CACHE INTERNAL "Found lodepng")
|
||||
message(STATUS "Found lodepng: ${LODEPNG_LIBRARY_DIRS}, ${LODEPNG_INCLUDE_DIRS}")
|
||||
else()
|
||||
set(LODEPNG_FOUND FALSE CACHE INTERNAL "Found lodepng")
|
||||
message(STATUS "Lodepng not found.")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(LODEPNG_FOUND AND NOT TARGET lodepng::lodepng)
|
||||
add_library(lodepng::lodepng UNKNOWN IMPORTED)
|
||||
set_target_properties(lodepng::lodepng PROPERTIES
|
||||
INCLUDE_DIRECTORIES ${LODEPNG_INCLUDE_DIRS}
|
||||
INTERFACE_LINK_LIBRARIES ${LODEPNG_LIBRARY_DIRS}
|
||||
IMPORTED_LOCATION ${LODEPNG_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
endif()
|
|
@ -1 +1 @@
|
|||
Subproject commit 9327192b0095dc1f420b2082d37bd427b5750d48
|
||||
Subproject commit a99c80c26686e44eddf0432140ae397f3efbd0b3
|
|
@ -1 +1 @@
|
|||
Subproject commit 48689ae7a73caeb747953f9ed664dc71d2f918d8
|
||||
Subproject commit 799e775484b8fce7e986ee7a4f4b651fec2bca07
|
|
@ -1 +1 @@
|
|||
Subproject commit d333a09b3b9152af3cb442902ae8ea18d8416470
|
||||
Subproject commit 30f1a3c6289075ef4af08f5ec502be2fc8627a0c
|
|
@ -0,0 +1,88 @@
|
|||
# Sources cut down to just what we need for AAC-LC.
|
||||
set(FAAD2_SOURCE_DIR "faad2/libfaad")
|
||||
add_library(faad2 STATIC EXCLUDE_FROM_ALL
|
||||
"${FAAD2_SOURCE_DIR}/bits.c"
|
||||
"${FAAD2_SOURCE_DIR}/cfft.c"
|
||||
"${FAAD2_SOURCE_DIR}/common.c"
|
||||
"${FAAD2_SOURCE_DIR}/decoder.c"
|
||||
"${FAAD2_SOURCE_DIR}/drc.c"
|
||||
"${FAAD2_SOURCE_DIR}/error.c"
|
||||
"${FAAD2_SOURCE_DIR}/filtbank.c"
|
||||
"${FAAD2_SOURCE_DIR}/huffman.c"
|
||||
"${FAAD2_SOURCE_DIR}/is.c"
|
||||
"${FAAD2_SOURCE_DIR}/mdct.c"
|
||||
"${FAAD2_SOURCE_DIR}/mp4.c"
|
||||
"${FAAD2_SOURCE_DIR}/ms.c"
|
||||
"${FAAD2_SOURCE_DIR}/output.c"
|
||||
"${FAAD2_SOURCE_DIR}/pns.c"
|
||||
"${FAAD2_SOURCE_DIR}/pulse.c"
|
||||
"${FAAD2_SOURCE_DIR}/specrec.c"
|
||||
"${FAAD2_SOURCE_DIR}/syntax.c"
|
||||
"${FAAD2_SOURCE_DIR}/tns.c"
|
||||
)
|
||||
target_include_directories(faad2 PUBLIC faad2/include PRIVATE "${FAAD2_SOURCE_DIR}")
|
||||
|
||||
# Configure compile definitions.
|
||||
|
||||
# Read version from properties file for configuring constant.
|
||||
file(READ faad2/properties.json FAAD_PROPERTIES_JSON)
|
||||
string(JSON FAAD_VERSION GET ${FAAD_PROPERTIES_JSON} PACKAGE_VERSION)
|
||||
message(STATUS "Building faad2 version ${FAAD_VERSION}")
|
||||
|
||||
# Check for functions and headers.
|
||||
include(CheckFunctionExists)
|
||||
include(CheckIncludeFiles)
|
||||
check_function_exists(getpwuid HAVE_GETPWUID)
|
||||
check_function_exists(lrintf HAVE_LRINTF)
|
||||
check_function_exists(memcpy HAVE_MEMCPY)
|
||||
check_function_exists(strchr HAVE_STRCHR)
|
||||
check_function_exists(strsep HAVE_STRSEP)
|
||||
check_include_files(dlfcn.h HAVE_DLFCN_H)
|
||||
check_include_files(errno.h HAVE_ERRNO_H)
|
||||
check_include_files(float.h HAVE_FLOAT_H)
|
||||
check_include_files(inttypes.h HAVE_INTTYPES_H)
|
||||
check_include_files(IOKit/IOKitLib.h HAVE_IOKIT_IOKITLIB_H)
|
||||
check_include_files(limits.h HAVE_LIMITS_H)
|
||||
check_include_files(mathf.h HAVE_MATHF_H)
|
||||
check_include_files(stdint.h HAVE_STDINT_H)
|
||||
check_include_files(stdio.h HAVE_STDIO_H)
|
||||
check_include_files(stdlib.h HAVE_STDLIB_H)
|
||||
check_include_files(strings.h HAVE_STRINGS_H)
|
||||
check_include_files(string.h HAVE_STRING_H)
|
||||
check_include_files(sysfs/libsysfs.h HAVE_SYSFS_LIBSYSFS_H)
|
||||
check_include_files(sys/stat.h HAVE_SYS_STAT_H)
|
||||
check_include_files(sys/time.h HAVE_SYS_TIME_H)
|
||||
check_include_files(sys/types.h HAVE_SYS_TYPES_H)
|
||||
check_include_files(unistd.h HAVE_UNISTD_H)
|
||||
|
||||
# faad2 uses a relative include for its config.h which breaks under CMake.
|
||||
# We can use target_compile_definitions to pass on the configuration instead.
|
||||
target_compile_definitions(faad2 PRIVATE
|
||||
-DFAAD_VERSION=${FAAD_VERSION}
|
||||
-DPACKAGE_VERSION=\"${FAAD_VERSION}\"
|
||||
-DSTDC_HEADERS
|
||||
-DHAVE_GETPWUID=${HAVE_GETPWUID}
|
||||
-DHAVE_LRINTF=${HAVE_LRINTF}
|
||||
-DHAVE_MEMCPY=${HAVE_MEMCPY}
|
||||
-DHAVE_STRCHR=${HAVE_STRCHR}
|
||||
-DHAVE_STRSEP=${HAVE_STRSEP}
|
||||
-DHAVE_DLFCN_H=${HAVE_DLFCN_H}
|
||||
-DHAVE_ERRNO_H=${HAVE_ERRNO_H}
|
||||
-DHAVE_FLOAT_H=${HAVE_FLOAT_H}
|
||||
-DHAVE_INTTYPES_H=${HAVE_INTTYPES_H}
|
||||
-DHAVE_IOKIT_IOKITLIB_H=${HAVE_IOKIT_IOKITLIB_H}
|
||||
-DHAVE_LIMITS_H=${HAVE_LIMITS_H}
|
||||
-DHAVE_MATHF_H=${HAVE_MATHF_H}
|
||||
-DHAVE_STDINT_H=${HAVE_STDINT_H}
|
||||
-DHAVE_STDIO_H=${HAVE_STDIO_H}
|
||||
-DHAVE_STDLIB_H=${HAVE_STDLIB_H}
|
||||
-DHAVE_STRINGS_H=${HAVE_STRINGS_H}
|
||||
-DHAVE_STRING_H=${HAVE_STRING_H}
|
||||
-DHAVE_SYSFS_LIBSYSFS_H=${HAVE_SYSFS_LIBSYSFS_H}
|
||||
-DHAVE_SYS_STAT_H=${HAVE_SYS_STAT_H}
|
||||
-DHAVE_SYS_TIME_H=${HAVE_SYS_TIME_H}
|
||||
-DHAVE_SYS_TYPES_H=${HAVE_SYS_TYPES_H}
|
||||
-DHAVE_UNISTD_H=${HAVE_UNISTD_H}
|
||||
# Only compile for AAC-LC decoding.
|
||||
-DLC_ONLY_DECODER -DDISABLE_SBR
|
||||
)
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 09b3c850c606e7fedd06597223e54344e8d23c8c
|
|
@ -0,0 +1,9 @@
|
|||
# SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
project(gamemode LANGUAGES CXX C)
|
||||
|
||||
add_library(gamemode include/gamemode_client.h)
|
||||
|
||||
target_include_directories(gamemode PUBLIC include)
|
||||
set_target_properties(gamemode PROPERTIES LINKER_LANGUAGE C)
|
|
@ -0,0 +1,379 @@
|
|||
// SPDX-FileCopyrightText: Copyright 2017-2019 Feral Interactive
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
/*
|
||||
|
||||
Copyright (c) 2017-2019, Feral Interactive
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of Feral Interactive nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software
|
||||
without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) 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 USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
*/
|
||||
#ifndef CLIENT_GAMEMODE_H
|
||||
#define CLIENT_GAMEMODE_H
|
||||
/*
|
||||
* GameMode supports the following client functions
|
||||
* Requests are refcounted in the daemon
|
||||
*
|
||||
* int gamemode_request_start() - Request gamemode starts
|
||||
* 0 if the request was sent successfully
|
||||
* -1 if the request failed
|
||||
*
|
||||
* int gamemode_request_end() - Request gamemode ends
|
||||
* 0 if the request was sent successfully
|
||||
* -1 if the request failed
|
||||
*
|
||||
* GAMEMODE_AUTO can be defined to make the above two functions apply during static init and
|
||||
* destruction, as appropriate. In this configuration, errors will be printed to stderr
|
||||
*
|
||||
* int gamemode_query_status() - Query the current status of gamemode
|
||||
* 0 if gamemode is inactive
|
||||
* 1 if gamemode is active
|
||||
* 2 if gamemode is active and this client is registered
|
||||
* -1 if the query failed
|
||||
*
|
||||
* int gamemode_request_start_for(pid_t pid) - Request gamemode starts for another process
|
||||
* 0 if the request was sent successfully
|
||||
* -1 if the request failed
|
||||
* -2 if the request was rejected
|
||||
*
|
||||
* int gamemode_request_end_for(pid_t pid) - Request gamemode ends for another process
|
||||
* 0 if the request was sent successfully
|
||||
* -1 if the request failed
|
||||
* -2 if the request was rejected
|
||||
*
|
||||
* int gamemode_query_status_for(pid_t pid) - Query status of gamemode for another process
|
||||
* 0 if gamemode is inactive
|
||||
* 1 if gamemode is active
|
||||
* 2 if gamemode is active and this client is registered
|
||||
* -1 if the query failed
|
||||
*
|
||||
* const char* gamemode_error_string() - Get an error string
|
||||
* returns a string describing any of the above errors
|
||||
*
|
||||
* Note: All the above requests can be blocking - dbus requests can and will block while the daemon
|
||||
* handles the request. It is not recommended to make these calls in performance critical code
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
static char internal_gamemode_client_error_string[512] = { 0 };
|
||||
|
||||
/**
|
||||
* Load libgamemode dynamically to dislodge us from most dependencies.
|
||||
* This allows clients to link and/or use this regardless of runtime.
|
||||
* See SDL2 for an example of the reasoning behind this in terms of
|
||||
* dynamic versioning as well.
|
||||
*/
|
||||
static volatile int internal_libgamemode_loaded = 1;
|
||||
|
||||
/* Typedefs for the functions to load */
|
||||
typedef int (*api_call_return_int)(void);
|
||||
typedef const char *(*api_call_return_cstring)(void);
|
||||
typedef int (*api_call_pid_return_int)(pid_t);
|
||||
|
||||
/* Storage for functors */
|
||||
static api_call_return_int REAL_internal_gamemode_request_start = NULL;
|
||||
static api_call_return_int REAL_internal_gamemode_request_end = NULL;
|
||||
static api_call_return_int REAL_internal_gamemode_query_status = NULL;
|
||||
static api_call_return_cstring REAL_internal_gamemode_error_string = NULL;
|
||||
static api_call_pid_return_int REAL_internal_gamemode_request_start_for = NULL;
|
||||
static api_call_pid_return_int REAL_internal_gamemode_request_end_for = NULL;
|
||||
static api_call_pid_return_int REAL_internal_gamemode_query_status_for = NULL;
|
||||
|
||||
/**
|
||||
* Internal helper to perform the symbol binding safely.
|
||||
*
|
||||
* Returns 0 on success and -1 on failure
|
||||
*/
|
||||
__attribute__((always_inline)) static inline int internal_bind_libgamemode_symbol(
|
||||
void *handle, const char *name, void **out_func, size_t func_size, bool required)
|
||||
{
|
||||
void *symbol_lookup = NULL;
|
||||
char *dl_error = NULL;
|
||||
|
||||
/* Safely look up the symbol */
|
||||
symbol_lookup = dlsym(handle, name);
|
||||
dl_error = dlerror();
|
||||
if (required && (dl_error || !symbol_lookup)) {
|
||||
snprintf(internal_gamemode_client_error_string,
|
||||
sizeof(internal_gamemode_client_error_string),
|
||||
"dlsym failed - %s",
|
||||
dl_error);
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Have the symbol correctly, copy it to make it usable */
|
||||
memcpy(out_func, &symbol_lookup, func_size);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads libgamemode and needed functions
|
||||
*
|
||||
* Returns 0 on success and -1 on failure
|
||||
*/
|
||||
__attribute__((always_inline)) static inline int internal_load_libgamemode(void)
|
||||
{
|
||||
/* We start at 1, 0 is a success and -1 is a fail */
|
||||
if (internal_libgamemode_loaded != 1) {
|
||||
return internal_libgamemode_loaded;
|
||||
}
|
||||
|
||||
/* Anonymous struct type to define our bindings */
|
||||
struct binding {
|
||||
const char *name;
|
||||
void **functor;
|
||||
size_t func_size;
|
||||
bool required;
|
||||
} bindings[] = {
|
||||
{ "real_gamemode_request_start",
|
||||
(void **)&REAL_internal_gamemode_request_start,
|
||||
sizeof(REAL_internal_gamemode_request_start),
|
||||
true },
|
||||
{ "real_gamemode_request_end",
|
||||
(void **)&REAL_internal_gamemode_request_end,
|
||||
sizeof(REAL_internal_gamemode_request_end),
|
||||
true },
|
||||
{ "real_gamemode_query_status",
|
||||
(void **)&REAL_internal_gamemode_query_status,
|
||||
sizeof(REAL_internal_gamemode_query_status),
|
||||
false },
|
||||
{ "real_gamemode_error_string",
|
||||
(void **)&REAL_internal_gamemode_error_string,
|
||||
sizeof(REAL_internal_gamemode_error_string),
|
||||
true },
|
||||
{ "real_gamemode_request_start_for",
|
||||
(void **)&REAL_internal_gamemode_request_start_for,
|
||||
sizeof(REAL_internal_gamemode_request_start_for),
|
||||
false },
|
||||
{ "real_gamemode_request_end_for",
|
||||
(void **)&REAL_internal_gamemode_request_end_for,
|
||||
sizeof(REAL_internal_gamemode_request_end_for),
|
||||
false },
|
||||
{ "real_gamemode_query_status_for",
|
||||
(void **)&REAL_internal_gamemode_query_status_for,
|
||||
sizeof(REAL_internal_gamemode_query_status_for),
|
||||
false },
|
||||
};
|
||||
|
||||
void *libgamemode = NULL;
|
||||
|
||||
/* Try and load libgamemode */
|
||||
libgamemode = dlopen("libgamemode.so.0", RTLD_NOW);
|
||||
if (!libgamemode) {
|
||||
/* Attempt to load unversioned library for compatibility with older
|
||||
* versions (as of writing, there are no ABI changes between the two -
|
||||
* this may need to change if ever ABI-breaking changes are made) */
|
||||
libgamemode = dlopen("libgamemode.so", RTLD_NOW);
|
||||
if (!libgamemode) {
|
||||
snprintf(internal_gamemode_client_error_string,
|
||||
sizeof(internal_gamemode_client_error_string),
|
||||
"dlopen failed - %s",
|
||||
dlerror());
|
||||
internal_libgamemode_loaded = -1;
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Attempt to bind all symbols */
|
||||
for (size_t i = 0; i < sizeof(bindings) / sizeof(bindings[0]); i++) {
|
||||
struct binding *binder = &bindings[i];
|
||||
|
||||
if (internal_bind_libgamemode_symbol(libgamemode,
|
||||
binder->name,
|
||||
binder->functor,
|
||||
binder->func_size,
|
||||
binder->required)) {
|
||||
internal_libgamemode_loaded = -1;
|
||||
return -1;
|
||||
};
|
||||
}
|
||||
|
||||
/* Success */
|
||||
internal_libgamemode_loaded = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the real libgamemode
|
||||
*/
|
||||
__attribute__((always_inline)) static inline const char *gamemode_error_string(void)
|
||||
{
|
||||
/* If we fail to load the system gamemode, or we have an error string already, return our error
|
||||
* string instead of diverting to the system version */
|
||||
if (internal_load_libgamemode() < 0 || internal_gamemode_client_error_string[0] != '\0') {
|
||||
return internal_gamemode_client_error_string;
|
||||
}
|
||||
|
||||
/* Assert for static analyser that the function is not NULL */
|
||||
assert(REAL_internal_gamemode_error_string != NULL);
|
||||
|
||||
return REAL_internal_gamemode_error_string();
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect to the real libgamemode
|
||||
* Allow automatically requesting game mode
|
||||
* Also prints errors as they happen.
|
||||
*/
|
||||
#ifdef GAMEMODE_AUTO
|
||||
__attribute__((constructor))
|
||||
#else
|
||||
__attribute__((always_inline)) static inline
|
||||
#endif
|
||||
int gamemode_request_start(void)
|
||||
{
|
||||
/* Need to load gamemode */
|
||||
if (internal_load_libgamemode() < 0) {
|
||||
#ifdef GAMEMODE_AUTO
|
||||
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Assert for static analyser that the function is not NULL */
|
||||
assert(REAL_internal_gamemode_request_start != NULL);
|
||||
|
||||
if (REAL_internal_gamemode_request_start() < 0) {
|
||||
#ifdef GAMEMODE_AUTO
|
||||
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Redirect to the real libgamemode */
|
||||
#ifdef GAMEMODE_AUTO
|
||||
__attribute__((destructor))
|
||||
#else
|
||||
__attribute__((always_inline)) static inline
|
||||
#endif
|
||||
int gamemode_request_end(void)
|
||||
{
|
||||
/* Need to load gamemode */
|
||||
if (internal_load_libgamemode() < 0) {
|
||||
#ifdef GAMEMODE_AUTO
|
||||
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* Assert for static analyser that the function is not NULL */
|
||||
assert(REAL_internal_gamemode_request_end != NULL);
|
||||
|
||||
if (REAL_internal_gamemode_request_end() < 0) {
|
||||
#ifdef GAMEMODE_AUTO
|
||||
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
|
||||
#endif
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Redirect to the real libgamemode */
|
||||
__attribute__((always_inline)) static inline int gamemode_query_status(void)
|
||||
{
|
||||
/* Need to load gamemode */
|
||||
if (internal_load_libgamemode() < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (REAL_internal_gamemode_query_status == NULL) {
|
||||
snprintf(internal_gamemode_client_error_string,
|
||||
sizeof(internal_gamemode_client_error_string),
|
||||
"gamemode_query_status missing (older host?)");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return REAL_internal_gamemode_query_status();
|
||||
}
|
||||
|
||||
/* Redirect to the real libgamemode */
|
||||
__attribute__((always_inline)) static inline int gamemode_request_start_for(pid_t pid)
|
||||
{
|
||||
/* Need to load gamemode */
|
||||
if (internal_load_libgamemode() < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (REAL_internal_gamemode_request_start_for == NULL) {
|
||||
snprintf(internal_gamemode_client_error_string,
|
||||
sizeof(internal_gamemode_client_error_string),
|
||||
"gamemode_request_start_for missing (older host?)");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return REAL_internal_gamemode_request_start_for(pid);
|
||||
}
|
||||
|
||||
/* Redirect to the real libgamemode */
|
||||
__attribute__((always_inline)) static inline int gamemode_request_end_for(pid_t pid)
|
||||
{
|
||||
/* Need to load gamemode */
|
||||
if (internal_load_libgamemode() < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (REAL_internal_gamemode_request_end_for == NULL) {
|
||||
snprintf(internal_gamemode_client_error_string,
|
||||
sizeof(internal_gamemode_client_error_string),
|
||||
"gamemode_request_end_for missing (older host?)");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return REAL_internal_gamemode_request_end_for(pid);
|
||||
}
|
||||
|
||||
/* Redirect to the real libgamemode */
|
||||
__attribute__((always_inline)) static inline int gamemode_query_status_for(pid_t pid)
|
||||
{
|
||||
/* Need to load gamemode */
|
||||
if (internal_load_libgamemode() < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (REAL_internal_gamemode_query_status_for == NULL) {
|
||||
snprintf(internal_gamemode_client_error_string,
|
||||
sizeof(internal_gamemode_client_error_string),
|
||||
"gamemode_query_status_for missing (older host?)");
|
||||
return -1;
|
||||
}
|
||||
|
||||
return REAL_internal_gamemode_query_status_for(pid);
|
||||
}
|
||||
|
||||
#endif // CLIENT_GAMEMODE_H
|
|
@ -3,3 +3,9 @@ These files were generated by the [glad](https://github.com/Dav1dde/glad) OpenGL
|
|||
```
|
||||
python -m glad --profile core --out-path glad/ --api "gl=4.3,gles2=3.2" --generator=c
|
||||
```
|
||||
|
||||
You can also generate the source using [this site](https://glad.dav1d.de/):
|
||||
1. Select '4.3' for GL, '3.2' for GLES2, and 'Core' for Profile.
|
||||
2. Input the currently supported extensions from [here](https://github.com/citra-emu/citra/blob/master/externals/glad/include/glad/glad.h#L9), plus any new required extensions.
|
||||
3. Click Generate and download the generated source zip.
|
||||
4. Unzip the new source over the current glad source files.
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
|
||||
OpenGL, OpenGL ES loader generated by glad 0.1.34 on Sat Aug 26 18:38:43 2023.
|
||||
OpenGL, OpenGL ES loader generated by glad 0.1.36 on Fri Nov 10 04:24:01 2023.
|
||||
|
||||
Language/Generator: C/C++
|
||||
Specification: gl
|
||||
|
@ -10,6 +10,7 @@
|
|||
GL_AMD_blend_minmax_factor,
|
||||
GL_ARB_buffer_storage,
|
||||
GL_ARB_clear_texture,
|
||||
GL_ARB_fragment_shader_interlock,
|
||||
GL_ARB_get_texture_sub_image,
|
||||
GL_ARB_texture_compression_bptc,
|
||||
GL_ARM_shader_framebuffer_fetch,
|
||||
|
@ -17,16 +18,18 @@
|
|||
GL_EXT_clip_cull_distance,
|
||||
GL_EXT_shader_framebuffer_fetch,
|
||||
GL_EXT_texture_compression_s3tc,
|
||||
GL_NV_blend_minmax_factor
|
||||
GL_INTEL_fragment_shader_ordering,
|
||||
GL_NV_blend_minmax_factor,
|
||||
GL_NV_fragment_shader_interlock
|
||||
Loader: True
|
||||
Local files: False
|
||||
Omit khrplatform: False
|
||||
Reproducible: False
|
||||
|
||||
Commandline:
|
||||
--profile="core" --api="gl=4.3,gles2=3.2" --generator="c" --spec="gl" --extensions="GL_AMD_blend_minmax_factor,GL_ARB_buffer_storage,GL_ARB_clear_texture,GL_ARB_get_texture_sub_image,GL_ARB_texture_compression_bptc,GL_ARM_shader_framebuffer_fetch,GL_EXT_buffer_storage,GL_EXT_clip_cull_distance,GL_EXT_shader_framebuffer_fetch,GL_EXT_texture_compression_s3tc,GL_NV_blend_minmax_factor"
|
||||
--profile="core" --api="gl=4.3,gles2=3.2" --generator="c" --spec="gl" --extensions="GL_AMD_blend_minmax_factor,GL_ARB_buffer_storage,GL_ARB_clear_texture,GL_ARB_fragment_shader_interlock,GL_ARB_get_texture_sub_image,GL_ARB_texture_compression_bptc,GL_ARM_shader_framebuffer_fetch,GL_EXT_buffer_storage,GL_EXT_clip_cull_distance,GL_EXT_shader_framebuffer_fetch,GL_EXT_texture_compression_s3tc,GL_INTEL_fragment_shader_ordering,GL_NV_blend_minmax_factor,GL_NV_fragment_shader_interlock"
|
||||
Online:
|
||||
https://glad.dav1d.de/#profile=core&language=c&specification=gl&loader=on&api=gl%3D4.3&api=gles2%3D3.2&extensions=GL_AMD_blend_minmax_factor&extensions=GL_ARB_buffer_storage&extensions=GL_ARB_clear_texture&extensions=GL_ARB_get_texture_sub_image&extensions=GL_ARB_texture_compression_bptc&extensions=GL_ARM_shader_framebuffer_fetch&extensions=GL_EXT_buffer_storage&extensions=GL_EXT_clip_cull_distance&extensions=GL_EXT_shader_framebuffer_fetch&extensions=GL_EXT_texture_compression_s3tc&extensions=GL_NV_blend_minmax_factor
|
||||
https://glad.dav1d.de/#profile=core&language=c&specification=gl&loader=on&api=gl%3D4.3&api=gles2%3D3.2&extensions=GL_AMD_blend_minmax_factor&extensions=GL_ARB_buffer_storage&extensions=GL_ARB_clear_texture&extensions=GL_ARB_fragment_shader_interlock&extensions=GL_ARB_get_texture_sub_image&extensions=GL_ARB_texture_compression_bptc&extensions=GL_ARM_shader_framebuffer_fetch&extensions=GL_EXT_buffer_storage&extensions=GL_EXT_clip_cull_distance&extensions=GL_EXT_shader_framebuffer_fetch&extensions=GL_EXT_texture_compression_s3tc&extensions=GL_INTEL_fragment_shader_ordering&extensions=GL_NV_blend_minmax_factor&extensions=GL_NV_fragment_shader_interlock
|
||||
*/
|
||||
|
||||
|
||||
|
@ -3384,6 +3387,10 @@ typedef void (APIENTRYP PFNGLCLEARTEXSUBIMAGEPROC)(GLuint texture, GLint level,
|
|||
GLAPI PFNGLCLEARTEXSUBIMAGEPROC glad_glClearTexSubImage;
|
||||
#define glClearTexSubImage glad_glClearTexSubImage
|
||||
#endif
|
||||
#ifndef GL_ARB_fragment_shader_interlock
|
||||
#define GL_ARB_fragment_shader_interlock 1
|
||||
GLAPI int GLAD_GL_ARB_fragment_shader_interlock;
|
||||
#endif
|
||||
#ifndef GL_ARB_get_texture_sub_image
|
||||
#define GL_ARB_get_texture_sub_image 1
|
||||
GLAPI int GLAD_GL_ARB_get_texture_sub_image;
|
||||
|
@ -3406,10 +3413,18 @@ GLAPI int GLAD_GL_EXT_shader_framebuffer_fetch;
|
|||
#define GL_EXT_texture_compression_s3tc 1
|
||||
GLAPI int GLAD_GL_EXT_texture_compression_s3tc;
|
||||
#endif
|
||||
#ifndef GL_INTEL_fragment_shader_ordering
|
||||
#define GL_INTEL_fragment_shader_ordering 1
|
||||
GLAPI int GLAD_GL_INTEL_fragment_shader_ordering;
|
||||
#endif
|
||||
#ifndef GL_NV_blend_minmax_factor
|
||||
#define GL_NV_blend_minmax_factor 1
|
||||
GLAPI int GLAD_GL_NV_blend_minmax_factor;
|
||||
#endif
|
||||
#ifndef GL_NV_fragment_shader_interlock
|
||||
#define GL_NV_fragment_shader_interlock 1
|
||||
GLAPI int GLAD_GL_NV_fragment_shader_interlock;
|
||||
#endif
|
||||
#ifndef GL_ARM_shader_framebuffer_fetch
|
||||
#define GL_ARM_shader_framebuffer_fetch 1
|
||||
GLAPI int GLAD_GL_ARM_shader_framebuffer_fetch;
|
||||
|
@ -3437,6 +3452,10 @@ GLAPI int GLAD_GL_EXT_texture_compression_s3tc;
|
|||
#define GL_NV_blend_minmax_factor 1
|
||||
GLAPI int GLAD_GL_NV_blend_minmax_factor;
|
||||
#endif
|
||||
#ifndef GL_NV_fragment_shader_interlock
|
||||
#define GL_NV_fragment_shader_interlock 1
|
||||
GLAPI int GLAD_GL_NV_fragment_shader_interlock;
|
||||
#endif
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
|
||||
OpenGL, OpenGL ES loader generated by glad 0.1.34 on Sat Aug 26 18:38:43 2023.
|
||||
OpenGL, OpenGL ES loader generated by glad 0.1.36 on Fri Nov 10 04:24:01 2023.
|
||||
|
||||
Language/Generator: C/C++
|
||||
Specification: gl
|
||||
|
@ -10,6 +10,7 @@
|
|||
GL_AMD_blend_minmax_factor,
|
||||
GL_ARB_buffer_storage,
|
||||
GL_ARB_clear_texture,
|
||||
GL_ARB_fragment_shader_interlock,
|
||||
GL_ARB_get_texture_sub_image,
|
||||
GL_ARB_texture_compression_bptc,
|
||||
GL_ARM_shader_framebuffer_fetch,
|
||||
|
@ -17,16 +18,18 @@
|
|||
GL_EXT_clip_cull_distance,
|
||||
GL_EXT_shader_framebuffer_fetch,
|
||||
GL_EXT_texture_compression_s3tc,
|
||||
GL_NV_blend_minmax_factor
|
||||
GL_INTEL_fragment_shader_ordering,
|
||||
GL_NV_blend_minmax_factor,
|
||||
GL_NV_fragment_shader_interlock
|
||||
Loader: True
|
||||
Local files: False
|
||||
Omit khrplatform: False
|
||||
Reproducible: False
|
||||
|
||||
Commandline:
|
||||
--profile="core" --api="gl=4.3,gles2=3.2" --generator="c" --spec="gl" --extensions="GL_AMD_blend_minmax_factor,GL_ARB_buffer_storage,GL_ARB_clear_texture,GL_ARB_get_texture_sub_image,GL_ARB_texture_compression_bptc,GL_ARM_shader_framebuffer_fetch,GL_EXT_buffer_storage,GL_EXT_clip_cull_distance,GL_EXT_shader_framebuffer_fetch,GL_EXT_texture_compression_s3tc,GL_NV_blend_minmax_factor"
|
||||
--profile="core" --api="gl=4.3,gles2=3.2" --generator="c" --spec="gl" --extensions="GL_AMD_blend_minmax_factor,GL_ARB_buffer_storage,GL_ARB_clear_texture,GL_ARB_fragment_shader_interlock,GL_ARB_get_texture_sub_image,GL_ARB_texture_compression_bptc,GL_ARM_shader_framebuffer_fetch,GL_EXT_buffer_storage,GL_EXT_clip_cull_distance,GL_EXT_shader_framebuffer_fetch,GL_EXT_texture_compression_s3tc,GL_INTEL_fragment_shader_ordering,GL_NV_blend_minmax_factor,GL_NV_fragment_shader_interlock"
|
||||
Online:
|
||||
https://glad.dav1d.de/#profile=core&language=c&specification=gl&loader=on&api=gl%3D4.3&api=gles2%3D3.2&extensions=GL_AMD_blend_minmax_factor&extensions=GL_ARB_buffer_storage&extensions=GL_ARB_clear_texture&extensions=GL_ARB_get_texture_sub_image&extensions=GL_ARB_texture_compression_bptc&extensions=GL_ARM_shader_framebuffer_fetch&extensions=GL_EXT_buffer_storage&extensions=GL_EXT_clip_cull_distance&extensions=GL_EXT_shader_framebuffer_fetch&extensions=GL_EXT_texture_compression_s3tc&extensions=GL_NV_blend_minmax_factor
|
||||
https://glad.dav1d.de/#profile=core&language=c&specification=gl&loader=on&api=gl%3D4.3&api=gles2%3D3.2&extensions=GL_AMD_blend_minmax_factor&extensions=GL_ARB_buffer_storage&extensions=GL_ARB_clear_texture&extensions=GL_ARB_fragment_shader_interlock&extensions=GL_ARB_get_texture_sub_image&extensions=GL_ARB_texture_compression_bptc&extensions=GL_ARM_shader_framebuffer_fetch&extensions=GL_EXT_buffer_storage&extensions=GL_EXT_clip_cull_distance&extensions=GL_EXT_shader_framebuffer_fetch&extensions=GL_EXT_texture_compression_s3tc&extensions=GL_INTEL_fragment_shader_ordering&extensions=GL_NV_blend_minmax_factor&extensions=GL_NV_fragment_shader_interlock
|
||||
*/
|
||||
|
||||
#include <stdio.h>
|
||||
|
@ -860,6 +863,7 @@ PFNGLWAITSYNCPROC glad_glWaitSync = NULL;
|
|||
int GLAD_GL_AMD_blend_minmax_factor = 0;
|
||||
int GLAD_GL_ARB_buffer_storage = 0;
|
||||
int GLAD_GL_ARB_clear_texture = 0;
|
||||
int GLAD_GL_ARB_fragment_shader_interlock = 0;
|
||||
int GLAD_GL_ARB_get_texture_sub_image = 0;
|
||||
int GLAD_GL_ARB_texture_compression_bptc = 0;
|
||||
int GLAD_GL_ARM_shader_framebuffer_fetch = 0;
|
||||
|
@ -867,7 +871,9 @@ int GLAD_GL_EXT_buffer_storage = 0;
|
|||
int GLAD_GL_EXT_clip_cull_distance = 0;
|
||||
int GLAD_GL_EXT_shader_framebuffer_fetch = 0;
|
||||
int GLAD_GL_EXT_texture_compression_s3tc = 0;
|
||||
int GLAD_GL_INTEL_fragment_shader_ordering = 0;
|
||||
int GLAD_GL_NV_blend_minmax_factor = 0;
|
||||
int GLAD_GL_NV_fragment_shader_interlock = 0;
|
||||
PFNGLBUFFERSTORAGEPROC glad_glBufferStorage = NULL;
|
||||
PFNGLCLEARTEXIMAGEPROC glad_glClearTexImage = NULL;
|
||||
PFNGLCLEARTEXSUBIMAGEPROC glad_glClearTexSubImage = NULL;
|
||||
|
@ -1509,11 +1515,14 @@ static int find_extensionsGL(void) {
|
|||
GLAD_GL_AMD_blend_minmax_factor = has_ext("GL_AMD_blend_minmax_factor");
|
||||
GLAD_GL_ARB_buffer_storage = has_ext("GL_ARB_buffer_storage");
|
||||
GLAD_GL_ARB_clear_texture = has_ext("GL_ARB_clear_texture");
|
||||
GLAD_GL_ARB_fragment_shader_interlock = has_ext("GL_ARB_fragment_shader_interlock");
|
||||
GLAD_GL_ARB_get_texture_sub_image = has_ext("GL_ARB_get_texture_sub_image");
|
||||
GLAD_GL_ARB_texture_compression_bptc = has_ext("GL_ARB_texture_compression_bptc");
|
||||
GLAD_GL_EXT_shader_framebuffer_fetch = has_ext("GL_EXT_shader_framebuffer_fetch");
|
||||
GLAD_GL_EXT_texture_compression_s3tc = has_ext("GL_EXT_texture_compression_s3tc");
|
||||
GLAD_GL_INTEL_fragment_shader_ordering = has_ext("GL_INTEL_fragment_shader_ordering");
|
||||
GLAD_GL_NV_blend_minmax_factor = has_ext("GL_NV_blend_minmax_factor");
|
||||
GLAD_GL_NV_fragment_shader_interlock = has_ext("GL_NV_fragment_shader_interlock");
|
||||
free_exts();
|
||||
return 1;
|
||||
}
|
||||
|
@ -1988,6 +1997,7 @@ static int find_extensionsGLES2(void) {
|
|||
GLAD_GL_EXT_shader_framebuffer_fetch = has_ext("GL_EXT_shader_framebuffer_fetch");
|
||||
GLAD_GL_EXT_texture_compression_s3tc = has_ext("GL_EXT_texture_compression_s3tc");
|
||||
GLAD_GL_NV_blend_minmax_factor = has_ext("GL_NV_blend_minmax_factor");
|
||||
GLAD_GL_NV_fragment_shader_interlock = has_ext("GL_NV_fragment_shader_interlock");
|
||||
free_exts();
|
||||
return 1;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1 @@
|
|||
Subproject commit 6b1d57ea7ed4882d32a91eeaa6557b0ecb4da152
|
|
@ -1 +1 @@
|
|||
Subproject commit 85c2334e92e215cce34e8e0ed8b2dce4700f4a50
|
||||
Subproject commit 5a5c9a643484d888873e32c5d7d484fae8e71d3d
|
BIN
keys.tar.enc
BIN
keys.tar.enc
Binary file not shown.
|
@ -51,6 +51,10 @@ if (MSVC)
|
|||
/Zc:throwingNew
|
||||
/GT
|
||||
|
||||
# Some flags for more deterministic builds, to aid caching.
|
||||
/experimental:deterministic
|
||||
/d1trimfile:"${CMAKE_SOURCE_DIR}"
|
||||
|
||||
# External headers diagnostics
|
||||
/experimental:external # Enables the external headers options. This option isn't required in Visual Studio 2019 version 16.10 and later
|
||||
/external:anglebrackets # Treats all headers included by #include <header>, where the header file is enclosed in angle brackets (< >), as external headers
|
||||
|
@ -87,7 +91,8 @@ if (MSVC)
|
|||
|
||||
# Since MSVC's debugging information is not very deterministic, so we have to disable it
|
||||
# when using ccache or other caching tools
|
||||
if (CITRA_USE_CCACHE OR CITRA_USE_PRECOMPILED_HEADERS)
|
||||
if (CMAKE_C_COMPILER_LAUNCHER STREQUAL "ccache" OR CMAKE_CXX_COMPILER_LAUNCHER STREQUAL "ccache"
|
||||
OR CITRA_USE_PRECOMPILED_HEADERS)
|
||||
# Precompiled headers are deleted if not using /Z7. See https://github.com/nanoant/CMakePCHCompiler/issues/21
|
||||
add_compile_options(/Z7)
|
||||
else()
|
||||
|
@ -98,13 +103,23 @@ if (MSVC)
|
|||
add_compile_options("$<$<CONFIG:Release>:/GS->")
|
||||
|
||||
set(CMAKE_EXE_LINKER_FLAGS_DEBUG "/DEBUG /MANIFEST:NO" CACHE STRING "" FORCE)
|
||||
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "/DEBUG /MANIFEST:NO /INCREMENTAL:NO /OPT:REF,ICF" CACHE STRING "" FORCE)
|
||||
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "/DEBUG /MANIFEST:NO /INCREMENTAL:NO /OPT:REF,ICF /PDBALTPATH:%_PDB%" CACHE STRING "" FORCE)
|
||||
else()
|
||||
add_compile_options(
|
||||
-Wall
|
||||
-Wno-attributes
|
||||
# In case a flag isn't supported on e.g. a certain architecture, don't error.
|
||||
-Wno-unused-command-line-argument
|
||||
# Build fortification options
|
||||
-Wp,-D_GLIBCXX_ASSERTIONS
|
||||
-fstack-protector-strong
|
||||
-fstack-clash-protection
|
||||
)
|
||||
|
||||
if (NOT CMAKE_BUILD_TYPE STREQUAL Debug)
|
||||
# _FORTIFY_SOURCE can't be used without optimizations.
|
||||
add_compile_options(-Wp,-D_FORTIFY_SOURCE=2)
|
||||
endif()
|
||||
|
||||
if (CITRA_WARNINGS_AS_ERRORS)
|
||||
add_compile_options(-Werror)
|
||||
endif()
|
||||
|
@ -113,6 +128,13 @@ else()
|
|||
add_compile_options("-stdlib=libc++")
|
||||
endif()
|
||||
|
||||
if (CMAKE_CXX_COMPILER_ID STREQUAL GNU)
|
||||
# GCC may warn when it ignores attributes like maybe_unused,
|
||||
# which is a problem for older versions (e.g. GCC 11).
|
||||
add_compile_options("-Wno-attributes")
|
||||
add_compile_options("-Wno-interference-size")
|
||||
endif()
|
||||
|
||||
if (MINGW)
|
||||
add_definitions(-DMINGW_HAS_SECURE_API)
|
||||
if (COMPILE_WITH_DWARF)
|
||||
|
@ -137,6 +159,16 @@ else()
|
|||
endif()
|
||||
endif()
|
||||
|
||||
if(ENABLE_SOFTWARE_RENDERER)
|
||||
add_compile_definitions(ENABLE_SOFTWARE_RENDERER)
|
||||
endif()
|
||||
if(ENABLE_OPENGL)
|
||||
add_compile_definitions(ENABLE_OPENGL)
|
||||
endif()
|
||||
if(ENABLE_VULKAN)
|
||||
add_compile_definitions(ENABLE_VULKAN)
|
||||
endif()
|
||||
|
||||
add_subdirectory(common)
|
||||
add_subdirectory(core)
|
||||
add_subdirectory(video_core)
|
||||
|
|
|
@ -2,15 +2,18 @@
|
|||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
import android.databinding.tool.ext.capitalizeUS
|
||||
import de.undercouch.gradle.tasks.download.Download
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("de.undercouch.download") version "5.5.0"
|
||||
id("kotlin-parcelize")
|
||||
kotlin("plugin.serialization") version "1.9.22"
|
||||
id("androidx.navigation.safeargs.kotlin")
|
||||
}
|
||||
|
||||
import android.databinding.tool.ext.capitalizeUS
|
||||
import de.undercouch.gradle.tasks.download.Download
|
||||
|
||||
/**
|
||||
* Use the number of seconds/10 since Jan 1 2016 as the versionCode.
|
||||
* This lets us upload a new build at most every 10 seconds for the
|
||||
|
@ -25,8 +28,8 @@ val downloadedJniLibsPath = "${buildDir}/downloadedJniLibs"
|
|||
android {
|
||||
namespace = "org.citra.citra_emu"
|
||||
|
||||
compileSdkVersion = "android-33"
|
||||
ndkVersion = "25.2.9519653"
|
||||
compileSdkVersion = "android-34"
|
||||
ndkVersion = "26.1.10909125"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
|
@ -37,6 +40,15 @@ android {
|
|||
jvmTarget = "17"
|
||||
}
|
||||
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
packaging {
|
||||
// This is necessary for libadrenotools custom driver loading
|
||||
jniLibs.useLegacyPackaging = true
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
}
|
||||
|
@ -51,7 +63,7 @@ android {
|
|||
// TODO If this is ever modified, change application_id in strings.xml
|
||||
applicationId = "org.citra.citra_emu"
|
||||
minSdk = 28
|
||||
targetSdk = 33
|
||||
targetSdk = 34
|
||||
versionCode = autoVersion
|
||||
versionName = getGitVersion()
|
||||
|
||||
|
@ -69,6 +81,9 @@ android {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
buildConfigField("String", "GIT_HASH", "\"${getGitHash()}\"")
|
||||
buildConfigField("String", "BRANCH", "\"${getBranch()}\"")
|
||||
}
|
||||
|
||||
val keystoreFile = System.getenv("ANDROID_KEYSTORE_FILE")
|
||||
|
@ -92,6 +107,12 @@ android {
|
|||
} else {
|
||||
signingConfigs.getByName("debug")
|
||||
}
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
|
||||
// builds a release build that doesn't need signing
|
||||
|
@ -101,9 +122,15 @@ android {
|
|||
applicationIdSuffix = ".debug"
|
||||
versionNameSuffix = "-debug"
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
isMinifyEnabled = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
isDebuggable = true
|
||||
isJniDebuggable = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
isDefault = true
|
||||
}
|
||||
|
||||
// Signed by debug key disallowing distribution on Play Store.
|
||||
|
@ -145,28 +172,24 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation("androidx.activity:activity-ktx:1.7.2")
|
||||
implementation("androidx.fragment:fragment-ktx:1.6.0")
|
||||
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||
implementation("androidx.activity:activity-ktx:1.8.2")
|
||||
implementation("androidx.fragment:fragment-ktx:1.6.2")
|
||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||
implementation("androidx.documentfile:documentfile:1.0.1")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
|
||||
implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
|
||||
implementation("com.google.android.material:material:1.9.0")
|
||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||
implementation("androidx.work:work-runtime:2.8.1")
|
||||
|
||||
// For loading huge screenshots from the disk.
|
||||
implementation("com.squareup.picasso:picasso:2.71828")
|
||||
|
||||
// Allows FRP-style asynchronous operations in Android.
|
||||
implementation("io.reactivex:rxandroid:1.2.1")
|
||||
|
||||
implementation("androidx.work:work-runtime:2.9.0")
|
||||
implementation("org.ini4j:ini4j:0.5.4")
|
||||
implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
|
||||
// Please don't upgrade the billing library as the newer version is not GPL-compatible
|
||||
implementation("com.android.billingclient:billing:2.0.3")
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:2.7.6")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:2.7.6")
|
||||
implementation("info.debatty:java-string-similarity:2.0.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
|
||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||
implementation("io.coil-kt:coil:2.5.0")
|
||||
}
|
||||
|
||||
// Download Vulkan Validation Layers from the KhronosGroup GitHub.
|
||||
|
@ -216,6 +239,34 @@ fun getGitVersion(): String {
|
|||
return versionName
|
||||
}
|
||||
|
||||
fun getGitHash(): String =
|
||||
runGitCommand(ProcessBuilder("git", "rev-parse", "--short", "HEAD")) ?: "dummy-hash"
|
||||
|
||||
fun getBranch(): String =
|
||||
runGitCommand(ProcessBuilder("git", "rev-parse", "--abbrev-ref", "HEAD")) ?: "dummy-branch"
|
||||
|
||||
fun runGitCommand(command: ProcessBuilder) : String? {
|
||||
try {
|
||||
command.directory(project.rootDir)
|
||||
val process = command.start()
|
||||
val inputStream = process.inputStream
|
||||
val errorStream = process.errorStream
|
||||
process.waitFor()
|
||||
|
||||
return if (process.exitValue() == 0) {
|
||||
inputStream.bufferedReader()
|
||||
.use { it.readText().trim() } // return the value of gitHash
|
||||
} else {
|
||||
val errorMessage = errorStream.bufferedReader().use { it.readText().trim() }
|
||||
logger.error("Error running git command: $errorMessage")
|
||||
return null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.error("$e: Cannot find git")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
android.applicationVariants.configureEach {
|
||||
val variant = this
|
||||
val capitalizedName = variant.name.capitalizeUS()
|
||||
|
|
|
@ -1,21 +1,25 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
# Copyright 2023 Citra Emulator Project
|
||||
# Licensed under GPLv2 or any later version
|
||||
# Refer to the license.txt file included.
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
# To get usable stack traces
|
||||
-dontobfuscate
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
# Prevents crashing when using Wini
|
||||
-keep class org.ini4j.spi.IniParser
|
||||
-keep class org.ini4j.spi.IniBuilder
|
||||
-keep class org.ini4j.spi.IniFormatter
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
# Suppress warnings for R8
|
||||
-dontwarn org.bouncycastle.jsse.BCSSLParameters
|
||||
-dontwarn org.bouncycastle.jsse.BCSSLSocket
|
||||
-dontwarn org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
|
||||
-dontwarn org.conscrypt.Conscrypt$Version
|
||||
-dontwarn org.conscrypt.Conscrypt
|
||||
-dontwarn org.conscrypt.ConscryptHostnameVerifier
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLParameters
|
||||
-dontwarn org.openjsse.javax.net.ssl.SSLSocket
|
||||
-dontwarn org.openjsse.net.ssl.OpenJSSE
|
||||
-dontwarn java.beans.Introspector
|
||||
-dontwarn java.beans.VetoableChangeListener
|
||||
-dontwarn java.beans.VetoableChangeSupport
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<application
|
||||
|
@ -41,11 +42,13 @@
|
|||
android:banner="@mipmap/ic_launcher"
|
||||
android:requestLegacyExternalStorage="true">
|
||||
|
||||
<meta-data android:name="android.game_mode_config"
|
||||
android:resource="@xml/game_mode_config" />
|
||||
|
||||
<activity
|
||||
android:name="org.citra.citra_emu.ui.main.MainActivity"
|
||||
android:theme="@style/Theme.Citra.Splash.Main"
|
||||
android:exported="true"
|
||||
android:resizeableActivity="false">
|
||||
android:exported="true">
|
||||
|
||||
<!-- This intentfilter marks this Activity as the one that gets launched from Home screen. -->
|
||||
<intent-filter>
|
||||
|
@ -64,25 +67,28 @@
|
|||
<activity
|
||||
android:name="org.citra.citra_emu.activities.EmulationActivity"
|
||||
android:exported="true"
|
||||
android:resizeableActivity="false"
|
||||
android:theme="@style/Theme.Citra.Main"
|
||||
android:launchMode="singleTop"/>
|
||||
android:launchMode="singleTop">
|
||||
|
||||
<service android:name="org.citra.citra_emu.utils.ForegroundService"/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data
|
||||
android:mimeType="application/octet-stream"
|
||||
android:scheme="content" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<service android:name="org.citra.citra_emu.utils.ForegroundService" android:foregroundServiceType="specialUse">
|
||||
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="Keep emulation running in background"/>
|
||||
</service>
|
||||
|
||||
<activity
|
||||
android:name="org.citra.citra_emu.features.cheats.ui.CheatsActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.Citra.Main"
|
||||
android:label="@string/cheats"/>
|
||||
|
||||
|
||||
<provider
|
||||
android:name="org.citra.citra_emu.model.GameProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
// Copyright 2019 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu;
|
||||
|
||||
import android.app.Application;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import org.citra.citra_emu.model.GameDatabase;
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization;
|
||||
import org.citra.citra_emu.utils.DocumentsTree;
|
||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
||||
|
||||
public class CitraApplication extends Application {
|
||||
public static GameDatabase databaseHelper;
|
||||
public static DocumentsTree documentsTree;
|
||||
private static CitraApplication application;
|
||||
|
||||
private void createNotificationChannel() {
|
||||
// Create the NotificationChannel, but only on API 26+ because
|
||||
// the NotificationChannel class is new and not in the support library
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
||||
{
|
||||
// General notification
|
||||
CharSequence name = getString(R.string.app_notification_channel_name);
|
||||
String description = getString(R.string.app_notification_channel_description);
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
getString(R.string.app_notification_channel_id), name,
|
||||
NotificationManager.IMPORTANCE_LOW);
|
||||
channel.setDescription(description);
|
||||
channel.setSound(null, null);
|
||||
channel.setVibrationPattern(null);
|
||||
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
{
|
||||
// CIA Install notifications
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
getString(R.string.cia_install_notification_channel_id),
|
||||
getString(R.string.cia_install_notification_channel_name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT);
|
||||
channel.setDescription(getString(R.string.cia_install_notification_channel_description));
|
||||
channel.setSound(null, null);
|
||||
channel.setVibrationPattern(null);
|
||||
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
application = this;
|
||||
documentsTree = new DocumentsTree();
|
||||
|
||||
if (PermissionsHandler.hasWriteAccess(getApplicationContext())) {
|
||||
DirectoryInitialization.start(getApplicationContext());
|
||||
}
|
||||
|
||||
NativeLibrary.LogDeviceInfo();
|
||||
createNotificationChannel();
|
||||
|
||||
databaseHelper = new GameDatabase(this);
|
||||
}
|
||||
|
||||
public static Context getAppContext() {
|
||||
return application.getApplicationContext();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,81 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import org.citra.citra_emu.utils.DirectoryInitialization
|
||||
import org.citra.citra_emu.utils.DocumentsTree
|
||||
import org.citra.citra_emu.utils.GpuDriverHelper
|
||||
import org.citra.citra_emu.utils.PermissionsHandler
|
||||
import org.citra.citra_emu.utils.Log
|
||||
import org.citra.citra_emu.utils.MemoryUtil
|
||||
|
||||
class CitraApplication : Application() {
|
||||
private fun createNotificationChannel() {
|
||||
with(getSystemService(NotificationManager::class.java)) {
|
||||
// General notification
|
||||
val name: CharSequence = getString(R.string.app_notification_channel_name)
|
||||
val description = getString(R.string.app_notification_channel_description)
|
||||
val generalChannel = NotificationChannel(
|
||||
getString(R.string.app_notification_channel_id),
|
||||
name,
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
)
|
||||
generalChannel.description = description
|
||||
generalChannel.setSound(null, null)
|
||||
generalChannel.vibrationPattern = null
|
||||
createNotificationChannel(generalChannel)
|
||||
|
||||
// CIA Install notifications
|
||||
val ciaChannel = NotificationChannel(
|
||||
getString(R.string.cia_install_notification_channel_id),
|
||||
getString(R.string.cia_install_notification_channel_name),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
ciaChannel.description =
|
||||
getString(R.string.cia_install_notification_channel_description)
|
||||
ciaChannel.setSound(null, null)
|
||||
ciaChannel.vibrationPattern = null
|
||||
createNotificationChannel(ciaChannel)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
application = this
|
||||
documentsTree = DocumentsTree()
|
||||
if (PermissionsHandler.hasWriteAccess(applicationContext)) {
|
||||
DirectoryInitialization.start()
|
||||
}
|
||||
|
||||
NativeLibrary.logDeviceInfo()
|
||||
logDeviceInfo()
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
||||
fun logDeviceInfo() {
|
||||
Log.info("Device Manufacturer - ${Build.MANUFACTURER}")
|
||||
Log.info("Device Model - ${Build.MODEL}")
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
|
||||
Log.info("SoC Manufacturer - ${Build.SOC_MANUFACTURER}")
|
||||
Log.info("SoC Model - ${Build.SOC_MODEL}")
|
||||
}
|
||||
Log.info("Total System Memory - ${MemoryUtil.getDeviceRAM()}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var application: CitraApplication? = null
|
||||
|
||||
val appContext: Context get() = application!!.applicationContext
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
lateinit var documentsTree: DocumentsTree
|
||||
}
|
||||
}
|
|
@ -1,720 +0,0 @@
|
|||
/*
|
||||
* Copyright 2013 Dolphin Emulator Project
|
||||
* Licensed under GPLv2+
|
||||
* Refer to the license.txt file included.
|
||||
*/
|
||||
|
||||
package org.citra.citra_emu;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.text.Html;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.Surface;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import org.citra.citra_emu.activities.EmulationActivity;
|
||||
import org.citra.citra_emu.applets.SoftwareKeyboard;
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
||||
import org.citra.citra_emu.utils.FileUtil;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
|
||||
import static android.Manifest.permission.CAMERA;
|
||||
import static android.Manifest.permission.RECORD_AUDIO;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
/**
|
||||
* Class which contains methods that interact
|
||||
* with the native side of the Citra code.
|
||||
*/
|
||||
public final class NativeLibrary {
|
||||
/**
|
||||
* Default touchscreen device
|
||||
*/
|
||||
public static final String TouchScreenDevice = "Touchscreen";
|
||||
public static WeakReference<EmulationActivity> sEmulationActivity = new WeakReference<>(null);
|
||||
|
||||
private static boolean alertResult = false;
|
||||
private static String alertPromptResult = "";
|
||||
private static int alertPromptButton = 0;
|
||||
private static final Object alertPromptLock = new Object();
|
||||
private static boolean alertPromptInProgress = false;
|
||||
private static String alertPromptCaption = "";
|
||||
private static int alertPromptButtonConfig = 0;
|
||||
private static EditText alertPromptEditText = null;
|
||||
|
||||
static {
|
||||
try {
|
||||
System.loadLibrary("citra-android");
|
||||
} catch (UnsatisfiedLinkError ex) {
|
||||
Log.error("[NativeLibrary] " + ex.toString());
|
||||
}
|
||||
}
|
||||
|
||||
private NativeLibrary() {
|
||||
// Disallows instantiation.
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles button press events for a gamepad.
|
||||
*
|
||||
* @param Device The input descriptor of the gamepad.
|
||||
* @param Button Key code identifying which button was pressed.
|
||||
* @param Action Mask identifying which action is happening (button pressed down, or button released).
|
||||
* @return If we handled the button press.
|
||||
*/
|
||||
public static native boolean onGamePadEvent(String Device, int Button, int Action);
|
||||
|
||||
/**
|
||||
* Handles gamepad movement events.
|
||||
*
|
||||
* @param Device The device ID of the gamepad.
|
||||
* @param Axis The axis ID
|
||||
* @param x_axis The value of the x-axis represented by the given ID.
|
||||
* @param y_axis The value of the y-axis represented by the given ID
|
||||
*/
|
||||
public static native boolean onGamePadMoveEvent(String Device, int Axis, float x_axis, float y_axis);
|
||||
|
||||
/**
|
||||
* Handles gamepad movement events.
|
||||
*
|
||||
* @param Device The device ID of the gamepad.
|
||||
* @param Axis_id The axis ID
|
||||
* @param axis_val The value of the axis represented by the given ID.
|
||||
*/
|
||||
public static native boolean onGamePadAxisEvent(String Device, int Axis_id, float axis_val);
|
||||
|
||||
/**
|
||||
* Handles touch events.
|
||||
*
|
||||
* @param x_axis The value of the x-axis.
|
||||
* @param y_axis The value of the y-axis
|
||||
* @param pressed To identify if the touch held down or released.
|
||||
* @return true if the pointer is within the touchscreen
|
||||
*/
|
||||
public static native boolean onTouchEvent(float x_axis, float y_axis, boolean pressed);
|
||||
|
||||
/**
|
||||
* Handles touch movement.
|
||||
*
|
||||
* @param x_axis The value of the instantaneous x-axis.
|
||||
* @param y_axis The value of the instantaneous y-axis.
|
||||
*/
|
||||
public static native void onTouchMoved(float x_axis, float y_axis);
|
||||
|
||||
public static native void ReloadSettings();
|
||||
|
||||
public static native String GetUserSetting(String gameID, String Section, String Key);
|
||||
|
||||
public static native void SetUserSetting(String gameID, String Section, String Key, String Value);
|
||||
|
||||
public static native void InitGameIni(String gameID);
|
||||
|
||||
public static native long GetTitleId(String filename);
|
||||
|
||||
public static native String GetGitRevision();
|
||||
|
||||
/**
|
||||
* Sets the current working user directory
|
||||
* If not set, it auto-detects a location
|
||||
*/
|
||||
public static native void SetUserDirectory(String directory);
|
||||
|
||||
public static native String[] GetInstalledGamePaths();
|
||||
|
||||
// Create the config.ini file.
|
||||
public static native void CreateConfigFile();
|
||||
|
||||
public static native void CreateLogFile();
|
||||
|
||||
public static native void LogUserDirectory(String directory);
|
||||
|
||||
public static native int DefaultCPUCore();
|
||||
|
||||
/**
|
||||
* Begins emulation.
|
||||
*/
|
||||
public static native void Run(String path);
|
||||
|
||||
public static native String[] GetTextureFilterNames();
|
||||
|
||||
/**
|
||||
* Begins emulation from the specified savestate.
|
||||
*/
|
||||
public static native void Run(String path, String savestatePath, boolean deleteSavestate);
|
||||
|
||||
// Surface Handling
|
||||
public static native void SurfaceChanged(Surface surf);
|
||||
|
||||
public static native void SurfaceDestroyed();
|
||||
|
||||
public static native void DoFrame();
|
||||
|
||||
/**
|
||||
* Unpauses emulation from a paused state.
|
||||
*/
|
||||
public static native void UnPauseEmulation();
|
||||
|
||||
/**
|
||||
* Pauses emulation.
|
||||
*/
|
||||
public static native void PauseEmulation();
|
||||
|
||||
/**
|
||||
* Stops emulation.
|
||||
*/
|
||||
public static native void StopEmulation();
|
||||
|
||||
/**
|
||||
* Returns true if emulation is running (or is paused).
|
||||
*/
|
||||
public static native boolean IsRunning();
|
||||
|
||||
/**
|
||||
* Returns the title ID of the currently running title, or 0 on failure.
|
||||
*/
|
||||
public static native long GetRunningTitleId();
|
||||
|
||||
/**
|
||||
* Returns the performance stats for the current game
|
||||
**/
|
||||
public static native double[] GetPerfStats();
|
||||
|
||||
/**
|
||||
* Notifies the core emulation that the orientation has changed.
|
||||
*/
|
||||
public static native void NotifyOrientationChange(int layout_option, int rotation);
|
||||
|
||||
/**
|
||||
* Swaps the top and bottom screens.
|
||||
*/
|
||||
public static native void SwapScreens(boolean swap_screens, int rotation);
|
||||
|
||||
public enum CoreError {
|
||||
ErrorSystemFiles,
|
||||
ErrorSavestate,
|
||||
ErrorUnknown,
|
||||
}
|
||||
|
||||
private static boolean coreErrorAlertResult = false;
|
||||
private static final Object coreErrorAlertLock = new Object();
|
||||
|
||||
public static class CoreErrorDialogFragment extends DialogFragment {
|
||||
static CoreErrorDialogFragment newInstance(String title, String message) {
|
||||
CoreErrorDialogFragment frag = new CoreErrorDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putString("title", title);
|
||||
args.putString("message", message);
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity emulationActivity = Objects.requireNonNull(getActivity());
|
||||
|
||||
final String title = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("title"));
|
||||
final String message = Objects.requireNonNull(Objects.requireNonNull(getArguments()).getString("message"));
|
||||
|
||||
return new MaterialAlertDialogBuilder(emulationActivity)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.continue_button, (dialog, which) -> {
|
||||
coreErrorAlertResult = true;
|
||||
synchronized (coreErrorAlertLock) {
|
||||
coreErrorAlertLock.notify();
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.abort_button, (dialog, which) -> {
|
||||
coreErrorAlertResult = false;
|
||||
synchronized (coreErrorAlertLock) {
|
||||
coreErrorAlertLock.notify();
|
||||
}
|
||||
}).setOnDismissListener(dialog -> {
|
||||
coreErrorAlertResult = true;
|
||||
synchronized (coreErrorAlertLock) {
|
||||
coreErrorAlertLock.notify();
|
||||
}
|
||||
}).create();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnCoreErrorImpl(String title, String message) {
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present");
|
||||
return;
|
||||
}
|
||||
|
||||
CoreErrorDialogFragment fragment = CoreErrorDialogFragment.newInstance(title, message);
|
||||
fragment.show(emulationActivity.getSupportFragmentManager(), "coreError");
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a core error.
|
||||
* @return true: continue; false: abort
|
||||
*/
|
||||
public static boolean OnCoreError(CoreError error, String details) {
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present");
|
||||
return false;
|
||||
}
|
||||
|
||||
String title, message;
|
||||
switch (error) {
|
||||
case ErrorSystemFiles: {
|
||||
title = emulationActivity.getString(R.string.system_archive_not_found);
|
||||
message = emulationActivity.getString(R.string.system_archive_not_found_message, details.isEmpty() ? emulationActivity.getString(R.string.system_archive_general) : details);
|
||||
break;
|
||||
}
|
||||
case ErrorSavestate: {
|
||||
title = emulationActivity.getString(R.string.save_load_error);
|
||||
message = details;
|
||||
break;
|
||||
}
|
||||
case ErrorUnknown: {
|
||||
title = emulationActivity.getString(R.string.fatal_error);
|
||||
message = emulationActivity.getString(R.string.fatal_error_message);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Show the AlertDialog on the main thread.
|
||||
emulationActivity.runOnUiThread(() -> OnCoreErrorImpl(title, message));
|
||||
|
||||
// Wait for the lock to notify that it is complete.
|
||||
synchronized (coreErrorAlertLock) {
|
||||
try {
|
||||
coreErrorAlertLock.wait();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return coreErrorAlertResult;
|
||||
}
|
||||
|
||||
public static boolean isPortraitMode() {
|
||||
return CitraApplication.getAppContext().getResources().getConfiguration().orientation ==
|
||||
Configuration.ORIENTATION_PORTRAIT;
|
||||
}
|
||||
|
||||
public static int landscapeScreenLayout() {
|
||||
return EmulationMenuSettings.getLandscapeScreenLayout();
|
||||
}
|
||||
|
||||
public static boolean displayAlertMsg(final String caption, final String text,
|
||||
final boolean yesNo) {
|
||||
Log.error("[NativeLibrary] Alert: " + text);
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
boolean result = false;
|
||||
if (emulationActivity == null) {
|
||||
Log.warning("[NativeLibrary] EmulationActivity is null, can't do panic alert.");
|
||||
} else {
|
||||
// Create object used for waiting.
|
||||
final Object lock = new Object();
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
|
||||
.setTitle(caption)
|
||||
.setMessage(text);
|
||||
|
||||
// If not yes/no dialog just have one button that dismisses modal,
|
||||
// otherwise have a yes and no button that sets alertResult accordingly.
|
||||
if (!yesNo) {
|
||||
builder
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, whichButton) ->
|
||||
{
|
||||
dialog.dismiss();
|
||||
synchronized (lock) {
|
||||
lock.notify();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
alertResult = false;
|
||||
|
||||
builder
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) ->
|
||||
{
|
||||
alertResult = true;
|
||||
dialog.dismiss();
|
||||
synchronized (lock) {
|
||||
lock.notify();
|
||||
}
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, (dialog, whichButton) ->
|
||||
{
|
||||
alertResult = false;
|
||||
dialog.dismiss();
|
||||
synchronized (lock) {
|
||||
lock.notify();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show the AlertDialog on the main thread.
|
||||
emulationActivity.runOnUiThread(builder::show);
|
||||
|
||||
// Wait for the lock to notify that it is complete.
|
||||
synchronized (lock) {
|
||||
try {
|
||||
lock.wait();
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (yesNo)
|
||||
result = alertResult;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void retryDisplayAlertPrompt() {
|
||||
if (!alertPromptInProgress) {
|
||||
return;
|
||||
}
|
||||
displayAlertPromptImpl(alertPromptCaption, alertPromptEditText.getText().toString(), alertPromptButtonConfig).show();
|
||||
}
|
||||
|
||||
public static String displayAlertPrompt(String caption, String text, int buttonConfig) {
|
||||
alertPromptCaption = caption;
|
||||
alertPromptButtonConfig = buttonConfig;
|
||||
alertPromptInProgress = true;
|
||||
|
||||
// Show the AlertDialog on the main thread
|
||||
sEmulationActivity.get().runOnUiThread(() -> displayAlertPromptImpl(alertPromptCaption, text, alertPromptButtonConfig).show());
|
||||
|
||||
// Wait for the lock to notify that it is complete
|
||||
synchronized (alertPromptLock) {
|
||||
try {
|
||||
alertPromptLock.wait();
|
||||
} catch (Exception e) {
|
||||
}
|
||||
}
|
||||
alertPromptInProgress = false;
|
||||
|
||||
return alertPromptResult;
|
||||
}
|
||||
|
||||
public static MaterialAlertDialogBuilder displayAlertPromptImpl(String caption, String text, int buttonConfig) {
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
alertPromptResult = "";
|
||||
alertPromptButton = 0;
|
||||
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.leftMargin = params.rightMargin = CitraApplication.getAppContext().getResources().getDimensionPixelSize(R.dimen.dialog_margin);
|
||||
|
||||
// Set up the input
|
||||
alertPromptEditText = new EditText(CitraApplication.getAppContext());
|
||||
alertPromptEditText.setText(text);
|
||||
alertPromptEditText.setSingleLine();
|
||||
alertPromptEditText.setLayoutParams(params);
|
||||
|
||||
FrameLayout container = new FrameLayout(emulationActivity);
|
||||
container.addView(alertPromptEditText);
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
|
||||
.setTitle(caption)
|
||||
.setView(container)
|
||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
|
||||
{
|
||||
alertPromptButton = buttonConfig;
|
||||
alertPromptResult = alertPromptEditText.getText().toString();
|
||||
synchronized (alertPromptLock) {
|
||||
alertPromptLock.notifyAll();
|
||||
}
|
||||
})
|
||||
.setOnDismissListener(dialogInterface ->
|
||||
{
|
||||
alertPromptResult = "";
|
||||
synchronized (alertPromptLock) {
|
||||
alertPromptLock.notifyAll();
|
||||
}
|
||||
});
|
||||
|
||||
if (buttonConfig > 0) {
|
||||
builder.setNegativeButton(android.R.string.cancel, (dialogInterface, i) ->
|
||||
{
|
||||
alertPromptResult = "";
|
||||
synchronized (alertPromptLock) {
|
||||
alertPromptLock.notifyAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static int alertPromptButton() {
|
||||
return alertPromptButton;
|
||||
}
|
||||
|
||||
public static void exitEmulationActivity(int resultCode) {
|
||||
final int Success = 0;
|
||||
final int ErrorNotInitialized = 1;
|
||||
final int ErrorGetLoader = 2;
|
||||
final int ErrorSystemMode = 3;
|
||||
final int ErrorLoader = 4;
|
||||
final int ErrorLoader_ErrorEncrypted = 5;
|
||||
final int ErrorLoader_ErrorInvalidFormat = 6;
|
||||
final int ErrorSystemFiles = 7;
|
||||
final int ShutdownRequested = 11;
|
||||
final int ErrorUnknown = 12;
|
||||
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
if (emulationActivity == null) {
|
||||
Log.warning("[NativeLibrary] EmulationActivity is null, can't exit.");
|
||||
return;
|
||||
}
|
||||
|
||||
int captionId = R.string.loader_error_invalid_format;
|
||||
if (resultCode == ErrorLoader_ErrorEncrypted) {
|
||||
captionId = R.string.loader_error_encrypted;
|
||||
}
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
|
||||
.setTitle(captionId)
|
||||
.setMessage(Html.fromHtml("Please follow the guides to redump your <a href=\"https://citra-emu.org/wiki/dumping-game-cartridges/\">game cartidges</a> or <a href=\"https://citra-emu.org/wiki/dumping-installed-titles/\">installed titles</a>.", Html.FROM_HTML_MODE_LEGACY))
|
||||
.setPositiveButton(android.R.string.ok, (dialog, whichButton) -> emulationActivity.finish())
|
||||
.setOnDismissListener(dialogInterface -> emulationActivity.finish());
|
||||
emulationActivity.runOnUiThread(() -> {
|
||||
AlertDialog alert = builder.create();
|
||||
alert.show();
|
||||
((TextView) alert.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance());
|
||||
});
|
||||
}
|
||||
|
||||
public static void setEmulationActivity(EmulationActivity emulationActivity) {
|
||||
Log.verbose("[NativeLibrary] Registering EmulationActivity.");
|
||||
sEmulationActivity = new WeakReference<>(emulationActivity);
|
||||
}
|
||||
|
||||
public static void clearEmulationActivity() {
|
||||
Log.verbose("[NativeLibrary] Unregistering EmulationActivity.");
|
||||
|
||||
sEmulationActivity.clear();
|
||||
}
|
||||
|
||||
private static final Object cameraPermissionLock = new Object();
|
||||
private static boolean cameraPermissionGranted = false;
|
||||
public static final int REQUEST_CODE_NATIVE_CAMERA = 800;
|
||||
|
||||
public static boolean RequestCameraPermission() {
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present");
|
||||
return false;
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(emulationActivity, CAMERA) == PackageManager.PERMISSION_GRANTED) {
|
||||
// Permission already granted
|
||||
return true;
|
||||
}
|
||||
emulationActivity.requestPermissions(new String[]{CAMERA}, REQUEST_CODE_NATIVE_CAMERA);
|
||||
|
||||
// Wait until result is returned
|
||||
synchronized (cameraPermissionLock) {
|
||||
try {
|
||||
cameraPermissionLock.wait();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
return cameraPermissionGranted;
|
||||
}
|
||||
|
||||
public static void CameraPermissionResult(boolean granted) {
|
||||
cameraPermissionGranted = granted;
|
||||
synchronized (cameraPermissionLock) {
|
||||
cameraPermissionLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
private static final Object micPermissionLock = new Object();
|
||||
private static boolean micPermissionGranted = false;
|
||||
public static final int REQUEST_CODE_NATIVE_MIC = 900;
|
||||
|
||||
public static boolean RequestMicPermission() {
|
||||
final EmulationActivity emulationActivity = sEmulationActivity.get();
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present");
|
||||
return false;
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(emulationActivity, RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) {
|
||||
// Permission already granted
|
||||
return true;
|
||||
}
|
||||
emulationActivity.requestPermissions(new String[]{RECORD_AUDIO}, REQUEST_CODE_NATIVE_MIC);
|
||||
|
||||
// Wait until result is returned
|
||||
synchronized (micPermissionLock) {
|
||||
try {
|
||||
micPermissionLock.wait();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
return micPermissionGranted;
|
||||
}
|
||||
|
||||
public static void MicPermissionResult(boolean granted) {
|
||||
micPermissionGranted = granted;
|
||||
synchronized (micPermissionLock) {
|
||||
micPermissionLock.notify();
|
||||
}
|
||||
}
|
||||
|
||||
/// Notifies that the activity is now in foreground and camera devices can now be reloaded
|
||||
public static native void ReloadCameraDevices();
|
||||
|
||||
public static native boolean LoadAmiibo(String path);
|
||||
|
||||
public static native void RemoveAmiibo();
|
||||
|
||||
public static final int SAVESTATE_SLOT_COUNT = 10;
|
||||
|
||||
public static final class SavestateInfo {
|
||||
public int slot;
|
||||
public Date time;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static native SavestateInfo[] GetSavestateInfo();
|
||||
|
||||
public static native void SaveState(int slot);
|
||||
public static native void LoadState(int slot);
|
||||
|
||||
/**
|
||||
* Logs the Citra version, Android version and, CPU.
|
||||
*/
|
||||
public static native void LogDeviceInfo();
|
||||
|
||||
/**
|
||||
* Button type for use in onTouchEvent
|
||||
*/
|
||||
public static final class ButtonType {
|
||||
public static final int BUTTON_A = 700;
|
||||
public static final int BUTTON_B = 701;
|
||||
public static final int BUTTON_X = 702;
|
||||
public static final int BUTTON_Y = 703;
|
||||
public static final int BUTTON_START = 704;
|
||||
public static final int BUTTON_SELECT = 705;
|
||||
public static final int BUTTON_HOME = 706;
|
||||
public static final int BUTTON_ZL = 707;
|
||||
public static final int BUTTON_ZR = 708;
|
||||
public static final int DPAD_UP = 709;
|
||||
public static final int DPAD_DOWN = 710;
|
||||
public static final int DPAD_LEFT = 711;
|
||||
public static final int DPAD_RIGHT = 712;
|
||||
public static final int STICK_LEFT = 713;
|
||||
public static final int STICK_LEFT_UP = 714;
|
||||
public static final int STICK_LEFT_DOWN = 715;
|
||||
public static final int STICK_LEFT_LEFT = 716;
|
||||
public static final int STICK_LEFT_RIGHT = 717;
|
||||
public static final int STICK_C = 718;
|
||||
public static final int STICK_C_UP = 719;
|
||||
public static final int STICK_C_DOWN = 720;
|
||||
public static final int STICK_C_LEFT = 771;
|
||||
public static final int STICK_C_RIGHT = 772;
|
||||
public static final int TRIGGER_L = 773;
|
||||
public static final int TRIGGER_R = 774;
|
||||
public static final int DPAD = 780;
|
||||
public static final int BUTTON_DEBUG = 781;
|
||||
public static final int BUTTON_GPIO14 = 782;
|
||||
}
|
||||
|
||||
/**
|
||||
* Button states
|
||||
*/
|
||||
public static final class ButtonState {
|
||||
public static final int RELEASED = 0;
|
||||
public static final int PRESSED = 1;
|
||||
}
|
||||
public static boolean createFile(String directory, String filename) {
|
||||
if (FileUtil.isNativePath(directory)) {
|
||||
return CitraApplication.documentsTree.createFile(directory, filename);
|
||||
}
|
||||
return FileUtil.createFile(CitraApplication.getAppContext(), directory, filename) != null;
|
||||
}
|
||||
|
||||
public static boolean createDir(String directory, String directoryName) {
|
||||
if (FileUtil.isNativePath(directory)) {
|
||||
return CitraApplication.documentsTree.createDir(directory, directoryName);
|
||||
}
|
||||
return FileUtil.createDir(CitraApplication.getAppContext(), directory, directoryName) != null;
|
||||
}
|
||||
|
||||
public static int openContentUri(String path, String openMode) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.openContentUri(path, openMode);
|
||||
}
|
||||
return FileUtil.openContentUri(CitraApplication.getAppContext(), path, openMode);
|
||||
}
|
||||
|
||||
public static String[] getFilesName(String path) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.getFilesName(path);
|
||||
}
|
||||
return FileUtil.getFilesName(CitraApplication.getAppContext(), path);
|
||||
}
|
||||
|
||||
public static long getSize(String path) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.getFileSize(path);
|
||||
}
|
||||
return FileUtil.getFileSize(CitraApplication.getAppContext(), path);
|
||||
}
|
||||
|
||||
public static boolean fileExists(String path) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.Exists(path);
|
||||
}
|
||||
return FileUtil.Exists(CitraApplication.getAppContext(), path);
|
||||
}
|
||||
|
||||
public static boolean isDirectory(String path) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.isDirectory(path);
|
||||
}
|
||||
return FileUtil.isDirectory(CitraApplication.getAppContext(), path);
|
||||
}
|
||||
|
||||
public static boolean copyFile(String sourcePath, String destinationParentPath, String destinationFilename) {
|
||||
if (FileUtil.isNativePath(sourcePath) && FileUtil.isNativePath(destinationParentPath)) {
|
||||
return CitraApplication.documentsTree.copyFile(sourcePath, destinationParentPath, destinationFilename);
|
||||
}
|
||||
return FileUtil.copyFile(CitraApplication.getAppContext(), sourcePath, destinationParentPath, destinationFilename);
|
||||
}
|
||||
|
||||
public static boolean renameFile(String path, String destinationFilename) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.renameFile(path, destinationFilename);
|
||||
}
|
||||
return FileUtil.renameFile(CitraApplication.getAppContext(), path, destinationFilename);
|
||||
}
|
||||
|
||||
public static boolean deleteDocument(String path) {
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
return CitraApplication.documentsTree.deleteDocument(path);
|
||||
}
|
||||
return FileUtil.deleteDocument(CitraApplication.getAppContext(), path);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,720 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu
|
||||
|
||||
import android.Manifest.permission
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Html
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.Surface
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.Keep
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.citra.citra_emu.activities.EmulationActivity
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings
|
||||
import org.citra.citra_emu.utils.FileUtil
|
||||
import org.citra.citra_emu.utils.Log
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Class which contains methods that interact
|
||||
* with the native side of the Citra code.
|
||||
*/
|
||||
object NativeLibrary {
|
||||
/**
|
||||
* Default touchscreen device
|
||||
*/
|
||||
const val TouchScreenDevice = "Touchscreen"
|
||||
|
||||
@JvmField
|
||||
var sEmulationActivity = WeakReference<EmulationActivity?>(null)
|
||||
private var alertResult = false
|
||||
val alertLock = Object()
|
||||
|
||||
init {
|
||||
try {
|
||||
System.loadLibrary("citra-android")
|
||||
} catch (ex: UnsatisfiedLinkError) {
|
||||
Log.error("[NativeLibrary] $ex")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles button press events for a gamepad.
|
||||
*
|
||||
* @param device The input descriptor of the gamepad.
|
||||
* @param button Key code identifying which button was pressed.
|
||||
* @param action Mask identifying which action is happening (button pressed down, or button released).
|
||||
* @return If we handled the button press.
|
||||
*/
|
||||
external fun onGamePadEvent(device: String, button: Int, action: Int): Boolean
|
||||
|
||||
/**
|
||||
* Handles gamepad movement events.
|
||||
*
|
||||
* @param device The device ID of the gamepad.
|
||||
* @param axis The axis ID
|
||||
* @param xAxis The value of the x-axis represented by the given ID.
|
||||
* @param yAxis The value of the y-axis represented by the given ID
|
||||
*/
|
||||
external fun onGamePadMoveEvent(device: String, axis: Int, xAxis: Float, yAxis: Float): Boolean
|
||||
|
||||
/**
|
||||
* Handles gamepad movement events.
|
||||
*
|
||||
* @param device The device ID of the gamepad.
|
||||
* @param axisId The axis ID
|
||||
* @param axisVal The value of the axis represented by the given ID.
|
||||
*/
|
||||
external fun onGamePadAxisEvent(device: String?, axisId: Int, axisVal: Float): Boolean
|
||||
|
||||
/**
|
||||
* Handles touch events.
|
||||
*
|
||||
* @param xAxis The value of the x-axis.
|
||||
* @param yAxis The value of the y-axis
|
||||
* @param pressed To identify if the touch held down or released.
|
||||
* @return true if the pointer is within the touchscreen
|
||||
*/
|
||||
external fun onTouchEvent(xAxis: Float, yAxis: Float, pressed: Boolean): Boolean
|
||||
|
||||
/**
|
||||
* Handles touch movement.
|
||||
*
|
||||
* @param xAxis The value of the instantaneous x-axis.
|
||||
* @param yAxis The value of the instantaneous y-axis.
|
||||
*/
|
||||
external fun onTouchMoved(xAxis: Float, yAxis: Float)
|
||||
|
||||
external fun reloadSettings()
|
||||
|
||||
external fun getTitleId(filename: String): Long
|
||||
|
||||
external fun getIsSystemTitle(path: String): Boolean
|
||||
|
||||
/**
|
||||
* Sets the current working user directory
|
||||
* If not set, it auto-detects a location
|
||||
*/
|
||||
external fun setUserDirectory(directory: String)
|
||||
external fun getInstalledGamePaths(): Array<String?>
|
||||
|
||||
// Create the config.ini file.
|
||||
external fun createConfigFile()
|
||||
external fun createLogFile()
|
||||
external fun logUserDirectory(directory: String)
|
||||
|
||||
/**
|
||||
* Begins emulation.
|
||||
*/
|
||||
external fun run(path: String)
|
||||
|
||||
// Surface Handling
|
||||
external fun surfaceChanged(surf: Surface)
|
||||
external fun surfaceDestroyed()
|
||||
external fun doFrame()
|
||||
|
||||
/**
|
||||
* Unpauses emulation from a paused state.
|
||||
*/
|
||||
external fun unPauseEmulation()
|
||||
|
||||
/**
|
||||
* Pauses emulation.
|
||||
*/
|
||||
external fun pauseEmulation()
|
||||
|
||||
/**
|
||||
* Stops emulation.
|
||||
*/
|
||||
external fun stopEmulation()
|
||||
|
||||
/**
|
||||
* Returns true if emulation is running (or is paused).
|
||||
*/
|
||||
external fun isRunning(): Boolean
|
||||
|
||||
/**
|
||||
* Returns the title ID of the currently running title, or 0 on failure.
|
||||
*/
|
||||
external fun getRunningTitleId(): Long
|
||||
|
||||
/**
|
||||
* Returns the performance stats for the current game
|
||||
*/
|
||||
external fun getPerfStats(): DoubleArray
|
||||
|
||||
/**
|
||||
* Notifies the core emulation that the orientation has changed.
|
||||
*/
|
||||
external fun notifyOrientationChange(layoutOption: Int, rotation: Int)
|
||||
|
||||
/**
|
||||
* Swaps the top and bottom screens.
|
||||
*/
|
||||
external fun swapScreens(swapScreens: Boolean, rotation: Int)
|
||||
|
||||
external fun initializeGpuDriver(
|
||||
hookLibDir: String?,
|
||||
customDriverDir: String?,
|
||||
customDriverName: String?,
|
||||
fileRedirectDir: String?
|
||||
)
|
||||
|
||||
external fun areKeysAvailable(): Boolean
|
||||
|
||||
external fun getHomeMenuPath(region: Int): String
|
||||
|
||||
external fun getSystemTitleIds(systemType: Int, region: Int): LongArray
|
||||
|
||||
external fun downloadTitleFromNus(title: Long): InstallStatus
|
||||
|
||||
private var coreErrorAlertResult = false
|
||||
private val coreErrorAlertLock = Object()
|
||||
|
||||
private fun onCoreErrorImpl(title: String, message: String) {
|
||||
val emulationActivity = sEmulationActivity.get()
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present")
|
||||
return
|
||||
}
|
||||
val fragment = CoreErrorDialogFragment.newInstance(title, message)
|
||||
fragment.show(emulationActivity.supportFragmentManager, CoreErrorDialogFragment.TAG)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles a core error.
|
||||
* @return true: continue; false: abort
|
||||
*/
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun onCoreError(error: CoreError?, details: String): Boolean {
|
||||
val emulationActivity = sEmulationActivity.get()
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present")
|
||||
return false
|
||||
}
|
||||
val title: String
|
||||
val message: String
|
||||
when (error) {
|
||||
CoreError.ErrorSystemFiles -> {
|
||||
title = emulationActivity.getString(R.string.system_archive_not_found)
|
||||
message = emulationActivity.getString(
|
||||
R.string.system_archive_not_found_message,
|
||||
details.ifEmpty { emulationActivity.getString(R.string.system_archive_general) }
|
||||
)
|
||||
}
|
||||
|
||||
CoreError.ErrorSavestate -> {
|
||||
title = emulationActivity.getString(R.string.save_load_error)
|
||||
message = details
|
||||
}
|
||||
|
||||
CoreError.ErrorUnknown -> {
|
||||
title = emulationActivity.getString(R.string.fatal_error)
|
||||
message = emulationActivity.getString(R.string.fatal_error_message)
|
||||
}
|
||||
|
||||
else -> {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Show the AlertDialog on the main thread.
|
||||
emulationActivity.runOnUiThread(Runnable { onCoreErrorImpl(title, message) })
|
||||
|
||||
// Wait for the lock to notify that it is complete.
|
||||
synchronized(coreErrorAlertLock) {
|
||||
try {
|
||||
coreErrorAlertLock.wait()
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
return coreErrorAlertResult
|
||||
}
|
||||
|
||||
@get:Keep
|
||||
@get:JvmStatic
|
||||
val isPortraitMode: Boolean
|
||||
get() = CitraApplication.appContext.resources.configuration.orientation ==
|
||||
Configuration.ORIENTATION_PORTRAIT
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun landscapeScreenLayout(): Int = EmulationMenuSettings.landscapeScreenLayout
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun displayAlertMsg(title: String, message: String, yesNo: Boolean): Boolean {
|
||||
Log.error("[NativeLibrary] Alert: $message")
|
||||
val emulationActivity = sEmulationActivity.get()
|
||||
var result = false
|
||||
if (emulationActivity == null) {
|
||||
Log.warning("[NativeLibrary] EmulationActivity is null, can't do panic alert.")
|
||||
} else {
|
||||
// Show the AlertDialog on the main thread.
|
||||
emulationActivity.runOnUiThread {
|
||||
AlertMessageDialogFragment.newInstance(title, message, yesNo).showNow(
|
||||
emulationActivity.supportFragmentManager,
|
||||
AlertMessageDialogFragment.TAG
|
||||
)
|
||||
}
|
||||
|
||||
// Wait for the lock to notify that it is complete.
|
||||
synchronized(alertLock) {
|
||||
try {
|
||||
alertLock.wait()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
if (yesNo) result = alertResult
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
class AlertMessageDialogFragment : DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
// Create object used for waiting.
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(requireArguments().getString(TITLE))
|
||||
.setMessage(requireArguments().getString(MESSAGE))
|
||||
|
||||
// If not yes/no dialog just have one button that dismisses modal,
|
||||
// otherwise have a yes and no button that sets alertResult accordingly.
|
||||
if (!requireArguments().getBoolean(YES_NO)) {
|
||||
builder
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
|
||||
synchronized(alertLock) { alertLock.notify() }
|
||||
}
|
||||
} else {
|
||||
alertResult = false
|
||||
builder
|
||||
.setPositiveButton(android.R.string.yes) { _: DialogInterface, _: Int ->
|
||||
alertResult = true
|
||||
synchronized(alertLock) { alertLock.notify() }
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) { _: DialogInterface, _: Int ->
|
||||
alertResult = false
|
||||
synchronized(alertLock) { alertLock.notify() }
|
||||
}
|
||||
}
|
||||
|
||||
return builder.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "AlertMessageDialogFragment"
|
||||
|
||||
const val TITLE = "title"
|
||||
const val MESSAGE = "message"
|
||||
const val YES_NO = "yesNo"
|
||||
|
||||
fun newInstance(
|
||||
title: String,
|
||||
message: String,
|
||||
yesNo: Boolean
|
||||
): AlertMessageDialogFragment {
|
||||
val args = Bundle()
|
||||
args.putString(TITLE, title)
|
||||
args.putString(MESSAGE, message)
|
||||
args.putBoolean(YES_NO, yesNo)
|
||||
val fragment = AlertMessageDialogFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun exitEmulationActivity(resultCode: Int) {
|
||||
val emulationActivity = sEmulationActivity.get()
|
||||
if (emulationActivity == null) {
|
||||
Log.warning("[NativeLibrary] EmulationActivity is null, can't exit.")
|
||||
return
|
||||
}
|
||||
|
||||
emulationActivity.runOnUiThread {
|
||||
EmulationErrorDialogFragment.newInstance(resultCode).showNow(
|
||||
emulationActivity.supportFragmentManager,
|
||||
EmulationErrorDialogFragment.TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class EmulationErrorDialogFragment : DialogFragment() {
|
||||
private lateinit var emulationActivity: EmulationActivity
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
emulationActivity = requireActivity() as EmulationActivity
|
||||
|
||||
var captionId = R.string.loader_error_invalid_format
|
||||
if (requireArguments().getInt(RESULT_CODE) == ErrorLoader_ErrorEncrypted) {
|
||||
captionId = R.string.loader_error_encrypted
|
||||
}
|
||||
|
||||
val alert = MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(captionId)
|
||||
.setMessage(
|
||||
Html.fromHtml(
|
||||
CitraApplication.appContext.resources.getString(R.string.redump_games),
|
||||
Html.FROM_HTML_MODE_LEGACY
|
||||
)
|
||||
)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
emulationActivity.finish()
|
||||
}
|
||||
.create()
|
||||
alert.show()
|
||||
|
||||
val alertMessage = alert.findViewById<View>(android.R.id.message) as TextView
|
||||
alertMessage.movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
isCancelable = false
|
||||
return alert
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "EmulationErrorDialogFragment"
|
||||
|
||||
const val RESULT_CODE = "resultcode"
|
||||
|
||||
const val Success = 0
|
||||
const val ErrorNotInitialized = 1
|
||||
const val ErrorGetLoader = 2
|
||||
const val ErrorSystemMode = 3
|
||||
const val ErrorLoader = 4
|
||||
const val ErrorLoader_ErrorEncrypted = 5
|
||||
const val ErrorLoader_ErrorInvalidFormat = 6
|
||||
const val ErrorSystemFiles = 7
|
||||
const val ShutdownRequested = 11
|
||||
const val ErrorUnknown = 12
|
||||
|
||||
fun newInstance(resultCode: Int): EmulationErrorDialogFragment {
|
||||
val args = Bundle()
|
||||
args.putInt(RESULT_CODE, resultCode)
|
||||
val fragment = EmulationErrorDialogFragment()
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setEmulationActivity(emulationActivity: EmulationActivity?) {
|
||||
Log.debug("[NativeLibrary] Registering EmulationActivity.")
|
||||
sEmulationActivity = WeakReference(emulationActivity)
|
||||
}
|
||||
|
||||
fun clearEmulationActivity() {
|
||||
Log.debug("[NativeLibrary] Unregistering EmulationActivity.")
|
||||
sEmulationActivity.clear()
|
||||
}
|
||||
|
||||
private val cameraPermissionLock = Object()
|
||||
private var cameraPermissionGranted = false
|
||||
const val REQUEST_CODE_NATIVE_CAMERA = 800
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun requestCameraPermission(): Boolean {
|
||||
val emulationActivity = sEmulationActivity.get()
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present")
|
||||
return false
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(emulationActivity, permission.CAMERA) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
// Permission already granted
|
||||
return true
|
||||
}
|
||||
emulationActivity.requestPermissions(arrayOf(permission.CAMERA), REQUEST_CODE_NATIVE_CAMERA)
|
||||
|
||||
// Wait until result is returned
|
||||
synchronized(cameraPermissionLock) {
|
||||
try {
|
||||
cameraPermissionLock.wait()
|
||||
} catch (ignored: InterruptedException) {
|
||||
}
|
||||
}
|
||||
return cameraPermissionGranted
|
||||
}
|
||||
|
||||
fun cameraPermissionResult(granted: Boolean) {
|
||||
cameraPermissionGranted = granted
|
||||
synchronized(cameraPermissionLock) { cameraPermissionLock.notify() }
|
||||
}
|
||||
|
||||
private val micPermissionLock = Object()
|
||||
private var micPermissionGranted = false
|
||||
const val REQUEST_CODE_NATIVE_MIC = 900
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun requestMicPermission(): Boolean {
|
||||
val emulationActivity = sEmulationActivity.get()
|
||||
if (emulationActivity == null) {
|
||||
Log.error("[NativeLibrary] EmulationActivity not present")
|
||||
return false
|
||||
}
|
||||
if (ContextCompat.checkSelfPermission(emulationActivity, permission.RECORD_AUDIO) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
// Permission already granted
|
||||
return true
|
||||
}
|
||||
emulationActivity.requestPermissions(
|
||||
arrayOf(permission.RECORD_AUDIO),
|
||||
REQUEST_CODE_NATIVE_MIC
|
||||
)
|
||||
|
||||
// Wait until result is returned
|
||||
synchronized(micPermissionLock) {
|
||||
try {
|
||||
micPermissionLock.wait()
|
||||
} catch (ignored: InterruptedException) {
|
||||
}
|
||||
}
|
||||
return micPermissionGranted
|
||||
}
|
||||
|
||||
fun micPermissionResult(granted: Boolean) {
|
||||
micPermissionGranted = granted
|
||||
synchronized(micPermissionLock) { micPermissionLock.notify() }
|
||||
}
|
||||
|
||||
// Notifies that the activity is now in foreground and camera devices can now be reloaded
|
||||
external fun reloadCameraDevices()
|
||||
|
||||
external fun loadAmiibo(path: String?): Boolean
|
||||
|
||||
external fun removeAmiibo()
|
||||
|
||||
const val SAVESTATE_SLOT_COUNT = 10
|
||||
|
||||
external fun getSavestateInfo(): Array<SaveStateInfo>?
|
||||
|
||||
external fun saveState(slot: Int)
|
||||
|
||||
external fun loadState(slot: Int)
|
||||
|
||||
/**
|
||||
* Logs the Citra version, Android version and, CPU.
|
||||
*/
|
||||
external fun logDeviceInfo()
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun createFile(directory: String, filename: String): Boolean =
|
||||
if (FileUtil.isNativePath(directory)) {
|
||||
CitraApplication.documentsTree.createFile(directory, filename)
|
||||
} else {
|
||||
FileUtil.createFile(directory, filename) != null
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun createDir(directory: String, directoryName: String): Boolean =
|
||||
if (FileUtil.isNativePath(directory)) {
|
||||
CitraApplication.documentsTree.createDir(directory, directoryName)
|
||||
} else {
|
||||
FileUtil.createDir(directory, directoryName) != null
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun openContentUri(path: String, openMode: String): Int =
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
CitraApplication.documentsTree.openContentUri(path, openMode)
|
||||
} else {
|
||||
FileUtil.openContentUri(path, openMode)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun getFilesName(path: String): Array<String?> =
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
CitraApplication.documentsTree.getFilesName(path)
|
||||
} else {
|
||||
FileUtil.getFilesName(path)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun getSize(path: String): Long =
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
CitraApplication.documentsTree.getFileSize(path)
|
||||
} else {
|
||||
FileUtil.getFileSize(path)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun fileExists(path: String): Boolean =
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
CitraApplication.documentsTree.exists(path)
|
||||
} else {
|
||||
FileUtil.exists(path)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun isDirectory(path: String): Boolean =
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
CitraApplication.documentsTree.isDirectory(path)
|
||||
} else {
|
||||
FileUtil.isDirectory(path)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun copyFile(
|
||||
sourcePath: String,
|
||||
destinationParentPath: String,
|
||||
destinationFilename: String
|
||||
): Boolean =
|
||||
if (FileUtil.isNativePath(sourcePath) &&
|
||||
FileUtil.isNativePath(destinationParentPath)
|
||||
) {
|
||||
CitraApplication.documentsTree
|
||||
.copyFile(sourcePath, destinationParentPath, destinationFilename)
|
||||
} else {
|
||||
FileUtil.copyFile(
|
||||
Uri.parse(sourcePath),
|
||||
Uri.parse(destinationParentPath),
|
||||
destinationFilename
|
||||
)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun renameFile(path: String, destinationFilename: String): Boolean =
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
CitraApplication.documentsTree.renameFile(path, destinationFilename)
|
||||
} else {
|
||||
FileUtil.renameFile(path, destinationFilename)
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun deleteDocument(path: String): Boolean =
|
||||
if (FileUtil.isNativePath(path)) {
|
||||
CitraApplication.documentsTree.deleteDocument(path)
|
||||
} else {
|
||||
FileUtil.deleteDocument(path)
|
||||
}
|
||||
|
||||
enum class CoreError {
|
||||
ErrorSystemFiles,
|
||||
ErrorSavestate,
|
||||
ErrorUnknown
|
||||
}
|
||||
|
||||
enum class InstallStatus {
|
||||
Success,
|
||||
ErrorFailedToOpenFile,
|
||||
ErrorFileNotFound,
|
||||
ErrorAborted,
|
||||
ErrorInvalid,
|
||||
ErrorEncrypted,
|
||||
Cancelled
|
||||
}
|
||||
|
||||
class CoreErrorDialogFragment : DialogFragment() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val title = requireArguments().getString(TITLE)
|
||||
val message = requireArguments().getString(MESSAGE)
|
||||
return MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.continue_button) { _: DialogInterface?, _: Int ->
|
||||
coreErrorAlertResult = true
|
||||
}
|
||||
.setNegativeButton(R.string.abort_button) { _: DialogInterface?, _: Int ->
|
||||
coreErrorAlertResult = false
|
||||
}.show()
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
coreErrorAlertResult = true
|
||||
synchronized(coreErrorAlertLock) { coreErrorAlertLock.notify() }
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "CoreErrorDialogFragment"
|
||||
|
||||
const val TITLE = "title"
|
||||
const val MESSAGE = "message"
|
||||
|
||||
fun newInstance(title: String, message: String): CoreErrorDialogFragment {
|
||||
val frag = CoreErrorDialogFragment()
|
||||
val args = Bundle()
|
||||
args.putString(TITLE, title)
|
||||
args.putString(MESSAGE, message)
|
||||
frag.arguments = args
|
||||
return frag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
class SaveStateInfo {
|
||||
var slot = 0
|
||||
var time: Date? = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Button type for use in onTouchEvent
|
||||
*/
|
||||
object ButtonType {
|
||||
const val BUTTON_A = 700
|
||||
const val BUTTON_B = 701
|
||||
const val BUTTON_X = 702
|
||||
const val BUTTON_Y = 703
|
||||
const val BUTTON_START = 704
|
||||
const val BUTTON_SELECT = 705
|
||||
const val BUTTON_HOME = 706
|
||||
const val BUTTON_ZL = 707
|
||||
const val BUTTON_ZR = 708
|
||||
const val DPAD_UP = 709
|
||||
const val DPAD_DOWN = 710
|
||||
const val DPAD_LEFT = 711
|
||||
const val DPAD_RIGHT = 712
|
||||
const val STICK_LEFT = 713
|
||||
const val STICK_LEFT_UP = 714
|
||||
const val STICK_LEFT_DOWN = 715
|
||||
const val STICK_LEFT_LEFT = 716
|
||||
const val STICK_LEFT_RIGHT = 717
|
||||
const val STICK_C = 718
|
||||
const val STICK_C_UP = 719
|
||||
const val STICK_C_DOWN = 720
|
||||
const val STICK_C_LEFT = 771
|
||||
const val STICK_C_RIGHT = 772
|
||||
const val TRIGGER_L = 773
|
||||
const val TRIGGER_R = 774
|
||||
const val DPAD = 780
|
||||
const val BUTTON_DEBUG = 781
|
||||
const val BUTTON_GPIO14 = 782
|
||||
}
|
||||
|
||||
/**
|
||||
* Button states
|
||||
*/
|
||||
object ButtonState {
|
||||
const val RELEASED = 0
|
||||
const val PRESSED = 1
|
||||
}
|
||||
}
|
|
@ -1,785 +0,0 @@
|
|||
package org.citra.citra_emu.activities;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Pair;
|
||||
import android.util.SparseIntArray;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SubMenu;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultCallback;
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.IntDef;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.contracts.OpenFileResultContract;
|
||||
import org.citra.citra_emu.features.cheats.ui.CheatsActivity;
|
||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
||||
import org.citra.citra_emu.features.settings.ui.SettingsActivity;
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile;
|
||||
import org.citra.citra_emu.camera.StillImageCameraHelper;
|
||||
import org.citra.citra_emu.fragments.EmulationFragment;
|
||||
import org.citra.citra_emu.ui.main.MainActivity;
|
||||
import org.citra.citra_emu.utils.ControllerMappingHelper;
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings;
|
||||
import org.citra.citra_emu.utils.FileBrowserHelper;
|
||||
import org.citra.citra_emu.utils.FileUtil;
|
||||
import org.citra.citra_emu.utils.ForegroundService;
|
||||
import org.citra.citra_emu.utils.ThemeUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static android.Manifest.permission.CAMERA;
|
||||
import static android.Manifest.permission.RECORD_AUDIO;
|
||||
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.slider.Slider;
|
||||
|
||||
public final class EmulationActivity extends AppCompatActivity {
|
||||
public static final String EXTRA_SELECTED_GAME = "SelectedGame";
|
||||
public static final String EXTRA_SELECTED_TITLE = "SelectedTitle";
|
||||
public static final int MENU_ACTION_EDIT_CONTROLS_PLACEMENT = 0;
|
||||
public static final int MENU_ACTION_TOGGLE_CONTROLS = 1;
|
||||
public static final int MENU_ACTION_ADJUST_SCALE = 2;
|
||||
public static final int MENU_ACTION_EXIT = 3;
|
||||
public static final int MENU_ACTION_SHOW_FPS = 4;
|
||||
public static final int MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE = 5;
|
||||
public static final int MENU_ACTION_SCREEN_LAYOUT_PORTRAIT = 6;
|
||||
public static final int MENU_ACTION_SCREEN_LAYOUT_SINGLE = 7;
|
||||
public static final int MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE = 8;
|
||||
public static final int MENU_ACTION_SWAP_SCREENS = 9;
|
||||
public static final int MENU_ACTION_RESET_OVERLAY = 10;
|
||||
public static final int MENU_ACTION_SHOW_OVERLAY = 11;
|
||||
public static final int MENU_ACTION_OPEN_SETTINGS = 12;
|
||||
public static final int MENU_ACTION_LOAD_AMIIBO = 13;
|
||||
public static final int MENU_ACTION_REMOVE_AMIIBO = 14;
|
||||
public static final int MENU_ACTION_JOYSTICK_REL_CENTER = 15;
|
||||
public static final int MENU_ACTION_DPAD_SLIDE_ENABLE = 16;
|
||||
public static final int MENU_ACTION_OPEN_CHEATS = 17;
|
||||
public static final int MENU_ACTION_CLOSE_GAME = 18;
|
||||
|
||||
public static final int REQUEST_SELECT_AMIIBO = 2;
|
||||
private static final int EMULATION_RUNNING_NOTIFICATION = 0x1000;
|
||||
private static SparseIntArray buttonsActionsMap = new SparseIntArray();
|
||||
|
||||
private final ActivityResultLauncher<Boolean> mOpenFileLauncher =
|
||||
registerForActivityResult(new OpenFileResultContract(), result -> {
|
||||
if (result == null)
|
||||
return;
|
||||
String[] selectedFiles = FileBrowserHelper.getSelectedFiles(
|
||||
result, getApplicationContext(), Collections.singletonList("bin"));
|
||||
if (selectedFiles == null)
|
||||
return;
|
||||
|
||||
onAmiiboSelected(selectedFiles[0]);
|
||||
});
|
||||
|
||||
static {
|
||||
buttonsActionsMap.append(R.id.menu_emulation_edit_layout,
|
||||
EmulationActivity.MENU_ACTION_EDIT_CONTROLS_PLACEMENT);
|
||||
buttonsActionsMap.append(R.id.menu_emulation_toggle_controls,
|
||||
EmulationActivity.MENU_ACTION_TOGGLE_CONTROLS);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_adjust_scale, EmulationActivity.MENU_ACTION_ADJUST_SCALE);
|
||||
buttonsActionsMap.append(R.id.menu_emulation_show_fps,
|
||||
EmulationActivity.MENU_ACTION_SHOW_FPS);
|
||||
buttonsActionsMap.append(R.id.menu_screen_layout_landscape,
|
||||
EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE);
|
||||
buttonsActionsMap.append(R.id.menu_screen_layout_portrait,
|
||||
EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_PORTRAIT);
|
||||
buttonsActionsMap.append(R.id.menu_screen_layout_single,
|
||||
EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SINGLE);
|
||||
buttonsActionsMap.append(R.id.menu_screen_layout_sidebyside,
|
||||
EmulationActivity.MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE);
|
||||
buttonsActionsMap.append(R.id.menu_emulation_swap_screens,
|
||||
EmulationActivity.MENU_ACTION_SWAP_SCREENS);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_reset_overlay, EmulationActivity.MENU_ACTION_RESET_OVERLAY);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_show_overlay, EmulationActivity.MENU_ACTION_SHOW_OVERLAY);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_open_settings, EmulationActivity.MENU_ACTION_OPEN_SETTINGS);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_amiibo_load, EmulationActivity.MENU_ACTION_LOAD_AMIIBO);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_amiibo_remove, EmulationActivity.MENU_ACTION_REMOVE_AMIIBO);
|
||||
buttonsActionsMap.append(R.id.menu_emulation_joystick_rel_center,
|
||||
EmulationActivity.MENU_ACTION_JOYSTICK_REL_CENTER);
|
||||
buttonsActionsMap.append(R.id.menu_emulation_dpad_slide_enable,
|
||||
EmulationActivity.MENU_ACTION_DPAD_SLIDE_ENABLE);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_open_cheats, EmulationActivity.MENU_ACTION_OPEN_CHEATS);
|
||||
buttonsActionsMap
|
||||
.append(R.id.menu_emulation_close_game, EmulationActivity.MENU_ACTION_CLOSE_GAME);
|
||||
}
|
||||
|
||||
private EmulationFragment mEmulationFragment;
|
||||
private SharedPreferences mPreferences;
|
||||
private ControllerMappingHelper mControllerMappingHelper;
|
||||
private Intent foregroundService;
|
||||
private boolean activityRecreated;
|
||||
private String mSelectedTitle;
|
||||
private String mPath;
|
||||
|
||||
public static void launch(FragmentActivity activity, String path, String title) {
|
||||
Intent launcher = new Intent(activity, EmulationActivity.class);
|
||||
|
||||
launcher.putExtra(EXTRA_SELECTED_GAME, path);
|
||||
launcher.putExtra(EXTRA_SELECTED_TITLE, title);
|
||||
activity.startActivity(launcher);
|
||||
}
|
||||
|
||||
public static void tryDismissRunningNotification(Activity activity) {
|
||||
NotificationManagerCompat.from(activity).cancel(EMULATION_RUNNING_NOTIFICATION);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
stopService(foregroundService);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
ThemeUtil.applyTheme(this);
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
// Get params we were passed
|
||||
Intent gameToEmulate = getIntent();
|
||||
mPath = gameToEmulate.getStringExtra(EXTRA_SELECTED_GAME);
|
||||
mSelectedTitle = gameToEmulate.getStringExtra(EXTRA_SELECTED_TITLE);
|
||||
activityRecreated = false;
|
||||
} else {
|
||||
activityRecreated = true;
|
||||
restoreState(savedInstanceState);
|
||||
}
|
||||
|
||||
mControllerMappingHelper = new ControllerMappingHelper();
|
||||
|
||||
// Set these options now so that the SurfaceView the game renders into is the right size.
|
||||
enableFullscreenImmersive();
|
||||
|
||||
setContentView(R.layout.activity_emulation);
|
||||
|
||||
// Find or create the EmulationFragment
|
||||
mEmulationFragment = (EmulationFragment) getSupportFragmentManager()
|
||||
.findFragmentById(R.id.frame_emulation_fragment);
|
||||
if (mEmulationFragment == null) {
|
||||
mEmulationFragment = EmulationFragment.newInstance(mPath);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.add(R.id.frame_emulation_fragment, mEmulationFragment)
|
||||
.commit();
|
||||
}
|
||||
|
||||
setTitle(mSelectedTitle);
|
||||
|
||||
mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
|
||||
// Start a foreground service to prevent the app from getting killed in the background
|
||||
foregroundService = new Intent(EmulationActivity.this, ForegroundService.class);
|
||||
startForegroundService(foregroundService);
|
||||
|
||||
// Override Citra core INI with the one set by our in game menu
|
||||
NativeLibrary.SwapScreens(EmulationMenuSettings.getSwapScreens(),
|
||||
getWindowManager().getDefaultDisplay().getRotation());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
outState.putString(EXTRA_SELECTED_GAME, mPath);
|
||||
outState.putString(EXTRA_SELECTED_TITLE, mSelectedTitle);
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
protected void restoreState(Bundle savedInstanceState) {
|
||||
mPath = savedInstanceState.getString(EXTRA_SELECTED_GAME);
|
||||
mSelectedTitle = savedInstanceState.getString(EXTRA_SELECTED_TITLE);
|
||||
|
||||
// If an alert prompt was in progress when state was restored, retry displaying it
|
||||
NativeLibrary.retryDisplayAlertPrompt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRestart() {
|
||||
super.onRestart();
|
||||
NativeLibrary.ReloadCameraDevices();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
View anchor = findViewById(R.id.menu_anchor);
|
||||
PopupMenu popupMenu = new PopupMenu(this, anchor);
|
||||
onCreateOptionsMenu(popupMenu.getMenu(), popupMenu.getMenuInflater());
|
||||
updateSavestateMenuOptions(popupMenu.getMenu());
|
||||
popupMenu.setOnMenuItemClickListener(this::onOptionsItemSelected);
|
||||
popupMenu.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
switch (requestCode) {
|
||||
case NativeLibrary.REQUEST_CODE_NATIVE_CAMERA:
|
||||
if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
|
||||
shouldShowRequestPermissionRationale(CAMERA)) {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.camera)
|
||||
.setMessage(R.string.camera_permission_needed)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
NativeLibrary.CameraPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
|
||||
break;
|
||||
case NativeLibrary.REQUEST_CODE_NATIVE_MIC:
|
||||
if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
|
||||
shouldShowRequestPermissionRationale(RECORD_AUDIO)) {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.microphone)
|
||||
.setMessage(R.string.microphone_permission_needed)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
NativeLibrary.MicPermissionResult(grantResults[0] == PackageManager.PERMISSION_GRANTED);
|
||||
break;
|
||||
default:
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void onEmulationStarted() {
|
||||
Toast.makeText(this, getString(R.string.emulation_menu_help), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
private void enableFullscreenImmersive() {
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
onCreateOptionsMenu(menu, getMenuInflater());
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.menu_emulation, menu);
|
||||
|
||||
int layoutOptionMenuItem = R.id.menu_screen_layout_landscape;
|
||||
switch (EmulationMenuSettings.getLandscapeScreenLayout()) {
|
||||
case EmulationMenuSettings.LayoutOption_SingleScreen:
|
||||
layoutOptionMenuItem = R.id.menu_screen_layout_single;
|
||||
break;
|
||||
case EmulationMenuSettings.LayoutOption_SideScreen:
|
||||
layoutOptionMenuItem = R.id.menu_screen_layout_sidebyside;
|
||||
break;
|
||||
case EmulationMenuSettings.LayoutOption_MobilePortrait:
|
||||
layoutOptionMenuItem = R.id.menu_screen_layout_portrait;
|
||||
break;
|
||||
}
|
||||
|
||||
menu.findItem(layoutOptionMenuItem).setChecked(true);
|
||||
menu.findItem(R.id.menu_emulation_joystick_rel_center).setChecked(EmulationMenuSettings.getJoystickRelCenter());
|
||||
menu.findItem(R.id.menu_emulation_dpad_slide_enable).setChecked(EmulationMenuSettings.getDpadSlideEnable());
|
||||
menu.findItem(R.id.menu_emulation_show_fps).setChecked(EmulationMenuSettings.getShowFps());
|
||||
menu.findItem(R.id.menu_emulation_swap_screens).setChecked(EmulationMenuSettings.getSwapScreens());
|
||||
menu.findItem(R.id.menu_emulation_show_overlay).setChecked(EmulationMenuSettings.getShowOverlay());
|
||||
}
|
||||
|
||||
private void DisplaySavestateWarning() {
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.getAppContext());
|
||||
if (preferences.getBoolean("savestateWarningShown", false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
LayoutInflater inflater = mEmulationFragment.requireActivity().getLayoutInflater();
|
||||
View view = inflater.inflate(R.layout.dialog_checkbox, null);
|
||||
CheckBox checkBox = view.findViewById(R.id.checkBox);
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.savestate_warning_title)
|
||||
.setMessage(R.string.savestate_warning_message)
|
||||
.setView(view)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
preferences.edit().putBoolean("savestateWarningShown", checkBox.isChecked()).apply();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareOptionsMenu(Menu menu) {
|
||||
super.onPrepareOptionsMenu(menu);
|
||||
updateSavestateMenuOptions(menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void updateSavestateMenuOptions(Menu menu) {
|
||||
final NativeLibrary.SavestateInfo[] savestates = NativeLibrary.GetSavestateInfo();
|
||||
if (savestates == null) {
|
||||
menu.findItem(R.id.menu_emulation_save_state).setVisible(false);
|
||||
menu.findItem(R.id.menu_emulation_load_state).setVisible(false);
|
||||
return;
|
||||
}
|
||||
menu.findItem(R.id.menu_emulation_save_state).setVisible(true);
|
||||
menu.findItem(R.id.menu_emulation_load_state).setVisible(true);
|
||||
|
||||
final SubMenu saveStateMenu = menu.findItem(R.id.menu_emulation_save_state).getSubMenu();
|
||||
final SubMenu loadStateMenu = menu.findItem(R.id.menu_emulation_load_state).getSubMenu();
|
||||
saveStateMenu.clear();
|
||||
loadStateMenu.clear();
|
||||
|
||||
// Update savestates information
|
||||
for (int i = 0; i < NativeLibrary.SAVESTATE_SLOT_COUNT; ++i) {
|
||||
final int slot = i + 1;
|
||||
final String text = getString(R.string.emulation_empty_state_slot, slot);
|
||||
saveStateMenu.add(text).setEnabled(true).setOnMenuItemClickListener((item) -> {
|
||||
DisplaySavestateWarning();
|
||||
NativeLibrary.SaveState(slot);
|
||||
return true;
|
||||
});
|
||||
loadStateMenu.add(text).setEnabled(false).setOnMenuItemClickListener((item) -> {
|
||||
NativeLibrary.LoadState(slot);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
for (final NativeLibrary.SavestateInfo info : savestates) {
|
||||
final String text = getString(R.string.emulation_occupied_state_slot, info.slot, info.time);
|
||||
saveStateMenu.getItem(info.slot - 1).setTitle(text);
|
||||
loadStateMenu.getItem(info.slot - 1).setTitle(text).setEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("WrongConstant")
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
int action = buttonsActionsMap.get(item.getItemId(), -1);
|
||||
|
||||
switch (action) {
|
||||
// Edit the placement of the controls
|
||||
case MENU_ACTION_EDIT_CONTROLS_PLACEMENT:
|
||||
editControlsPlacement();
|
||||
break;
|
||||
|
||||
// Enable/Disable specific buttons or the entire input overlay.
|
||||
case MENU_ACTION_TOGGLE_CONTROLS:
|
||||
toggleControls();
|
||||
break;
|
||||
|
||||
// Adjust the scale of the overlay controls.
|
||||
case MENU_ACTION_ADJUST_SCALE:
|
||||
adjustScale();
|
||||
break;
|
||||
|
||||
// Toggle the visibility of the Performance stats TextView
|
||||
case MENU_ACTION_SHOW_FPS: {
|
||||
final boolean isEnabled = !EmulationMenuSettings.getShowFps();
|
||||
EmulationMenuSettings.setShowFps(isEnabled);
|
||||
item.setChecked(isEnabled);
|
||||
|
||||
mEmulationFragment.updateShowFpsOverlay();
|
||||
break;
|
||||
}
|
||||
// Sets the screen layout to Landscape
|
||||
case MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE:
|
||||
changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobileLandscape, item);
|
||||
break;
|
||||
|
||||
// Sets the screen layout to Portrait
|
||||
case MENU_ACTION_SCREEN_LAYOUT_PORTRAIT:
|
||||
changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobilePortrait, item);
|
||||
break;
|
||||
|
||||
// Sets the screen layout to Single
|
||||
case MENU_ACTION_SCREEN_LAYOUT_SINGLE:
|
||||
changeScreenOrientation(EmulationMenuSettings.LayoutOption_SingleScreen, item);
|
||||
break;
|
||||
|
||||
// Sets the screen layout to Side by Side
|
||||
case MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE:
|
||||
changeScreenOrientation(EmulationMenuSettings.LayoutOption_SideScreen, item);
|
||||
break;
|
||||
|
||||
// Swap the top and bottom screen locations
|
||||
case MENU_ACTION_SWAP_SCREENS: {
|
||||
final boolean isEnabled = !EmulationMenuSettings.getSwapScreens();
|
||||
EmulationMenuSettings.setSwapScreens(isEnabled);
|
||||
item.setChecked(isEnabled);
|
||||
|
||||
NativeLibrary.SwapScreens(isEnabled, getWindowManager().getDefaultDisplay()
|
||||
.getRotation());
|
||||
break;
|
||||
}
|
||||
|
||||
// Reset overlay placement
|
||||
case MENU_ACTION_RESET_OVERLAY:
|
||||
resetOverlay();
|
||||
break;
|
||||
|
||||
// Show or hide overlay
|
||||
case MENU_ACTION_SHOW_OVERLAY: {
|
||||
final boolean isEnabled = !EmulationMenuSettings.getShowOverlay();
|
||||
EmulationMenuSettings.setShowOverlay(isEnabled);
|
||||
item.setChecked(isEnabled);
|
||||
|
||||
mEmulationFragment.refreshInputOverlay();
|
||||
break;
|
||||
}
|
||||
|
||||
case MENU_ACTION_EXIT:
|
||||
mEmulationFragment.stopEmulation();
|
||||
finish();
|
||||
break;
|
||||
|
||||
case MENU_ACTION_OPEN_SETTINGS:
|
||||
SettingsActivity.launch(this, SettingsFile.FILE_NAME_CONFIG, "");
|
||||
break;
|
||||
|
||||
case MENU_ACTION_LOAD_AMIIBO:
|
||||
mOpenFileLauncher.launch(false);
|
||||
break;
|
||||
|
||||
case MENU_ACTION_REMOVE_AMIIBO:
|
||||
RemoveAmiibo();
|
||||
break;
|
||||
|
||||
case MENU_ACTION_JOYSTICK_REL_CENTER:
|
||||
final boolean isJoystickRelCenterEnabled = !EmulationMenuSettings.getJoystickRelCenter();
|
||||
EmulationMenuSettings.setJoystickRelCenter(isJoystickRelCenterEnabled);
|
||||
item.setChecked(isJoystickRelCenterEnabled);
|
||||
break;
|
||||
|
||||
case MENU_ACTION_DPAD_SLIDE_ENABLE:
|
||||
final boolean isDpadSlideEnabled = !EmulationMenuSettings.getDpadSlideEnable();
|
||||
EmulationMenuSettings.setDpadSlideEnable(isDpadSlideEnabled);
|
||||
item.setChecked(isDpadSlideEnabled);
|
||||
break;
|
||||
|
||||
case MENU_ACTION_OPEN_CHEATS:
|
||||
CheatsActivity.launch(this, NativeLibrary.GetRunningTitleId());
|
||||
break;
|
||||
|
||||
case MENU_ACTION_CLOSE_GAME:
|
||||
NativeLibrary.PauseEmulation();
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.emulation_close_game)
|
||||
.setMessage(R.string.emulation_close_game_message)
|
||||
.setPositiveButton(android.R.string.yes, (dialogInterface, i) ->
|
||||
{
|
||||
mEmulationFragment.stopEmulation();
|
||||
finish();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> NativeLibrary.UnPauseEmulation())
|
||||
.setOnCancelListener(dialogInterface -> NativeLibrary.UnPauseEmulation())
|
||||
.show();
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void changeScreenOrientation(int layoutOption, MenuItem item) {
|
||||
item.setChecked(true);
|
||||
NativeLibrary.NotifyOrientationChange(layoutOption, getWindowManager().getDefaultDisplay()
|
||||
.getRotation());
|
||||
EmulationMenuSettings.setLandscapeScreenLayout(layoutOption);
|
||||
}
|
||||
|
||||
private void editControlsPlacement() {
|
||||
if (mEmulationFragment.isConfiguringControls()) {
|
||||
mEmulationFragment.stopConfiguringControls();
|
||||
} else {
|
||||
mEmulationFragment.startConfiguringControls();
|
||||
}
|
||||
}
|
||||
|
||||
// Gets button presses
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
int action;
|
||||
int button = mPreferences.getInt(InputBindingSetting.getInputButtonKey(event.getKeyCode()), event.getKeyCode());
|
||||
|
||||
switch (event.getAction()) {
|
||||
case KeyEvent.ACTION_DOWN:
|
||||
// Handling the case where the back button is pressed.
|
||||
if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Normal key events.
|
||||
action = NativeLibrary.ButtonState.PRESSED;
|
||||
break;
|
||||
case KeyEvent.ACTION_UP:
|
||||
action = NativeLibrary.ButtonState.RELEASED;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
InputDevice input = event.getDevice();
|
||||
|
||||
if (input == null) {
|
||||
// Controller was disconnected
|
||||
return false;
|
||||
}
|
||||
|
||||
return NativeLibrary.onGamePadEvent(input.getDescriptor(), button, action);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent result) {
|
||||
super.onActivityResult(requestCode, resultCode, result);
|
||||
if (requestCode == StillImageCameraHelper.REQUEST_CAMERA_FILE_PICKER) {
|
||||
StillImageCameraHelper.OnFilePickerResult(resultCode == RESULT_OK ? result : null);
|
||||
}
|
||||
}
|
||||
|
||||
private void onAmiiboSelected(String selectedFile) {
|
||||
boolean success = NativeLibrary.LoadAmiibo(selectedFile);
|
||||
|
||||
if (!success) {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.amiibo_load_error)
|
||||
.setMessage(R.string.amiibo_load_error_message)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveAmiibo() {
|
||||
NativeLibrary.RemoveAmiibo();
|
||||
}
|
||||
|
||||
private void toggleControls() {
|
||||
final SharedPreferences.Editor editor = mPreferences.edit();
|
||||
boolean[] enabledButtons = new boolean[14];
|
||||
|
||||
for (int i = 0; i < enabledButtons.length; i++) {
|
||||
// Buttons that are disabled by default
|
||||
boolean defaultValue = true;
|
||||
switch (i) {
|
||||
case 6: // ZL
|
||||
case 7: // ZR
|
||||
case 12: // C-stick
|
||||
defaultValue = false;
|
||||
break;
|
||||
}
|
||||
|
||||
enabledButtons[i] = mPreferences.getBoolean("buttonToggle" + i, defaultValue);
|
||||
}
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.emulation_toggle_controls)
|
||||
.setMultiChoiceItems(R.array.n3dsButtons, enabledButtons,
|
||||
(dialog, indexSelected, isChecked) -> editor
|
||||
.putBoolean("buttonToggle" + indexSelected, isChecked))
|
||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) ->
|
||||
{
|
||||
editor.apply();
|
||||
mEmulationFragment.refreshInputOverlay();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void adjustScale() {
|
||||
LayoutInflater inflater = LayoutInflater.from(this);
|
||||
View view = inflater.inflate(R.layout.dialog_slider, null);
|
||||
|
||||
final Slider slider = view.findViewById(R.id.slider);
|
||||
final TextView textValue = view.findViewById(R.id.text_value);
|
||||
final TextView units = view.findViewById(R.id.text_units);
|
||||
|
||||
slider.setValueTo(150);
|
||||
slider.setValue(mPreferences.getInt("controlScale", 50));
|
||||
slider.addOnChangeListener((slider1, progress, fromUser) -> {
|
||||
textValue.setText(String.valueOf((int) progress + 50));
|
||||
setControlScale((int) slider1.getValue());
|
||||
});
|
||||
|
||||
textValue.setText(String.valueOf((int) slider.getValue() + 50));
|
||||
units.setText("%");
|
||||
|
||||
final int previousProgress = (int) slider.getValue();
|
||||
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(R.string.emulation_control_scale)
|
||||
.setView(view)
|
||||
.setNegativeButton(android.R.string.cancel, (dialogInterface, i) -> setControlScale(previousProgress))
|
||||
.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> setControlScale((int) slider.getValue()))
|
||||
.setNeutralButton(R.string.slider_default, (dialogInterface, i) -> setControlScale(50))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void setControlScale(int scale) {
|
||||
SharedPreferences.Editor editor = mPreferences.edit();
|
||||
editor.putInt("controlScale", scale);
|
||||
editor.apply();
|
||||
mEmulationFragment.refreshInputOverlay();
|
||||
}
|
||||
|
||||
private void resetOverlay() {
|
||||
new MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.emulation_touch_overlay_reset))
|
||||
.setPositiveButton(android.R.string.yes, (dialogInterface, i) -> mEmulationFragment.resetInputOverlay())
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchGenericMotionEvent(MotionEvent event) {
|
||||
if (((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)) {
|
||||
return super.dispatchGenericMotionEvent(event);
|
||||
}
|
||||
|
||||
// Don't attempt to do anything if we are disconnecting a device.
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
|
||||
return true;
|
||||
}
|
||||
|
||||
InputDevice input = event.getDevice();
|
||||
List<InputDevice.MotionRange> motions = input.getMotionRanges();
|
||||
|
||||
float[] axisValuesCirclePad = {0.0f, 0.0f};
|
||||
float[] axisValuesCStick = {0.0f, 0.0f};
|
||||
float[] axisValuesDPad = {0.0f, 0.0f};
|
||||
boolean isTriggerPressedLMapped = false;
|
||||
boolean isTriggerPressedRMapped = false;
|
||||
boolean isTriggerPressedZLMapped = false;
|
||||
boolean isTriggerPressedZRMapped = false;
|
||||
boolean isTriggerPressedL = false;
|
||||
boolean isTriggerPressedR = false;
|
||||
boolean isTriggerPressedZL = false;
|
||||
boolean isTriggerPressedZR = false;
|
||||
|
||||
for (InputDevice.MotionRange range : motions) {
|
||||
int axis = range.getAxis();
|
||||
float origValue = event.getAxisValue(axis);
|
||||
float value = mControllerMappingHelper.scaleAxis(input, axis, origValue);
|
||||
int nextMapping = mPreferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1);
|
||||
int guestOrientation = mPreferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1);
|
||||
|
||||
if (nextMapping == -1 || guestOrientation == -1) {
|
||||
// Axis is unmapped
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((value > 0.f && value < 0.1f) || (value < 0.f && value > -0.1f)) {
|
||||
// Skip joystick wobble
|
||||
value = 0.f;
|
||||
}
|
||||
|
||||
if (nextMapping == NativeLibrary.ButtonType.STICK_LEFT) {
|
||||
axisValuesCirclePad[guestOrientation] = value;
|
||||
} else if (nextMapping == NativeLibrary.ButtonType.STICK_C) {
|
||||
axisValuesCStick[guestOrientation] = value;
|
||||
} else if (nextMapping == NativeLibrary.ButtonType.DPAD) {
|
||||
axisValuesDPad[guestOrientation] = value;
|
||||
} else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_L) {
|
||||
isTriggerPressedLMapped = true;
|
||||
isTriggerPressedL = value != 0.f;
|
||||
} else if (nextMapping == NativeLibrary.ButtonType.TRIGGER_R) {
|
||||
isTriggerPressedRMapped = true;
|
||||
isTriggerPressedR = value != 0.f;
|
||||
} else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZL) {
|
||||
isTriggerPressedZLMapped = true;
|
||||
isTriggerPressedZL = value != 0.f;
|
||||
} else if (nextMapping == NativeLibrary.ButtonType.BUTTON_ZR) {
|
||||
isTriggerPressedZRMapped = true;
|
||||
isTriggerPressedZR = value != 0.f;
|
||||
}
|
||||
}
|
||||
|
||||
// Circle-Pad and C-Stick status
|
||||
NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_LEFT, axisValuesCirclePad[0], axisValuesCirclePad[1]);
|
||||
NativeLibrary.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_C, axisValuesCStick[0], axisValuesCStick[1]);
|
||||
|
||||
// Triggers L/R and ZL/ZR
|
||||
if (isTriggerPressedLMapped) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_L, isTriggerPressedL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (isTriggerPressedRMapped) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_R, isTriggerPressedR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (isTriggerPressedZLMapped) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZL, isTriggerPressedZL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (isTriggerPressedZRMapped) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZR, isTriggerPressedZR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
|
||||
// Work-around to allow D-pad axis to be bound to emulated buttons
|
||||
if (axisValuesDPad[0] == 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (axisValuesDPad[0] < 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.PRESSED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (axisValuesDPad[0] > 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.PRESSED);
|
||||
}
|
||||
if (axisValuesDPad[1] == 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (axisValuesDPad[1] < 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.PRESSED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
|
||||
}
|
||||
if (axisValuesDPad[1] > 0.f) {
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
|
||||
NativeLibrary.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.PRESSED);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isActivityRecreated() {
|
||||
return activityRecreated;
|
||||
}
|
||||
|
||||
@Retention(SOURCE)
|
||||
@IntDef({MENU_ACTION_EDIT_CONTROLS_PLACEMENT, MENU_ACTION_TOGGLE_CONTROLS, MENU_ACTION_ADJUST_SCALE,
|
||||
MENU_ACTION_EXIT, MENU_ACTION_SHOW_FPS, MENU_ACTION_SCREEN_LAYOUT_LANDSCAPE,
|
||||
MENU_ACTION_SCREEN_LAYOUT_PORTRAIT, MENU_ACTION_SCREEN_LAYOUT_SINGLE, MENU_ACTION_SCREEN_LAYOUT_SIDEBYSIDE,
|
||||
MENU_ACTION_SWAP_SCREENS, MENU_ACTION_RESET_OVERLAY, MENU_ACTION_SHOW_OVERLAY, MENU_ACTION_OPEN_SETTINGS})
|
||||
public @interface MenuAction {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,464 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.activities
|
||||
|
||||
import android.Manifest.permission
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.InputDevice
|
||||
import android.view.KeyEvent
|
||||
import android.view.MotionEvent
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
import androidx.preference.PreferenceManager
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.camera.StillImageCameraHelper.OnFilePickerResult
|
||||
import org.citra.citra_emu.contracts.OpenFileResultContract
|
||||
import org.citra.citra_emu.databinding.ActivityEmulationBinding
|
||||
import org.citra.citra_emu.display.ScreenAdjustmentUtil
|
||||
import org.citra.citra_emu.features.hotkeys.HotkeyUtility
|
||||
import org.citra.citra_emu.features.settings.model.SettingsViewModel
|
||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
|
||||
import org.citra.citra_emu.fragments.MessageDialogFragment
|
||||
import org.citra.citra_emu.utils.ControllerMappingHelper
|
||||
import org.citra.citra_emu.utils.FileBrowserHelper
|
||||
import org.citra.citra_emu.utils.ForegroundService
|
||||
import org.citra.citra_emu.utils.EmulationLifecycleUtil
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings
|
||||
import org.citra.citra_emu.utils.ThemeUtil
|
||||
import org.citra.citra_emu.viewmodel.EmulationViewModel
|
||||
|
||||
class EmulationActivity : AppCompatActivity() {
|
||||
private val preferences: SharedPreferences
|
||||
get() = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
private var foregroundService: Intent? = null
|
||||
var isActivityRecreated = false
|
||||
|
||||
private val settingsViewModel: SettingsViewModel by viewModels()
|
||||
private val emulationViewModel: EmulationViewModel by viewModels()
|
||||
|
||||
private lateinit var binding: ActivityEmulationBinding
|
||||
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
|
||||
private lateinit var hotkeyUtility: HotkeyUtility
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
ThemeUtil.setTheme(this)
|
||||
|
||||
settingsViewModel.settings.loadSettings()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityEmulationBinding.inflate(layoutInflater)
|
||||
screenAdjustmentUtil = ScreenAdjustmentUtil(windowManager, settingsViewModel.settings)
|
||||
hotkeyUtility = HotkeyUtility(screenAdjustmentUtil)
|
||||
setContentView(binding.root)
|
||||
|
||||
val navHostFragment =
|
||||
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
|
||||
val navController = navHostFragment.navController
|
||||
navController.setGraph(R.navigation.emulation_navigation, intent.extras)
|
||||
|
||||
isActivityRecreated = savedInstanceState != null
|
||||
|
||||
// Set these options now so that the SurfaceView the game renders into is the right size.
|
||||
enableFullscreenImmersive()
|
||||
|
||||
// Override Citra core INI with the one set by our in game menu
|
||||
NativeLibrary.swapScreens(
|
||||
EmulationMenuSettings.swapScreens,
|
||||
windowManager.defaultDisplay.rotation
|
||||
)
|
||||
|
||||
// Start a foreground service to prevent the app from getting killed in the background
|
||||
foregroundService = Intent(this, ForegroundService::class.java)
|
||||
startForegroundService(foregroundService)
|
||||
|
||||
EmulationLifecycleUtil.addShutdownHook(hook = { this.finish() })
|
||||
}
|
||||
|
||||
// On some devices, the system bars will not disappear on first boot or after some
|
||||
// rotations. Here we set full screen immersive repeatedly in onResume and in
|
||||
// onWindowFocusChanged to prevent the unwanted status bar state.
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
enableFullscreenImmersive()
|
||||
}
|
||||
|
||||
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||
super.onWindowFocusChanged(hasFocus)
|
||||
enableFullscreenImmersive()
|
||||
}
|
||||
|
||||
public override fun onRestart() {
|
||||
super.onRestart()
|
||||
NativeLibrary.reloadCameraDevices()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
EmulationLifecycleUtil.clear()
|
||||
stopForegroundService(this)
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
when (requestCode) {
|
||||
NativeLibrary.REQUEST_CODE_NATIVE_CAMERA -> {
|
||||
if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
|
||||
shouldShowRequestPermissionRationale(permission.CAMERA)
|
||||
) {
|
||||
MessageDialogFragment.newInstance(
|
||||
R.string.camera,
|
||||
R.string.camera_permission_needed
|
||||
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
||||
}
|
||||
NativeLibrary.cameraPermissionResult(
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
)
|
||||
}
|
||||
|
||||
NativeLibrary.REQUEST_CODE_NATIVE_MIC -> {
|
||||
if (grantResults[0] != PackageManager.PERMISSION_GRANTED &&
|
||||
shouldShowRequestPermissionRationale(permission.RECORD_AUDIO)
|
||||
) {
|
||||
MessageDialogFragment.newInstance(
|
||||
R.string.microphone,
|
||||
R.string.microphone_permission_needed
|
||||
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
||||
}
|
||||
NativeLibrary.micPermissionResult(
|
||||
grantResults[0] == PackageManager.PERMISSION_GRANTED
|
||||
)
|
||||
}
|
||||
|
||||
else -> super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
}
|
||||
}
|
||||
|
||||
fun onEmulationStarted() {
|
||||
emulationViewModel.setEmulationStarted(true)
|
||||
Toast.makeText(
|
||||
applicationContext,
|
||||
getString(R.string.emulation_menu_help),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
|
||||
private fun enableFullscreenImmersive() {
|
||||
// TODO: Remove this once we properly account for display insets in the input overlay
|
||||
window.attributes.layoutInDisplayCutoutMode =
|
||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
WindowInsetsControllerCompat(window, window.decorView).let { controller ->
|
||||
controller.hide(WindowInsetsCompat.Type.systemBars())
|
||||
controller.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
}
|
||||
}
|
||||
|
||||
// Gets button presses
|
||||
@Suppress("DEPRECATION")
|
||||
@SuppressLint("GestureBackNavigation")
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
// TODO: Move this check into native code - prevents crash if input pressed before starting emulation
|
||||
if (!NativeLibrary.isRunning()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val button =
|
||||
preferences.getInt(InputBindingSetting.getInputButtonKey(event.keyCode), event.keyCode)
|
||||
val action: Int = when (event.action) {
|
||||
KeyEvent.ACTION_DOWN -> {
|
||||
// On some devices, the back gesture / button press is not intercepted by androidx
|
||||
// and fails to open the emulation menu. So we're stuck running deprecated code to
|
||||
// cover for either a fault on androidx's side or in OEM skins (MIUI at least)
|
||||
if (event.keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
onBackPressed()
|
||||
}
|
||||
|
||||
hotkeyUtility.handleHotkey(button)
|
||||
|
||||
// Normal key events.
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
}
|
||||
|
||||
KeyEvent.ACTION_UP -> NativeLibrary.ButtonState.RELEASED
|
||||
else -> return false
|
||||
}
|
||||
val input = event.device
|
||||
?: // Controller was disconnected
|
||||
return false
|
||||
return NativeLibrary.onGamePadEvent(input.descriptor, button, action)
|
||||
}
|
||||
|
||||
private fun onAmiiboSelected(selectedFile: String) {
|
||||
val success = NativeLibrary.loadAmiibo(selectedFile)
|
||||
if (!success) {
|
||||
MessageDialogFragment.newInstance(
|
||||
R.string.amiibo_load_error,
|
||||
R.string.amiibo_load_error_message
|
||||
).show(supportFragmentManager, MessageDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
|
||||
// TODO: Move this check into native code - prevents crash if input pressed before starting emulation
|
||||
if (!NativeLibrary.isRunning()) {
|
||||
return super.dispatchGenericMotionEvent(event)
|
||||
}
|
||||
|
||||
if (event.source and InputDevice.SOURCE_CLASS_JOYSTICK == 0) {
|
||||
return super.dispatchGenericMotionEvent(event)
|
||||
}
|
||||
|
||||
// Don't attempt to do anything if we are disconnecting a device.
|
||||
if (event.actionMasked == MotionEvent.ACTION_CANCEL) {
|
||||
return true
|
||||
}
|
||||
val input = event.device
|
||||
val motions = input.motionRanges
|
||||
val axisValuesCirclePad = floatArrayOf(0.0f, 0.0f)
|
||||
val axisValuesCStick = floatArrayOf(0.0f, 0.0f)
|
||||
val axisValuesDPad = floatArrayOf(0.0f, 0.0f)
|
||||
var isTriggerPressedLMapped = false
|
||||
var isTriggerPressedRMapped = false
|
||||
var isTriggerPressedZLMapped = false
|
||||
var isTriggerPressedZRMapped = false
|
||||
var isTriggerPressedL = false
|
||||
var isTriggerPressedR = false
|
||||
var isTriggerPressedZL = false
|
||||
var isTriggerPressedZR = false
|
||||
for (range in motions) {
|
||||
val axis = range.axis
|
||||
val origValue = event.getAxisValue(axis)
|
||||
var value = ControllerMappingHelper.scaleAxis(input, axis, origValue)
|
||||
val nextMapping =
|
||||
preferences.getInt(InputBindingSetting.getInputAxisButtonKey(axis), -1)
|
||||
val guestOrientation =
|
||||
preferences.getInt(InputBindingSetting.getInputAxisOrientationKey(axis), -1)
|
||||
if (nextMapping == -1 || guestOrientation == -1) {
|
||||
// Axis is unmapped
|
||||
continue
|
||||
}
|
||||
if (value > 0f && value < 0.1f || value < 0f && value > -0.1f) {
|
||||
// Skip joystick wobble
|
||||
value = 0f
|
||||
}
|
||||
when (nextMapping) {
|
||||
NativeLibrary.ButtonType.STICK_LEFT -> {
|
||||
axisValuesCirclePad[guestOrientation] = value
|
||||
}
|
||||
|
||||
NativeLibrary.ButtonType.STICK_C -> {
|
||||
axisValuesCStick[guestOrientation] = value
|
||||
}
|
||||
|
||||
NativeLibrary.ButtonType.DPAD -> {
|
||||
axisValuesDPad[guestOrientation] = value
|
||||
}
|
||||
|
||||
NativeLibrary.ButtonType.TRIGGER_L -> {
|
||||
isTriggerPressedLMapped = true
|
||||
isTriggerPressedL = value != 0f
|
||||
}
|
||||
|
||||
NativeLibrary.ButtonType.TRIGGER_R -> {
|
||||
isTriggerPressedRMapped = true
|
||||
isTriggerPressedR = value != 0f
|
||||
}
|
||||
|
||||
NativeLibrary.ButtonType.BUTTON_ZL -> {
|
||||
isTriggerPressedZLMapped = true
|
||||
isTriggerPressedZL = value != 0f
|
||||
}
|
||||
|
||||
NativeLibrary.ButtonType.BUTTON_ZR -> {
|
||||
isTriggerPressedZRMapped = true
|
||||
isTriggerPressedZR = value != 0f
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Circle-Pad and C-Stick status
|
||||
NativeLibrary.onGamePadMoveEvent(
|
||||
input.descriptor,
|
||||
NativeLibrary.ButtonType.STICK_LEFT,
|
||||
axisValuesCirclePad[0],
|
||||
axisValuesCirclePad[1]
|
||||
)
|
||||
NativeLibrary.onGamePadMoveEvent(
|
||||
input.descriptor,
|
||||
NativeLibrary.ButtonType.STICK_C,
|
||||
axisValuesCStick[0],
|
||||
axisValuesCStick[1]
|
||||
)
|
||||
|
||||
// Triggers L/R and ZL/ZR
|
||||
if (isTriggerPressedLMapped) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.TRIGGER_L,
|
||||
if (isTriggerPressedL) {
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
} else {
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
}
|
||||
)
|
||||
}
|
||||
if (isTriggerPressedRMapped) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.TRIGGER_R,
|
||||
if (isTriggerPressedR) {
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
} else {
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
}
|
||||
)
|
||||
}
|
||||
if (isTriggerPressedZLMapped) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.BUTTON_ZL,
|
||||
if (isTriggerPressedZL) {
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
} else {
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
}
|
||||
)
|
||||
}
|
||||
if (isTriggerPressedZRMapped) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.BUTTON_ZR,
|
||||
if (isTriggerPressedZR) {
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
} else {
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Work-around to allow D-pad axis to be bound to emulated buttons
|
||||
if (axisValuesDPad[0] == 0f) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_LEFT,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_RIGHT,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
)
|
||||
}
|
||||
if (axisValuesDPad[0] < 0f) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_LEFT,
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_RIGHT,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
)
|
||||
}
|
||||
if (axisValuesDPad[0] > 0f) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_LEFT,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_RIGHT,
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
)
|
||||
}
|
||||
if (axisValuesDPad[1] == 0f) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_UP,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_DOWN,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
)
|
||||
}
|
||||
if (axisValuesDPad[1] < 0f) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_UP,
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_DOWN,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
)
|
||||
}
|
||||
if (axisValuesDPad[1] > 0f) {
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_UP,
|
||||
NativeLibrary.ButtonState.RELEASED
|
||||
)
|
||||
NativeLibrary.onGamePadEvent(
|
||||
NativeLibrary.TouchScreenDevice,
|
||||
NativeLibrary.ButtonType.DPAD_DOWN,
|
||||
NativeLibrary.ButtonState.PRESSED
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
val openFileLauncher =
|
||||
registerForActivityResult(OpenFileResultContract()) { result: Intent? ->
|
||||
if (result == null) return@registerForActivityResult
|
||||
val selectedFiles = FileBrowserHelper.getSelectedFiles(
|
||||
result, applicationContext, listOf<String>("bin")
|
||||
) ?: return@registerForActivityResult
|
||||
onAmiiboSelected(selectedFiles[0])
|
||||
}
|
||||
|
||||
val openImageLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.PickVisualMedia()) { result: Uri? ->
|
||||
if (result == null) {
|
||||
return@registerForActivityResult
|
||||
}
|
||||
|
||||
OnFilePickerResult(result.toString())
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun stopForegroundService(activity: Activity) {
|
||||
val startIntent = Intent(activity, ForegroundService::class.java)
|
||||
startIntent.action = ForegroundService.ACTION_STOP
|
||||
activity.startForegroundService(startIntent)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,119 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.adapters
|
||||
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.databinding.CardDriverOptionBinding
|
||||
import org.citra.citra_emu.utils.GpuDriverMetadata
|
||||
import org.citra.citra_emu.viewmodel.DriverViewModel
|
||||
import org.citra.citra_emu.utils.GpuDriverHelper
|
||||
|
||||
class DriverAdapter(private val driverViewModel: DriverViewModel) :
|
||||
ListAdapter<Pair<Uri, GpuDriverMetadata>, DriverAdapter.DriverViewHolder>(
|
||||
AsyncDifferConfig.Builder(DiffCallback()).build()
|
||||
) {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DriverViewHolder {
|
||||
val binding =
|
||||
CardDriverOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return DriverViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = currentList.size
|
||||
|
||||
override fun onBindViewHolder(holder: DriverViewHolder, position: Int) =
|
||||
holder.bind(currentList[position])
|
||||
|
||||
private fun onSelectDriver(position: Int) {
|
||||
driverViewModel.setSelectedDriverIndex(position)
|
||||
notifyItemChanged(driverViewModel.previouslySelectedDriver)
|
||||
notifyItemChanged(driverViewModel.selectedDriver)
|
||||
}
|
||||
|
||||
private fun onDeleteDriver(driverData: Pair<Uri, GpuDriverMetadata>, position: Int) {
|
||||
if (driverViewModel.selectedDriver > position) {
|
||||
driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1)
|
||||
}
|
||||
if (GpuDriverHelper.customDriverData == driverData.second) {
|
||||
driverViewModel.setSelectedDriverIndex(0)
|
||||
}
|
||||
driverViewModel.driversToDelete.add(driverData.first)
|
||||
driverViewModel.removeDriver(driverData)
|
||||
notifyItemRemoved(position)
|
||||
notifyItemChanged(driverViewModel.selectedDriver)
|
||||
}
|
||||
|
||||
inner class DriverViewHolder(val binding: CardDriverOptionBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
private lateinit var driverData: Pair<Uri, GpuDriverMetadata>
|
||||
|
||||
fun bind(driverData: Pair<Uri, GpuDriverMetadata>) {
|
||||
this.driverData = driverData
|
||||
val driver = driverData.second
|
||||
|
||||
binding.apply {
|
||||
radioButton.isChecked = driverViewModel.selectedDriver == bindingAdapterPosition
|
||||
root.setOnClickListener {
|
||||
onSelectDriver(bindingAdapterPosition)
|
||||
}
|
||||
buttonDelete.setOnClickListener {
|
||||
onDeleteDriver(driverData, bindingAdapterPosition)
|
||||
}
|
||||
|
||||
// Delay marquee by 3s
|
||||
title.postDelayed(
|
||||
{
|
||||
title.isSelected = true
|
||||
title.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||
version.isSelected = true
|
||||
version.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||
description.isSelected = true
|
||||
description.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||
},
|
||||
3000
|
||||
)
|
||||
if (driver.name == null) {
|
||||
title.setText(R.string.system_gpu_driver)
|
||||
description.text = ""
|
||||
version.text = ""
|
||||
version.visibility = View.GONE
|
||||
description.visibility = View.GONE
|
||||
buttonDelete.visibility = View.GONE
|
||||
} else {
|
||||
title.text = driver.name
|
||||
version.text = driver.version
|
||||
description.text = driver.description
|
||||
version.visibility = View.VISIBLE
|
||||
description.visibility = View.VISIBLE
|
||||
buttonDelete.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<Pair<Uri, GpuDriverMetadata>>() {
|
||||
override fun areItemsTheSame(
|
||||
oldItem: Pair<Uri, GpuDriverMetadata>,
|
||||
newItem: Pair<Uri, GpuDriverMetadata>
|
||||
): Boolean {
|
||||
return oldItem.first == newItem.first
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItem: Pair<Uri, GpuDriverMetadata>,
|
||||
newItem: Pair<Uri, GpuDriverMetadata>
|
||||
): Boolean {
|
||||
return oldItem.second == newItem.second
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,261 +0,0 @@
|
|||
package org.citra.citra_emu.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.DataSetObserver;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.color.MaterialColors;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.activities.EmulationActivity;
|
||||
import org.citra.citra_emu.features.cheats.ui.CheatsActivity;
|
||||
import org.citra.citra_emu.model.GameDatabase;
|
||||
import org.citra.citra_emu.utils.FileUtil;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
import org.citra.citra_emu.utils.PicassoUtils;
|
||||
import org.citra.citra_emu.viewholders.GameViewHolder;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* This adapter gets its information from a database Cursor. This fact, paired with the usage of
|
||||
* ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly)
|
||||
* large dataset.
|
||||
*/
|
||||
public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> {
|
||||
private Cursor mCursor;
|
||||
private GameDataSetObserver mObserver;
|
||||
|
||||
private boolean mDatasetValid;
|
||||
private long mLastClickTime = 0;
|
||||
|
||||
/**
|
||||
* Initializes the adapter's observer, which watches for changes to the dataset. The adapter will
|
||||
* display no data until a Cursor is supplied by a CursorLoader.
|
||||
*/
|
||||
public GameAdapter() {
|
||||
mDatasetValid = false;
|
||||
mObserver = new GameDataSetObserver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the LayoutManager when it is necessary to create a new view.
|
||||
*
|
||||
* @param parent The RecyclerView (I think?) the created view will be thrown into.
|
||||
* @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView.
|
||||
* @return The created ViewHolder with references to all the child view's members.
|
||||
*/
|
||||
@Override
|
||||
public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
// Create a new view.
|
||||
View gameCard = LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.card_game, parent, false);
|
||||
|
||||
gameCard.setOnClickListener(this::onClick);
|
||||
gameCard.setOnLongClickListener(this::onLongClick);
|
||||
|
||||
// Use that view to create a ViewHolder.
|
||||
return new GameViewHolder(gameCard);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the LayoutManager when a new view is not necessary because we can recycle
|
||||
* an existing one (for example, if a view just scrolled onto the screen from the bottom, we
|
||||
* can use the view that just scrolled off the top instead of inflating a new one.)
|
||||
*
|
||||
* @param holder A ViewHolder representing the view we're recycling.
|
||||
* @param position The position of the 'new' view in the dataset.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull GameViewHolder holder, int position) {
|
||||
if (mDatasetValid) {
|
||||
if (mCursor.moveToPosition(position)) {
|
||||
PicassoUtils.loadGameIcon(holder.imageIcon,
|
||||
mCursor.getString(GameDatabase.GAME_COLUMN_PATH));
|
||||
|
||||
holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " "));
|
||||
holder.textCompany.setText(mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
|
||||
|
||||
String filepath = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
|
||||
String filename;
|
||||
if (FileUtil.isNativePath(filepath)) {
|
||||
filename = CitraApplication.documentsTree.getFilename(filepath);
|
||||
} else {
|
||||
filename = FileUtil.getFilename(CitraApplication.getAppContext(), filepath);
|
||||
}
|
||||
holder.textFileName.setText(filename);
|
||||
|
||||
// TODO These shouldn't be necessary once the move to a DB-based model is complete.
|
||||
holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID);
|
||||
holder.path = mCursor.getString(GameDatabase.GAME_COLUMN_PATH);
|
||||
holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE);
|
||||
holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION);
|
||||
holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS);
|
||||
holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_COMPANY);
|
||||
|
||||
final int backgroundColorId = isValidGame(holder.path) ? R.attr.colorSurface : R.attr.colorErrorContainer;
|
||||
View itemView = holder.getItemView();
|
||||
itemView.setBackgroundColor(MaterialColors.getColor(itemView, backgroundColorId));
|
||||
} else {
|
||||
Log.error("[GameAdapter] Can't bind view; Cursor is not valid.");
|
||||
}
|
||||
} else {
|
||||
Log.error("[GameAdapter] Can't bind view; dataset is not valid.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the LayoutManager to find out how much data we have.
|
||||
*
|
||||
* @return Size of the dataset.
|
||||
*/
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
if (mDatasetValid && mCursor != null) {
|
||||
return mCursor.getCount();
|
||||
}
|
||||
Log.error("[GameAdapter] Dataset is not valid.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contents of the _id column for a given row.
|
||||
*
|
||||
* @param position The row for which Android wants an ID.
|
||||
* @return A valid ID from the database, or 0 if not available.
|
||||
*/
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
if (mDatasetValid && mCursor != null) {
|
||||
if (mCursor.moveToPosition(position)) {
|
||||
return mCursor.getLong(GameDatabase.COLUMN_DB_ID);
|
||||
}
|
||||
}
|
||||
|
||||
Log.error("[GameAdapter] Dataset is not valid.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tell Android whether or not each item in the dataset has a stable identifier.
|
||||
* Which it does, because it's a database, so always tell Android 'true'.
|
||||
*
|
||||
* @param hasStableIds ignored.
|
||||
*/
|
||||
@Override
|
||||
public void setHasStableIds(boolean hasStableIds) {
|
||||
super.setHasStableIds(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* When a load is finished, call this to replace the existing data with the newly-loaded
|
||||
* data.
|
||||
*
|
||||
* @param cursor The newly-loaded Cursor.
|
||||
*/
|
||||
public void swapCursor(Cursor cursor) {
|
||||
// Sanity check.
|
||||
if (cursor == mCursor) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Before getting rid of the old cursor, disassociate it from the Observer.
|
||||
final Cursor oldCursor = mCursor;
|
||||
if (oldCursor != null && mObserver != null) {
|
||||
oldCursor.unregisterDataSetObserver(mObserver);
|
||||
}
|
||||
|
||||
mCursor = cursor;
|
||||
if (mCursor != null) {
|
||||
// Attempt to associate the new Cursor with the Observer.
|
||||
if (mObserver != null) {
|
||||
mCursor.registerDataSetObserver(mObserver);
|
||||
}
|
||||
|
||||
mDatasetValid = true;
|
||||
} else {
|
||||
mDatasetValid = false;
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the game that was clicked on.
|
||||
*
|
||||
* @param view The view representing the game the user wants to play.
|
||||
*/
|
||||
private void onClick(View view) {
|
||||
// Double-click prevention, using threshold of 1000 ms
|
||||
if (SystemClock.elapsedRealtime() - mLastClickTime < 1000) {
|
||||
return;
|
||||
}
|
||||
mLastClickTime = SystemClock.elapsedRealtime();
|
||||
|
||||
GameViewHolder holder = (GameViewHolder) view.getTag();
|
||||
|
||||
EmulationActivity.launch((FragmentActivity) view.getContext(), holder.path, holder.title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the cheats settings for the game that was clicked on.
|
||||
*
|
||||
* @param view The view representing the game the user wants to play.
|
||||
*/
|
||||
private boolean onLongClick(View view) {
|
||||
Context context = view.getContext();
|
||||
GameViewHolder holder = (GameViewHolder) view.getTag();
|
||||
|
||||
final long titleId = NativeLibrary.GetTitleId(holder.path);
|
||||
|
||||
if (titleId == 0) {
|
||||
new MaterialAlertDialogBuilder(context)
|
||||
.setIcon(R.mipmap.ic_launcher)
|
||||
.setTitle(R.string.properties)
|
||||
.setMessage(R.string.properties_not_loaded)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
} else {
|
||||
CheatsActivity.launch(context, titleId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean isValidGame(String path) {
|
||||
return Stream.of(
|
||||
".rar", ".zip", ".7z", ".torrent", ".tar", ".gz").noneMatch(suffix -> path.toLowerCase().endsWith(suffix));
|
||||
}
|
||||
|
||||
private final class GameDataSetObserver extends DataSetObserver {
|
||||
@Override
|
||||
public void onChanged() {
|
||||
super.onChanged();
|
||||
|
||||
mDatasetValid = true;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInvalidated() {
|
||||
super.onInvalidated();
|
||||
|
||||
mDatasetValid = false;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.adapters
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.SystemClock
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.findNavController
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.citra.citra_emu.HomeNavigationDirections
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.adapters.GameAdapter.GameViewHolder
|
||||
import org.citra.citra_emu.databinding.CardGameBinding
|
||||
import org.citra.citra_emu.features.cheats.ui.CheatsFragmentDirections
|
||||
import org.citra.citra_emu.model.Game
|
||||
import org.citra.citra_emu.utils.GameIconUtils
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
|
||||
class GameAdapter(private val activity: AppCompatActivity) :
|
||||
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
|
||||
View.OnClickListener, View.OnLongClickListener {
|
||||
private var lastClickTime = 0L
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
|
||||
// Create a new view.
|
||||
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
binding.cardGame.setOnClickListener(this)
|
||||
binding.cardGame.setOnLongClickListener(this)
|
||||
|
||||
// Use that view to create a ViewHolder.
|
||||
return GameViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
|
||||
holder.bind(currentList[position])
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = currentList.size
|
||||
|
||||
/**
|
||||
* Launches the game that was clicked on.
|
||||
*
|
||||
* @param view The card representing the game the user wants to play.
|
||||
*/
|
||||
override fun onClick(view: View) {
|
||||
// Double-click prevention, using threshold of 1000 ms
|
||||
if (SystemClock.elapsedRealtime() - lastClickTime < 1000) {
|
||||
return
|
||||
}
|
||||
lastClickTime = SystemClock.elapsedRealtime()
|
||||
|
||||
val holder = view.tag as GameViewHolder
|
||||
gameExists(holder)
|
||||
|
||||
val preferences =
|
||||
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
|
||||
preferences.edit()
|
||||
.putLong(
|
||||
holder.game.keyLastPlayedTime,
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
.apply()
|
||||
|
||||
val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game)
|
||||
view.findNavController().navigate(action)
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the cheats settings for the game that was clicked on.
|
||||
*
|
||||
* @param view The view representing the game the user wants to play.
|
||||
*/
|
||||
override fun onLongClick(view: View): Boolean {
|
||||
val context = view.context
|
||||
val holder = view.tag as GameViewHolder
|
||||
gameExists(holder)
|
||||
|
||||
if (holder.game.titleId == 0L) {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.properties)
|
||||
.setMessage(R.string.properties_not_loaded)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
} else {
|
||||
val action = CheatsFragmentDirections.actionGlobalCheatsFragment(holder.game.titleId)
|
||||
view.findNavController().navigate(action)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Triggers a library refresh if the user clicks on stale data
|
||||
private fun gameExists(holder: GameViewHolder): Boolean {
|
||||
if (holder.game.isInstalled) {
|
||||
return true
|
||||
}
|
||||
|
||||
val gameExists = DocumentFile.fromSingleUri(
|
||||
CitraApplication.appContext,
|
||||
Uri.parse(holder.game.path)
|
||||
)?.exists() == true
|
||||
return if (!gameExists) {
|
||||
Toast.makeText(
|
||||
CitraApplication.appContext,
|
||||
R.string.loader_error_file_not_found,
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
|
||||
ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
inner class GameViewHolder(val binding: CardGameBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
lateinit var game: Game
|
||||
|
||||
init {
|
||||
binding.cardGame.tag = this
|
||||
}
|
||||
|
||||
fun bind(game: Game) {
|
||||
this.game = game
|
||||
|
||||
binding.imageGameScreen.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
GameIconUtils.loadGameIcon(activity, game, binding.imageGameScreen)
|
||||
|
||||
binding.textGameTitle.visibility = if (game.title.isEmpty()) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
binding.textCompany.visibility = if (game.company.isEmpty()) {
|
||||
View.GONE
|
||||
} else {
|
||||
View.VISIBLE
|
||||
}
|
||||
|
||||
binding.textGameTitle.text = game.title
|
||||
binding.textCompany.text = game.company
|
||||
binding.textFilename.text = game.filename
|
||||
|
||||
val backgroundColorId =
|
||||
if (
|
||||
isValidGame(game.filename.substring(game.filename.lastIndexOf(".") + 1).lowercase())
|
||||
) {
|
||||
R.attr.colorSurface
|
||||
} else {
|
||||
R.attr.colorErrorContainer
|
||||
}
|
||||
binding.cardContents.setBackgroundColor(
|
||||
MaterialColors.getColor(
|
||||
binding.cardContents,
|
||||
backgroundColorId
|
||||
)
|
||||
)
|
||||
|
||||
binding.textGameTitle.postDelayed(
|
||||
{
|
||||
binding.textGameTitle.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||
binding.textGameTitle.isSelected = true
|
||||
|
||||
binding.textCompany.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||
binding.textCompany.isSelected = true
|
||||
|
||||
binding.textFilename.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||
binding.textFilename.isSelected = true
|
||||
},
|
||||
3000
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isValidGame(extension: String): Boolean {
|
||||
return Game.badExtensions.stream()
|
||||
.noneMatch { extension == it.lowercase() }
|
||||
}
|
||||
|
||||
private class DiffCallback : DiffUtil.ItemCallback<Game>() {
|
||||
override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
|
||||
return oldItem.titleId == newItem.titleId
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {
|
||||
return oldItem == newItem
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.adapters
|
||||
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.databinding.CardHomeOptionBinding
|
||||
import org.citra.citra_emu.fragments.MessageDialogFragment
|
||||
import org.citra.citra_emu.model.HomeSetting
|
||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||
|
||||
class HomeSettingAdapter(
|
||||
private val activity: AppCompatActivity,
|
||||
private val viewLifecycle: LifecycleOwner,
|
||||
var options: List<HomeSetting>
|
||||
) : RecyclerView.Adapter<HomeSettingAdapter.HomeOptionViewHolder>(), View.OnClickListener {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HomeOptionViewHolder {
|
||||
val binding =
|
||||
CardHomeOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
binding.root.setOnClickListener(this)
|
||||
return HomeOptionViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return options.size
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: HomeOptionViewHolder, position: Int) {
|
||||
holder.bind(options[position])
|
||||
}
|
||||
|
||||
override fun onClick(view: View) {
|
||||
val holder = view.tag as HomeOptionViewHolder
|
||||
if (holder.option.isEnabled.invoke()) {
|
||||
holder.option.onClick.invoke()
|
||||
} else {
|
||||
MessageDialogFragment.newInstance(
|
||||
holder.option.disabledTitleId,
|
||||
holder.option.disabledMessageId
|
||||
).show(activity.supportFragmentManager, MessageDialogFragment.TAG)
|
||||
}
|
||||
}
|
||||
|
||||
inner class HomeOptionViewHolder(val binding: CardHomeOptionBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
lateinit var option: HomeSetting
|
||||
|
||||
init {
|
||||
itemView.tag = this
|
||||
}
|
||||
|
||||
fun bind(option: HomeSetting) {
|
||||
this.option = option
|
||||
|
||||
binding.optionTitle.text = activity.resources.getString(option.titleId)
|
||||
binding.optionDescription.text = activity.resources.getString(option.descriptionId)
|
||||
binding.optionIcon.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(
|
||||
activity.resources,
|
||||
option.iconId,
|
||||
activity.theme
|
||||
)
|
||||
)
|
||||
|
||||
viewLifecycle.lifecycleScope.launch {
|
||||
viewLifecycle.repeatOnLifecycle(Lifecycle.State.CREATED) {
|
||||
option.details.collect { updateOptionDetails(it) }
|
||||
}
|
||||
}
|
||||
binding.optionDetail.postDelayed(
|
||||
{
|
||||
binding.optionDetail.ellipsize = TextUtils.TruncateAt.MARQUEE
|
||||
binding.optionDetail.isSelected = true
|
||||
},
|
||||
3000
|
||||
)
|
||||
|
||||
if (option.isEnabled.invoke()) {
|
||||
binding.optionTitle.alpha = 1f
|
||||
binding.optionDescription.alpha = 1f
|
||||
binding.optionIcon.alpha = 1f
|
||||
} else {
|
||||
binding.optionTitle.alpha = 0.5f
|
||||
binding.optionDescription.alpha = 0.5f
|
||||
binding.optionIcon.alpha = 0.5f
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateOptionDetails(detailString: String) {
|
||||
if (detailString != "") {
|
||||
binding.optionDetail.text = detailString
|
||||
binding.optionDetail.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.databinding.ListItemSettingBinding
|
||||
import org.citra.citra_emu.fragments.LicenseBottomSheetDialogFragment
|
||||
import org.citra.citra_emu.model.License
|
||||
|
||||
class LicenseAdapter(private val activity: AppCompatActivity, var licenses: List<License>) :
|
||||
RecyclerView.Adapter<LicenseAdapter.LicenseViewHolder>(),
|
||||
View.OnClickListener {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LicenseViewHolder {
|
||||
val binding =
|
||||
ListItemSettingBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
binding.root.setOnClickListener(this)
|
||||
return LicenseViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = licenses.size
|
||||
|
||||
override fun onBindViewHolder(holder: LicenseViewHolder, position: Int) {
|
||||
holder.bind(licenses[position])
|
||||
}
|
||||
|
||||
override fun onClick(view: View) {
|
||||
val license = (view.tag as LicenseViewHolder).license
|
||||
LicenseBottomSheetDialogFragment.newInstance(license)
|
||||
.show(activity.supportFragmentManager, LicenseBottomSheetDialogFragment.TAG)
|
||||
}
|
||||
|
||||
inner class LicenseViewHolder(val binding: ListItemSettingBinding) : ViewHolder(binding.root) {
|
||||
lateinit var license: License
|
||||
|
||||
init {
|
||||
itemView.tag = this
|
||||
}
|
||||
|
||||
fun bind(license: License) {
|
||||
this.license = license
|
||||
|
||||
val context = CitraApplication.appContext
|
||||
binding.textSettingName.text = context.getString(license.titleId)
|
||||
binding.textSettingDescription.text = context.getString(license.descriptionId)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.adapters
|
||||
|
||||
import android.text.Html
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.res.ResourcesCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import org.citra.citra_emu.databinding.PageSetupBinding
|
||||
import org.citra.citra_emu.model.SetupCallback
|
||||
import org.citra.citra_emu.model.SetupPage
|
||||
import org.citra.citra_emu.model.StepState
|
||||
import org.citra.citra_emu.utils.ViewUtils
|
||||
|
||||
class SetupAdapter(val activity: AppCompatActivity, val pages: List<SetupPage>) :
|
||||
RecyclerView.Adapter<SetupAdapter.SetupPageViewHolder>() {
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SetupPageViewHolder {
|
||||
val binding = PageSetupBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
||||
return SetupPageViewHolder(binding)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = pages.size
|
||||
|
||||
override fun onBindViewHolder(holder: SetupPageViewHolder, position: Int) =
|
||||
holder.bind(pages[position])
|
||||
|
||||
inner class SetupPageViewHolder(val binding: PageSetupBinding) :
|
||||
RecyclerView.ViewHolder(binding.root), SetupCallback {
|
||||
lateinit var page: SetupPage
|
||||
|
||||
init {
|
||||
itemView.tag = this
|
||||
}
|
||||
|
||||
fun bind(page: SetupPage) {
|
||||
this.page = page
|
||||
|
||||
if (page.stepCompleted.invoke() == StepState.STEP_COMPLETE) {
|
||||
onStepCompleted()
|
||||
}
|
||||
|
||||
binding.icon.setImageDrawable(
|
||||
ResourcesCompat.getDrawable(
|
||||
activity.resources,
|
||||
page.iconId,
|
||||
activity.theme
|
||||
)
|
||||
)
|
||||
binding.textTitle.text = activity.resources.getString(page.titleId)
|
||||
binding.textDescription.text =
|
||||
Html.fromHtml(activity.resources.getString(page.descriptionId), 0)
|
||||
binding.textDescription.movementMethod = LinkMovementMethod.getInstance()
|
||||
|
||||
binding.buttonAction.apply {
|
||||
text = activity.resources.getString(page.buttonTextId)
|
||||
if (page.buttonIconId != 0) {
|
||||
icon = ResourcesCompat.getDrawable(
|
||||
activity.resources,
|
||||
page.buttonIconId,
|
||||
activity.theme
|
||||
)
|
||||
}
|
||||
iconGravity =
|
||||
if (page.leftAlignedIcon) {
|
||||
MaterialButton.ICON_GRAVITY_START
|
||||
} else {
|
||||
MaterialButton.ICON_GRAVITY_END
|
||||
}
|
||||
setOnClickListener {
|
||||
page.buttonAction.invoke(this@SetupPageViewHolder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStepCompleted() {
|
||||
ViewUtils.hideView(binding.buttonAction, 200)
|
||||
ViewUtils.showView(binding.textConfirmation, 200)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,126 +0,0 @@
|
|||
// Copyright 2020 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.applets;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.activities.EmulationActivity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Objects;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
public final class MiiSelector {
|
||||
public static class MiiSelectorConfig implements java.io.Serializable {
|
||||
public boolean enable_cancel_button;
|
||||
public String title;
|
||||
public long initially_selected_mii_index;
|
||||
// List of Miis to display
|
||||
public String[] mii_names;
|
||||
}
|
||||
|
||||
public static class MiiSelectorData {
|
||||
public long return_code;
|
||||
public int index;
|
||||
|
||||
private MiiSelectorData(long return_code, int index) {
|
||||
this.return_code = return_code;
|
||||
this.index = index;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MiiSelectorDialogFragment extends DialogFragment {
|
||||
static MiiSelectorDialogFragment newInstance(MiiSelectorConfig config) {
|
||||
MiiSelectorDialogFragment frag = new MiiSelectorDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putSerializable("config", config);
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity emulationActivity = Objects.requireNonNull(getActivity());
|
||||
|
||||
MiiSelectorConfig config =
|
||||
Objects.requireNonNull((MiiSelectorConfig) Objects.requireNonNull(getArguments())
|
||||
.getSerializable("config"));
|
||||
|
||||
// Note: we intentionally leave out the Standard Mii in the native code so that
|
||||
// the string can get translated
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
list.add(emulationActivity.getString(R.string.standard_mii));
|
||||
list.addAll(Arrays.asList(config.mii_names));
|
||||
|
||||
final int initialIndex = config.initially_selected_mii_index < list.size()
|
||||
? (int) config.initially_selected_mii_index
|
||||
: 0;
|
||||
data.index = initialIndex;
|
||||
MaterialAlertDialogBuilder builder =
|
||||
new MaterialAlertDialogBuilder(emulationActivity)
|
||||
.setTitle(config.title.isEmpty()
|
||||
? emulationActivity.getString(R.string.mii_selector)
|
||||
: config.title)
|
||||
.setSingleChoiceItems(list.toArray(new String[]{}), initialIndex,
|
||||
(dialog, which) -> {
|
||||
data.index = which;
|
||||
})
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
data.return_code = 0;
|
||||
synchronized (finishLock) {
|
||||
finishLock.notifyAll();
|
||||
}
|
||||
});
|
||||
if (config.enable_cancel_button) {
|
||||
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
|
||||
data.return_code = 1;
|
||||
synchronized (finishLock) {
|
||||
finishLock.notifyAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
setCancelable(false);
|
||||
return builder.create();
|
||||
}
|
||||
}
|
||||
|
||||
private static MiiSelectorData data;
|
||||
private static final Object finishLock = new Object();
|
||||
|
||||
private static void ExecuteImpl(MiiSelectorConfig config) {
|
||||
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
|
||||
|
||||
data = new MiiSelectorData(0, 0);
|
||||
|
||||
MiiSelectorDialogFragment fragment = MiiSelectorDialogFragment.newInstance(config);
|
||||
fragment.show(emulationActivity.getSupportFragmentManager(), "mii_selector");
|
||||
}
|
||||
|
||||
public static MiiSelectorData Execute(MiiSelectorConfig config) {
|
||||
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config));
|
||||
|
||||
synchronized (finishLock) {
|
||||
try {
|
||||
finishLock.wait();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.applets
|
||||
|
||||
import androidx.annotation.Keep
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.fragments.MiiSelectorDialogFragment
|
||||
import java.io.Serializable
|
||||
|
||||
@Keep
|
||||
object MiiSelector {
|
||||
lateinit var data: MiiSelectorData
|
||||
val finishLock = Object()
|
||||
|
||||
private fun ExecuteImpl(config: MiiSelectorConfig) {
|
||||
val emulationActivity = NativeLibrary.sEmulationActivity.get()
|
||||
data = MiiSelectorData(0, 0)
|
||||
val fragment = MiiSelectorDialogFragment.newInstance(config)
|
||||
fragment.show(emulationActivity!!.supportFragmentManager, "mii_selector")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun Execute(config: MiiSelectorConfig): MiiSelectorData {
|
||||
NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { ExecuteImpl(config) }
|
||||
synchronized(finishLock) {
|
||||
try {
|
||||
finishLock.wait()
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
@Keep
|
||||
class MiiSelectorConfig : Serializable {
|
||||
var enableCancelButton = false
|
||||
var title: String? = null
|
||||
var initiallySelectedMiiIndex: Long = 0
|
||||
|
||||
// List of Miis to display
|
||||
lateinit var miiNames: Array<String>
|
||||
}
|
||||
|
||||
class MiiSelectorData (var returnCode: Long, var index: Int)
|
||||
}
|
|
@ -1,266 +0,0 @@
|
|||
// Copyright 2020 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.applets;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.text.InputFilter;
|
||||
import android.text.Spanned;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.citra.citra_emu.CitraApplication;
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.activities.EmulationActivity;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class SoftwareKeyboard {
|
||||
/// Corresponds to Frontend::ButtonConfig
|
||||
private interface ButtonConfig {
|
||||
int Single = 0; /// Ok button
|
||||
int Dual = 1; /// Cancel | Ok buttons
|
||||
int Triple = 2; /// Cancel | I Forgot | Ok buttons
|
||||
int None = 3; /// No button (returned by swkbdInputText in special cases)
|
||||
}
|
||||
|
||||
/// Corresponds to Frontend::ValidationError
|
||||
public enum ValidationError {
|
||||
None,
|
||||
// Button Selection
|
||||
ButtonOutOfRange,
|
||||
// Configured Filters
|
||||
MaxDigitsExceeded,
|
||||
AtSignNotAllowed,
|
||||
PercentNotAllowed,
|
||||
BackslashNotAllowed,
|
||||
ProfanityNotAllowed,
|
||||
CallbackFailed,
|
||||
// Allowed Input Type
|
||||
FixedLengthRequired,
|
||||
MaxLengthExceeded,
|
||||
BlankInputNotAllowed,
|
||||
EmptyInputNotAllowed,
|
||||
}
|
||||
|
||||
public static class KeyboardConfig implements java.io.Serializable {
|
||||
public int button_config;
|
||||
public int max_text_length;
|
||||
public boolean multiline_mode; /// True if the keyboard accepts multiple lines of input
|
||||
public String hint_text; /// Displayed in the field as a hint before
|
||||
@Nullable
|
||||
public String[] button_text; /// Contains the button text that the caller provides
|
||||
}
|
||||
|
||||
/// Corresponds to Frontend::KeyboardData
|
||||
public static class KeyboardData {
|
||||
public int button;
|
||||
public String text;
|
||||
|
||||
private KeyboardData(int button, String text) {
|
||||
this.button = button;
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
private static class Filter implements InputFilter {
|
||||
@Override
|
||||
public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
|
||||
int dstart, int dend) {
|
||||
String text = new StringBuilder(dest)
|
||||
.replace(dstart, dend, source.subSequence(start, end).toString())
|
||||
.toString();
|
||||
if (ValidateFilters(text) == ValidationError.None) {
|
||||
return null; // Accept replacement
|
||||
}
|
||||
return dest.subSequence(dstart, dend); // Request the subsequence to be unchanged
|
||||
}
|
||||
}
|
||||
|
||||
public static class KeyboardDialogFragment extends DialogFragment {
|
||||
static KeyboardDialogFragment newInstance(KeyboardConfig config) {
|
||||
KeyboardDialogFragment frag = new KeyboardDialogFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putSerializable("config", config);
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final Activity emulationActivity = getActivity();
|
||||
assert emulationActivity != null;
|
||||
|
||||
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
params.leftMargin = params.rightMargin =
|
||||
CitraApplication.getAppContext().getResources().getDimensionPixelSize(
|
||||
R.dimen.dialog_margin);
|
||||
|
||||
KeyboardConfig config = Objects.requireNonNull(
|
||||
(KeyboardConfig) Objects.requireNonNull(getArguments()).getSerializable("config"));
|
||||
|
||||
// Set up the input
|
||||
EditText editText = new EditText(CitraApplication.getAppContext());
|
||||
editText.setHint(config.hint_text);
|
||||
editText.setSingleLine(!config.multiline_mode);
|
||||
editText.setLayoutParams(params);
|
||||
editText.setFilters(new InputFilter[]{
|
||||
new Filter(), new InputFilter.LengthFilter(config.max_text_length)});
|
||||
|
||||
FrameLayout container = new FrameLayout(emulationActivity);
|
||||
container.addView(editText);
|
||||
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
|
||||
.setTitle(R.string.software_keyboard)
|
||||
.setView(container);
|
||||
setCancelable(false);
|
||||
|
||||
switch (config.button_config) {
|
||||
case ButtonConfig.Triple: {
|
||||
final String text = config.button_text[1].isEmpty()
|
||||
? emulationActivity.getString(R.string.i_forgot)
|
||||
: config.button_text[1];
|
||||
builder.setNeutralButton(text, null);
|
||||
}
|
||||
// fallthrough
|
||||
case ButtonConfig.Dual: {
|
||||
final String text = config.button_text[0].isEmpty()
|
||||
? emulationActivity.getString(android.R.string.cancel)
|
||||
: config.button_text[0];
|
||||
builder.setNegativeButton(text, null);
|
||||
}
|
||||
// fallthrough
|
||||
case ButtonConfig.Single: {
|
||||
final String text = config.button_text[2].isEmpty()
|
||||
? emulationActivity.getString(android.R.string.ok)
|
||||
: config.button_text[2];
|
||||
builder.setPositiveButton(text, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final AlertDialog dialog = builder.create();
|
||||
dialog.create();
|
||||
if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) {
|
||||
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> {
|
||||
data.button = config.button_config;
|
||||
data.text = editText.getText().toString();
|
||||
final ValidationError error = ValidateInput(data.text);
|
||||
if (error != ValidationError.None) {
|
||||
HandleValidationError(config, error);
|
||||
return;
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
|
||||
synchronized (finishLock) {
|
||||
finishLock.notifyAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) {
|
||||
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> {
|
||||
data.button = 1;
|
||||
dialog.dismiss();
|
||||
synchronized (finishLock) {
|
||||
finishLock.notifyAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) {
|
||||
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> {
|
||||
data.button = 0;
|
||||
dialog.dismiss();
|
||||
synchronized (finishLock) {
|
||||
finishLock.notifyAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
|
||||
private static KeyboardData data;
|
||||
private static final Object finishLock = new Object();
|
||||
|
||||
private static void ExecuteImpl(KeyboardConfig config) {
|
||||
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
|
||||
|
||||
data = new KeyboardData(0, "");
|
||||
|
||||
KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config);
|
||||
fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard");
|
||||
}
|
||||
|
||||
private static void HandleValidationError(KeyboardConfig config, ValidationError error) {
|
||||
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
|
||||
String message = "";
|
||||
switch (error) {
|
||||
case FixedLengthRequired:
|
||||
message =
|
||||
emulationActivity.getString(R.string.fixed_length_required, config.max_text_length);
|
||||
break;
|
||||
case MaxLengthExceeded:
|
||||
message =
|
||||
emulationActivity.getString(R.string.max_length_exceeded, config.max_text_length);
|
||||
break;
|
||||
case BlankInputNotAllowed:
|
||||
message = emulationActivity.getString(R.string.blank_input_not_allowed);
|
||||
break;
|
||||
case EmptyInputNotAllowed:
|
||||
message = emulationActivity.getString(R.string.empty_input_not_allowed);
|
||||
break;
|
||||
}
|
||||
|
||||
new MaterialAlertDialogBuilder(emulationActivity)
|
||||
.setTitle(R.string.software_keyboard)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
public static KeyboardData Execute(KeyboardConfig config) {
|
||||
if (config.button_config == ButtonConfig.None) {
|
||||
Log.error("Unexpected button config None");
|
||||
return new KeyboardData(0, "");
|
||||
}
|
||||
|
||||
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config));
|
||||
|
||||
synchronized (finishLock) {
|
||||
try {
|
||||
finishLock.wait();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
public static void ShowError(String error) {
|
||||
NativeLibrary.displayAlertMsg(
|
||||
CitraApplication.getAppContext().getResources().getString(R.string.software_keyboard),
|
||||
error, false);
|
||||
}
|
||||
|
||||
private static native ValidationError ValidateFilters(String text);
|
||||
|
||||
private static native ValidationError ValidateInput(String text);
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.applets
|
||||
|
||||
import android.text.InputFilter
|
||||
import android.text.Spanned
|
||||
import androidx.annotation.Keep
|
||||
import org.citra.citra_emu.CitraApplication.Companion.appContext
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.R
|
||||
import org.citra.citra_emu.fragments.KeyboardDialogFragment
|
||||
import org.citra.citra_emu.fragments.MessageDialogFragment
|
||||
import org.citra.citra_emu.utils.Log
|
||||
import java.io.Serializable
|
||||
|
||||
@Keep
|
||||
object SoftwareKeyboard {
|
||||
lateinit var data: KeyboardData
|
||||
val finishLock = Object()
|
||||
|
||||
private fun ExecuteImpl(config: KeyboardConfig) {
|
||||
val emulationActivity = NativeLibrary.sEmulationActivity.get()
|
||||
data = KeyboardData(0, "")
|
||||
KeyboardDialogFragment.newInstance(config)
|
||||
.show(emulationActivity!!.supportFragmentManager, KeyboardDialogFragment.TAG)
|
||||
}
|
||||
|
||||
fun HandleValidationError(config: KeyboardConfig, error: ValidationError) {
|
||||
val emulationActivity = NativeLibrary.sEmulationActivity.get()!!
|
||||
val message: String = when (error) {
|
||||
ValidationError.FixedLengthRequired -> emulationActivity.getString(
|
||||
R.string.fixed_length_required,
|
||||
config.maxTextLength
|
||||
)
|
||||
|
||||
ValidationError.MaxLengthExceeded ->
|
||||
emulationActivity.getString(R.string.max_length_exceeded, config.maxTextLength)
|
||||
|
||||
ValidationError.BlankInputNotAllowed ->
|
||||
emulationActivity.getString(R.string.blank_input_not_allowed)
|
||||
|
||||
ValidationError.EmptyInputNotAllowed ->
|
||||
emulationActivity.getString(R.string.empty_input_not_allowed)
|
||||
|
||||
else -> emulationActivity.getString(R.string.invalid_input)
|
||||
}
|
||||
|
||||
MessageDialogFragment.newInstance(R.string.software_keyboard, message).show(
|
||||
NativeLibrary.sEmulationActivity.get()!!.supportFragmentManager,
|
||||
MessageDialogFragment.TAG
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun Execute(config: KeyboardConfig): KeyboardData {
|
||||
if (config.buttonConfig == ButtonConfig.None) {
|
||||
Log.error("Unexpected button config None")
|
||||
return KeyboardData(0, "")
|
||||
}
|
||||
NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { ExecuteImpl(config) }
|
||||
synchronized(finishLock) {
|
||||
try {
|
||||
finishLock.wait()
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun ShowError(error: String) {
|
||||
NativeLibrary.displayAlertMsg(
|
||||
appContext.resources.getString(R.string.software_keyboard),
|
||||
error,
|
||||
false
|
||||
)
|
||||
}
|
||||
|
||||
private external fun ValidateFilters(text: String): ValidationError
|
||||
external fun ValidateInput(text: String): ValidationError
|
||||
|
||||
/// Corresponds to Frontend::ButtonConfig
|
||||
interface ButtonConfig {
|
||||
companion object {
|
||||
const val Single = 0 /// Ok button
|
||||
const val Dual = 1 /// Cancel | Ok buttons
|
||||
const val Triple = 2 /// Cancel | I Forgot | Ok buttons
|
||||
const val None = 3 /// No button (returned by swkbdInputText in special cases)
|
||||
}
|
||||
}
|
||||
|
||||
/// Corresponds to Frontend::ValidationError
|
||||
enum class ValidationError {
|
||||
None,
|
||||
|
||||
// Button Selection
|
||||
ButtonOutOfRange,
|
||||
|
||||
// Configured Filters
|
||||
MaxDigitsExceeded,
|
||||
AtSignNotAllowed,
|
||||
PercentNotAllowed,
|
||||
BackslashNotAllowed,
|
||||
ProfanityNotAllowed,
|
||||
CallbackFailed,
|
||||
|
||||
// Allowed Input Type
|
||||
FixedLengthRequired,
|
||||
MaxLengthExceeded,
|
||||
BlankInputNotAllowed,
|
||||
EmptyInputNotAllowed
|
||||
}
|
||||
|
||||
@Keep
|
||||
class KeyboardConfig : Serializable {
|
||||
var buttonConfig = 0
|
||||
var maxTextLength = 0
|
||||
|
||||
// True if the keyboard accepts multiple lines of input
|
||||
var multilineMode = false
|
||||
|
||||
// Displayed in the field as a hint before
|
||||
var hintText: String? = null
|
||||
|
||||
// Contains the button text that the caller provides
|
||||
lateinit var buttonText: Array<String>
|
||||
}
|
||||
|
||||
/// Corresponds to Frontend::KeyboardData
|
||||
class KeyboardData(var button: Int, var text: String)
|
||||
class Filter : InputFilter {
|
||||
override fun filter(
|
||||
source: CharSequence,
|
||||
start: Int,
|
||||
end: Int,
|
||||
dest: Spanned,
|
||||
dstart: Int,
|
||||
dend: Int
|
||||
): CharSequence? {
|
||||
val text = StringBuilder(dest)
|
||||
.replace(dstart, dend, source.subSequence(start, end).toString())
|
||||
.toString()
|
||||
return if (ValidateFilters(text) == ValidationError.None) {
|
||||
null // Accept replacement
|
||||
} else {
|
||||
dest.subSequence(dstart, dend) // Request the subsequence to be unchanged
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
// Copyright 2020 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.camera;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
import org.citra.citra_emu.NativeLibrary;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.activities.EmulationActivity;
|
||||
import org.citra.citra_emu.utils.PicassoUtils;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
// Used in native code.
|
||||
public final class StillImageCameraHelper {
|
||||
public static final int REQUEST_CAMERA_FILE_PICKER = 1;
|
||||
private static final Object filePickerLock = new Object();
|
||||
private static @Nullable
|
||||
String filePickerPath;
|
||||
|
||||
// Opens file picker for camera.
|
||||
public static @Nullable
|
||||
String OpenFilePicker() {
|
||||
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
|
||||
|
||||
// At this point, we are assuming that we already have permissions as they are
|
||||
// needed to launch a game
|
||||
emulationActivity.runOnUiThread(() -> {
|
||||
Intent intent = new Intent(Intent.ACTION_PICK);
|
||||
intent.setDataAndType(MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||
emulationActivity.startActivityForResult(
|
||||
Intent.createChooser(intent,
|
||||
emulationActivity.getString(R.string.camera_select_image)),
|
||||
REQUEST_CAMERA_FILE_PICKER);
|
||||
});
|
||||
|
||||
synchronized (filePickerLock) {
|
||||
try {
|
||||
filePickerLock.wait();
|
||||
} catch (InterruptedException ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
return filePickerPath;
|
||||
}
|
||||
|
||||
// Called from EmulationActivity.
|
||||
public static void OnFilePickerResult(Intent result) {
|
||||
filePickerPath = result == null ? null : result.getDataString();
|
||||
|
||||
synchronized (filePickerLock) {
|
||||
filePickerLock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
// Blocking call. Load image from file and crop/resize it to fit in width x height.
|
||||
@Nullable
|
||||
public static Bitmap LoadImageFromFile(String uri, int width, int height) {
|
||||
return PicassoUtils.LoadBitmapFromFile(uri, width, height);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.camera
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.activity.result.PickVisualMediaRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.Keep
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import coil.executeBlocking
|
||||
import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import org.citra.citra_emu.CitraApplication
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
|
||||
// Used in native code.
|
||||
object StillImageCameraHelper {
|
||||
private val filePickerLock = Object()
|
||||
private var filePickerPath: String? = null
|
||||
|
||||
// Opens file picker for camera.
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun OpenFilePicker(): String? {
|
||||
val emulationActivity = NativeLibrary.sEmulationActivity.get()
|
||||
|
||||
// At this point, we are assuming that we already have permissions as they are
|
||||
// needed to launch a game
|
||||
emulationActivity!!.runOnUiThread {
|
||||
val request = PickVisualMediaRequest.Builder()
|
||||
.setMediaType(ActivityResultContracts.PickVisualMedia.ImageOnly).build()
|
||||
emulationActivity.openImageLauncher.launch(request)
|
||||
}
|
||||
synchronized(filePickerLock) {
|
||||
try {
|
||||
filePickerLock.wait()
|
||||
} catch (ignored: InterruptedException) {
|
||||
}
|
||||
}
|
||||
return filePickerPath
|
||||
}
|
||||
|
||||
// Called from EmulationActivity.
|
||||
@JvmStatic
|
||||
fun OnFilePickerResult(result: String) {
|
||||
filePickerPath = result
|
||||
synchronized(filePickerLock) { filePickerLock.notifyAll() }
|
||||
}
|
||||
|
||||
// Blocking call. Load image from file and crop/resize it to fit in width x height.
|
||||
@Keep
|
||||
@JvmStatic
|
||||
fun LoadImageFromFile(uri: String?, width: Int, height: Int): Bitmap? {
|
||||
val context = CitraApplication.appContext
|
||||
val request = ImageRequest.Builder(context)
|
||||
.data(uri)
|
||||
.size(width, height)
|
||||
.build()
|
||||
return context.imageLoader.executeBlocking(request).drawable?.toBitmap(
|
||||
width,
|
||||
height,
|
||||
Bitmap.Config.ARGB_8888
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
package org.citra.citra_emu.contracts;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.activity.result.contract.ActivityResultContract;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class OpenFileResultContract extends ActivityResultContract<Boolean, Intent> {
|
||||
@NonNull
|
||||
@Override
|
||||
public Intent createIntent(@NonNull Context context, Boolean allowMultiple) {
|
||||
return new Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
.setType("application/octet-stream")
|
||||
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Intent parseResult(int i, @Nullable Intent intent) {
|
||||
return intent;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.contracts
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.activity.result.contract.ActivityResultContract
|
||||
|
||||
class OpenFileResultContract : ActivityResultContract<Boolean?, Intent?>() {
|
||||
override fun createIntent(context: Context, input: Boolean?): Intent {
|
||||
return Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
.setType("application/octet-stream")
|
||||
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, input)
|
||||
}
|
||||
|
||||
override fun parseResult(resultCode: Int, intent: Intent?): Intent? = intent
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
package org.citra.citra_emu.dialogs;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import java.util.Objects;
|
||||
import org.citra.citra_emu.R;
|
||||
import org.citra.citra_emu.utils.FileUtil;
|
||||
import org.citra.citra_emu.utils.PermissionsHandler;
|
||||
|
||||
public class CitraDirectoryDialog extends DialogFragment {
|
||||
public static final String TAG = "citra_directory_dialog_fragment";
|
||||
|
||||
private static final String MOVE_DATE_ENABLE = "IS_MODE_DATA_ENABLE";
|
||||
|
||||
TextView pathView;
|
||||
|
||||
TextView spaceView;
|
||||
|
||||
CheckBox checkBox;
|
||||
|
||||
AlertDialog dialog;
|
||||
|
||||
Listener listener;
|
||||
|
||||
public interface Listener {
|
||||
void onPressPositiveButton(boolean moveData, Uri path);
|
||||
}
|
||||
|
||||
public static CitraDirectoryDialog newInstance(String path, Listener listener) {
|
||||
CitraDirectoryDialog frag = new CitraDirectoryDialog();
|
||||
frag.listener = listener;
|
||||
Bundle args = new Bundle();
|
||||
args.putString("path", path);
|
||||
frag.setArguments(args);
|
||||
return frag;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final FragmentActivity activity = requireActivity();
|
||||
final Uri path = Uri.parse(Objects.requireNonNull(requireArguments().getString("path")));
|
||||
SharedPreferences mPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||
String freeSpaceText =
|
||||
getResources().getString(R.string.free_space, FileUtil.getFreeSpace(activity, path));
|
||||
|
||||
LayoutInflater inflater = getLayoutInflater();
|
||||
View view = inflater.inflate(R.layout.dialog_citra_directory, null);
|
||||
|
||||
checkBox = view.findViewById(R.id.checkBox);
|
||||
pathView = view.findViewById(R.id.path);
|
||||
spaceView = view.findViewById(R.id.space);
|
||||
|
||||
checkBox.setChecked(mPreferences.getBoolean(MOVE_DATE_ENABLE, true));
|
||||
if (!PermissionsHandler.hasWriteAccess(activity)) {
|
||||
checkBox.setVisibility(View.GONE);
|
||||
}
|
||||
checkBox.setOnCheckedChangeListener(
|
||||
(v, isChecked)
|
||||
// record move data selection with SharedPreferences
|
||||
-> mPreferences.edit().putBoolean(MOVE_DATE_ENABLE, checkBox.isChecked()).apply());
|
||||
|
||||
pathView.setText(path.getPath());
|
||||
spaceView.setText(freeSpaceText);
|
||||
|
||||
setCancelable(false);
|
||||
|
||||
dialog = new MaterialAlertDialogBuilder(activity)
|
||||
.setView(view)
|
||||
.setIcon(R.mipmap.ic_launcher)
|
||||
.setTitle(R.string.app_name)
|
||||
.setPositiveButton(
|
||||
android.R.string.ok,
|
||||
(d, v) -> listener.onPressPositiveButton(checkBox.isChecked(), path))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
return dialog;
|
||||
}
|
||||
}
|
|
@ -1,61 +0,0 @@
|
|||
package org.citra.citra_emu.dialogs;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import org.citra.citra_emu.R;
|
||||
|
||||
public class CopyDirProgressDialog extends DialogFragment {
|
||||
public static final String TAG = "copy_dir_progress_dialog";
|
||||
ProgressBar progressBar;
|
||||
|
||||
TextView progressText;
|
||||
|
||||
AlertDialog dialog;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
final FragmentActivity activity = requireActivity();
|
||||
|
||||
LayoutInflater inflater = getLayoutInflater();
|
||||
View view = inflater.inflate(R.layout.dialog_progress_bar, null);
|
||||
|
||||
progressBar = view.findViewById(R.id.progress_bar);
|
||||
progressText = view.findViewById(R.id.progress_text);
|
||||
progressText.setText("");
|
||||
|
||||
setCancelable(false);
|
||||
|
||||
dialog = new MaterialAlertDialogBuilder(activity)
|
||||
.setView(view)
|
||||
.setIcon(R.mipmap.ic_launcher)
|
||||
.setTitle(R.string.move_data)
|
||||
.setMessage("")
|
||||
.create();
|
||||
return dialog;
|
||||
}
|
||||
|
||||
public void onUpdateSearchProgress(String msg) {
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
dialog.setMessage(getResources().getString(R.string.searching_direcotry, msg));
|
||||
});
|
||||
}
|
||||
|
||||
public void onUpdateCopyProgress(String msg, int progress, int max) {
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
progressBar.setProgress(progress);
|
||||
progressBar.setMax(max);
|
||||
progressText.setText(String.format("%d/%d", progress, max));
|
||||
dialog.setMessage(getResources().getString(R.string.copy_file_name, msg));
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,140 +0,0 @@
|
|||
package org.citra.citra_emu.dialogs;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting;
|
||||
import org.citra.citra_emu.utils.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link AlertDialog} derivative that listens for
|
||||
* motion events from controllers and joysticks.
|
||||
*/
|
||||
public final class MotionAlertDialog extends AlertDialog {
|
||||
// The selected input preference
|
||||
private final InputBindingSetting setting;
|
||||
private final ArrayList<Float> mPreviousValues = new ArrayList<>();
|
||||
private int mPrevDeviceId = 0;
|
||||
private boolean mWaitingForEvent = true;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param context The current {@link Context}.
|
||||
* @param setting The Preference to show this dialog for.
|
||||
*/
|
||||
public MotionAlertDialog(Context context, InputBindingSetting setting) {
|
||||
super(context);
|
||||
|
||||
this.setting = setting;
|
||||
}
|
||||
|
||||
public boolean onKeyEvent(int keyCode, KeyEvent event) {
|
||||
Log.debug("[MotionAlertDialog] Received key event: " + event.getAction());
|
||||
switch (event.getAction()) {
|
||||
case KeyEvent.ACTION_UP:
|
||||
setting.onKeyInput(event);
|
||||
dismiss();
|
||||
// Even if we ignore the key, we still consume it. Thus return true regardless.
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyLongPress(int keyCode, @NonNull KeyEvent event) {
|
||||
return super.onKeyLongPress(keyCode, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
// Handle this key if we care about it, otherwise pass it down the framework
|
||||
return onKeyEvent(event.getKeyCode(), event) || super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchGenericMotionEvent(@NonNull MotionEvent event) {
|
||||
// Handle this event if we care about it, otherwise pass it down the framework
|
||||
return onMotionEvent(event) || super.dispatchGenericMotionEvent(event);
|
||||
}
|
||||
|
||||
private boolean onMotionEvent(MotionEvent event) {
|
||||
if ((event.getSource() & InputDevice.SOURCE_CLASS_JOYSTICK) == 0)
|
||||
return false;
|
||||
if (event.getAction() != MotionEvent.ACTION_MOVE)
|
||||
return false;
|
||||
|
||||
InputDevice input = event.getDevice();
|
||||
|
||||
List<InputDevice.MotionRange> motionRanges = input.getMotionRanges();
|
||||
|
||||
if (input.getId() != mPrevDeviceId) {
|
||||
mPreviousValues.clear();
|
||||
}
|
||||
mPrevDeviceId = input.getId();
|
||||
boolean firstEvent = mPreviousValues.isEmpty();
|
||||
|
||||
int numMovedAxis = 0;
|
||||
float axisMoveValue = 0.0f;
|
||||
InputDevice.MotionRange lastMovedRange = null;
|
||||
char lastMovedDir = '?';
|
||||
if (mWaitingForEvent) {
|
||||
for (int i = 0; i < motionRanges.size(); i++) {
|
||||
InputDevice.MotionRange range = motionRanges.get(i);
|
||||
int axis = range.getAxis();
|
||||
float origValue = event.getAxisValue(axis);
|
||||
float value = origValue;//ControllerMappingHelper.scaleAxis(input, axis, origValue);
|
||||
if (firstEvent) {
|
||||
mPreviousValues.add(value);
|
||||
} else {
|
||||
float previousValue = mPreviousValues.get(i);
|
||||
|
||||
// Only handle the axes that are not neutral (more than 0.5)
|
||||
// but ignore any axis that has a constant value (e.g. always 1)
|
||||
if (Math.abs(value) > 0.5f && value != previousValue) {
|
||||
// It is common to have multiple axes with the same physical input. For example,
|
||||
// shoulder butters are provided as both AXIS_LTRIGGER and AXIS_BRAKE.
|
||||
// To handle this, we ignore an axis motion that's the exact same as a motion
|
||||
// we already saw. This way, we ignore axes with two names, but catch the case
|
||||
// where a joystick is moved in two directions.
|
||||
// ref: bottom of https://developer.android.com/training/game-controllers/controller-input.html
|
||||
if (value != axisMoveValue) {
|
||||
axisMoveValue = value;
|
||||
numMovedAxis++;
|
||||
lastMovedRange = range;
|
||||
lastMovedDir = value < 0.0f ? '-' : '+';
|
||||
}
|
||||
}
|
||||
// Special case for d-pads (axis value jumps between 0 and 1 without any values
|
||||
// in between). Without this, the user would need to press the d-pad twice
|
||||
// due to the first press being caught by the "if (firstEvent)" case further up.
|
||||
else if (Math.abs(value) < 0.25f && Math.abs(previousValue) > 0.75f) {
|
||||
numMovedAxis++;
|
||||
lastMovedRange = range;
|
||||
lastMovedDir = previousValue < 0.0f ? '-' : '+';
|
||||
}
|
||||
}
|
||||
|
||||
mPreviousValues.set(i, value);
|
||||
}
|
||||
|
||||
// If only one axis moved, that's the winner.
|
||||
if (numMovedAxis == 1) {
|
||||
mWaitingForEvent = false;
|
||||
setting.onMotionInput(input, lastMovedRange, lastMovedDir);
|
||||
dismiss();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.display
|
||||
|
||||
import android.view.WindowManager
|
||||
import org.citra.citra_emu.NativeLibrary
|
||||
import org.citra.citra_emu.features.settings.model.BooleanSetting
|
||||
import org.citra.citra_emu.features.settings.model.IntSetting
|
||||
import org.citra.citra_emu.features.settings.model.Settings
|
||||
import org.citra.citra_emu.features.settings.utils.SettingsFile
|
||||
import org.citra.citra_emu.utils.EmulationMenuSettings
|
||||
|
||||
class ScreenAdjustmentUtil(private val windowManager: WindowManager,
|
||||
private val settings: Settings) {
|
||||
fun swapScreen() {
|
||||
val isEnabled = !EmulationMenuSettings.swapScreens
|
||||
EmulationMenuSettings.swapScreens = isEnabled
|
||||
NativeLibrary.swapScreens(
|
||||
isEnabled,
|
||||
windowManager.defaultDisplay.rotation
|
||||
)
|
||||
BooleanSetting.SWAP_SCREEN.boolean = isEnabled
|
||||
settings.saveSetting(BooleanSetting.SWAP_SCREEN, SettingsFile.FILE_NAME_CONFIG)
|
||||
}
|
||||
|
||||
fun cycleLayouts() {
|
||||
val nextLayout = (EmulationMenuSettings.landscapeScreenLayout + 1) % ScreenLayout.entries.size
|
||||
changeScreenOrientation(ScreenLayout.from(nextLayout))
|
||||
}
|
||||
|
||||
fun changeScreenOrientation(layoutOption: ScreenLayout) {
|
||||
EmulationMenuSettings.landscapeScreenLayout = layoutOption.int
|
||||
NativeLibrary.notifyOrientationChange(
|
||||
EmulationMenuSettings.landscapeScreenLayout,
|
||||
windowManager.defaultDisplay.rotation
|
||||
)
|
||||
IntSetting.SCREEN_LAYOUT.int = layoutOption.int
|
||||
settings.saveSetting(IntSetting.SCREEN_LAYOUT, SettingsFile.FILE_NAME_CONFIG)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.display
|
||||
|
||||
enum class ScreenLayout(val int: Int) {
|
||||
// These must match what is defined in src/common/settings.h
|
||||
DEFAULT(0),
|
||||
SINGLE_SCREEN(1),
|
||||
LARGE_SCREEN(2),
|
||||
SIDE_SCREEN(3),
|
||||
HYBRID_SCREEN(4),
|
||||
MOBILE_PORTRAIT(5),
|
||||
MOBILE_LANDSCAPE(6);
|
||||
|
||||
companion object {
|
||||
fun from(int: Int): ScreenLayout {
|
||||
return entries.firstOrNull { it.int == int } ?: DEFAULT
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
package org.citra.citra_emu.features.cheats.model;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
public class Cheat {
|
||||
@Keep
|
||||
private final long mPointer;
|
||||
|
||||
private Runnable mEnabledChangedCallback = null;
|
||||
|
||||
@Keep
|
||||
private Cheat(long pointer) {
|
||||
mPointer = pointer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected native void finalize();
|
||||
|
||||
@NonNull
|
||||
public native String getName();
|
||||
|
||||
@NonNull
|
||||
public native String getNotes();
|
||||
|
||||
@NonNull
|
||||
public native String getCode();
|
||||
|
||||
public native boolean getEnabled();
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
setEnabledImpl(enabled);
|
||||
onEnabledChanged();
|
||||
}
|
||||
|
||||
private native void setEnabledImpl(boolean enabled);
|
||||
|
||||
public void setEnabledChangedCallback(@Nullable Runnable callback) {
|
||||
mEnabledChangedCallback = callback;
|
||||
}
|
||||
|
||||
private void onEnabledChanged() {
|
||||
if (mEnabledChangedCallback != null) {
|
||||
mEnabledChangedCallback.run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the code is valid, returns 0. Otherwise, returns the 1-based index
|
||||
* for the line containing the error.
|
||||
*/
|
||||
public static native int isValidGatewayCode(@NonNull String code);
|
||||
|
||||
public static native Cheat createGatewayCode(@NonNull String name, @NonNull String notes,
|
||||
@NonNull String code);
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.features.cheats.model
|
||||
|
||||
import androidx.annotation.Keep
|
||||
|
||||
@Keep
|
||||
class Cheat(@field:Keep private val mPointer: Long) {
|
||||
private var enabledChangedCallback: Runnable? = null
|
||||
protected external fun finalize()
|
||||
|
||||
external fun getName(): String
|
||||
|
||||
external fun getNotes(): String
|
||||
|
||||
external fun getCode(): String
|
||||
|
||||
external fun getEnabled(): Boolean
|
||||
|
||||
fun setEnabled(enabled: Boolean) {
|
||||
setEnabledImpl(enabled)
|
||||
onEnabledChanged()
|
||||
}
|
||||
|
||||
private external fun setEnabledImpl(enabled: Boolean)
|
||||
|
||||
fun setEnabledChangedCallback(callback: Runnable) {
|
||||
enabledChangedCallback = callback
|
||||
}
|
||||
|
||||
private fun onEnabledChanged() {
|
||||
enabledChangedCallback?.run()
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* If the code is valid, returns 0. Otherwise, returns the 1-based index
|
||||
* for the line containing the error.
|
||||
*/
|
||||
@JvmStatic
|
||||
external fun isValidGatewayCode(code: String): Int
|
||||
|
||||
@JvmStatic
|
||||
external fun createGatewayCode(name: String, notes: String, code: String): Cheat
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package org.citra.citra_emu.features.cheats.model;
|
||||
|
||||
import androidx.annotation.Keep;
|
||||
|
||||
public class CheatEngine {
|
||||
@Keep
|
||||
private final long mPointer;
|
||||
|
||||
@Keep
|
||||
public CheatEngine(long titleId) {
|
||||
mPointer = initialize(titleId);
|
||||
}
|
||||
|
||||
private static native long initialize(long titleId);
|
||||
|
||||
@Override
|
||||
protected native void finalize();
|
||||
|
||||
public native Cheat[] getCheats();
|
||||
|
||||
public native void addCheat(Cheat cheat);
|
||||
|
||||
public native void removeCheat(int index);
|
||||
|
||||
public native void updateCheat(int index, Cheat newCheat);
|
||||
|
||||
public native void saveCheatFile();
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// Copyright 2023 Citra Emulator Project
|
||||
// Licensed under GPLv2 or any later version
|
||||
// Refer to the license.txt file included.
|
||||
|
||||
package org.citra.citra_emu.features.cheats.model
|
||||
|
||||
import androidx.annotation.Keep
|
||||
|
||||
@Keep
|
||||
object CheatEngine {
|
||||
external fun loadCheatFile(titleId: Long)
|
||||
external fun saveCheatFile(titleId: Long)
|
||||
|
||||
external fun getCheats(): Array<Cheat>
|
||||
|
||||
external fun addCheat(cheat: Cheat?)
|
||||
external fun removeCheat(index: Int)
|
||||
external fun updateCheat(index: Int, newCheat: Cheat?)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue