mirror of
https://github.com/citra-emu/citra-nightly.git
synced 2025-09-26 18:27:02 +00:00
Compare commits
No commits in common. "master" and "nightly-2032" have entirely different histories.
master
...
nightly-20
14
.ci/linux.sh
14
.ci/linux.sh
|
@ -1,19 +1,13 @@
|
||||||
#!/bin/bash -ex
|
#!/bin/sh -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
|
mkdir build && cd build
|
||||||
cmake .. -G Ninja \
|
cmake .. -G Ninja \
|
||||||
-DCMAKE_BUILD_TYPE=Release \
|
-DCMAKE_BUILD_TYPE=Release \
|
||||||
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
|
||||||
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
|
||||||
"${EXTRA_CMAKE_FLAGS[@]}" \
|
-DCMAKE_CXX_COMPILER=clang++ \
|
||||||
|
-DCMAKE_C_COMPILER=clang \
|
||||||
|
-DCMAKE_LINKER=/etc/bin/ld.lld \
|
||||||
-DENABLE_QT_TRANSLATION=ON \
|
-DENABLE_QT_TRANSLATION=ON \
|
||||||
-DCITRA_ENABLE_COMPATIBILITY_REPORTING=ON \
|
-DCITRA_ENABLE_COMPATIBILITY_REPORTING=ON \
|
||||||
-DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON \
|
-DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON \
|
||||||
|
|
16
.ci/pack.sh
16
.ci/pack.sh
|
@ -61,20 +61,12 @@ function pack_artifacts() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ -n "$UNPACKED" ]; then
|
if [ -z "$PACK_INDIVIDUALLY" ]; then
|
||||||
# Copy the artifacts to be uploaded unpacked.
|
# Pack all of the artifacts at once.
|
||||||
for ARTIFACT in build/bundle/*; do
|
pack_artifacts build/bundle
|
||||||
FILENAME=$(basename "$ARTIFACT")
|
else
|
||||||
EXTENSION="${FILENAME##*.}"
|
|
||||||
|
|
||||||
mv "$ARTIFACT" "artifacts/$REV_NAME.$EXTENSION"
|
|
||||||
done
|
|
||||||
elif [ -n "$PACK_INDIVIDUALLY" ]; then
|
|
||||||
# Pack and upload the artifacts one-by-one.
|
# Pack and upload the artifacts one-by-one.
|
||||||
for ARTIFACT in build/bundle/*; do
|
for ARTIFACT in build/bundle/*; do
|
||||||
pack_artifacts "$ARTIFACT"
|
pack_artifacts "$ARTIFACT"
|
||||||
done
|
done
|
||||||
else
|
|
||||||
# Pack all of the artifacts into a single archive.
|
|
||||||
pack_artifacts build/bundle
|
|
||||||
fi
|
fi
|
||||||
|
|
78
.github/workflows/build.yml
vendored
78
.github/workflows/build.yml
vendored
|
@ -12,13 +12,13 @@ jobs:
|
||||||
if: ${{ !github.head_ref }}
|
if: ${{ !github.head_ref }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Pack
|
- name: Pack
|
||||||
run: ./.ci/source.sh
|
run: ./.ci/source.sh
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: source
|
name: source
|
||||||
path: artifacts/
|
path: artifacts/
|
||||||
|
@ -33,15 +33,15 @@ jobs:
|
||||||
env:
|
env:
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
CCACHE_COMPILERCHECK: content
|
CCACHE_COMPILERCHECK: content
|
||||||
CCACHE_SLOPPINESS: time_macros
|
CCACHE_SLOPPINESS: pch_defines,time_macros
|
||||||
OS: linux
|
OS: linux
|
||||||
TARGET: ${{ matrix.target }}
|
TARGET: ${{ matrix.target }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Set up cache
|
- name: Set up cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ env.CCACHE_DIR }}
|
path: ${{ env.CCACHE_DIR }}
|
||||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
|
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
|
||||||
|
@ -53,60 +53,60 @@ jobs:
|
||||||
run: ./.ci/pack.sh
|
run: ./.ci/pack.sh
|
||||||
if: ${{ matrix.target == 'appimage' }}
|
if: ${{ matrix.target == 'appimage' }}
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
if: ${{ matrix.target == 'appimage' }}
|
if: ${{ matrix.target == 'appimage' }}
|
||||||
with:
|
with:
|
||||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||||
path: artifacts/
|
path: artifacts/
|
||||||
macos:
|
macos:
|
||||||
runs-on: ${{ (matrix.target == 'x86_64' && 'macos-13') || 'macos-14' }}
|
runs-on: macos-13
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
target: ["x86_64", "arm64"]
|
target: ["x86_64", "arm64"]
|
||||||
env:
|
env:
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
CCACHE_COMPILERCHECK: content
|
CCACHE_COMPILERCHECK: content
|
||||||
CCACHE_SLOPPINESS: time_macros
|
CCACHE_SLOPPINESS: pch_defines,time_macros
|
||||||
OS: macos
|
OS: macos
|
||||||
TARGET: ${{ matrix.target }}
|
TARGET: ${{ matrix.target }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Set up cache
|
- name: Set up cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ env.CCACHE_DIR }}
|
path: ${{ env.CCACHE_DIR }}
|
||||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
|
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-${{ matrix.target }}-
|
${{ runner.os }}-${{ matrix.target }}-
|
||||||
- name: Install tools
|
- name: Install tools
|
||||||
run: brew install ccache ninja
|
run: brew install ccache glslang ninja
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./.ci/macos.sh
|
run: ./.ci/macos.sh
|
||||||
- name: Prepare outputs for caching
|
- name: Prepare outputs for caching
|
||||||
run: mv build/bundle $OS-$TARGET
|
run: mv build/bundle $OS-$TARGET
|
||||||
- name: Cache outputs for universal build
|
- name: Cache outputs for universal build
|
||||||
uses: actions/cache/save@v4
|
uses: actions/cache/save@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ env.OS }}-${{ env.TARGET }}
|
path: ${{ env.OS }}-${{ env.TARGET }}
|
||||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
macos-universal:
|
macos-universal:
|
||||||
runs-on: macos-14
|
runs-on: macos-13
|
||||||
needs: macos
|
needs: macos
|
||||||
env:
|
env:
|
||||||
OS: macos
|
OS: macos
|
||||||
TARGET: universal
|
TARGET: universal
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Download x86_64 build from cache
|
- name: Download x86_64 build from cache
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ env.OS }}-x86_64
|
path: ${{ env.OS }}-x86_64
|
||||||
key: ${{ runner.os }}-x86_64-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
key: ${{ runner.os }}-x86_64-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
fail-on-cache-miss: true
|
fail-on-cache-miss: true
|
||||||
- name: Download ARM64 build from cache
|
- name: Download ARM64 build from cache
|
||||||
uses: actions/cache/restore@v4
|
uses: actions/cache/restore@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ env.OS }}-arm64
|
path: ${{ env.OS }}-arm64
|
||||||
key: ${{ runner.os }}-arm64-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
key: ${{ runner.os }}-arm64-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
|
||||||
|
@ -118,7 +118,7 @@ jobs:
|
||||||
- name: Pack
|
- name: Pack
|
||||||
run: ./.ci/pack.sh
|
run: ./.ci/pack.sh
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||||
path: artifacts/
|
path: artifacts/
|
||||||
|
@ -133,15 +133,15 @@ jobs:
|
||||||
env:
|
env:
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
CCACHE_COMPILERCHECK: content
|
CCACHE_COMPILERCHECK: content
|
||||||
CCACHE_SLOPPINESS: time_macros
|
CCACHE_SLOPPINESS: pch_defines,time_macros
|
||||||
OS: windows
|
OS: windows
|
||||||
TARGET: ${{ matrix.target }}
|
TARGET: ${{ matrix.target }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Set up cache
|
- name: Set up cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ env.CCACHE_DIR }}
|
path: ${{ env.CCACHE_DIR }}
|
||||||
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
|
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
|
||||||
|
@ -153,6 +153,13 @@ jobs:
|
||||||
- name: Install extra tools (MSVC)
|
- name: Install extra tools (MSVC)
|
||||||
run: choco install ccache ninja wget
|
run: choco install ccache ninja wget
|
||||||
if: ${{ matrix.target == 'msvc' }}
|
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
|
- name: Set up MSYS2
|
||||||
uses: msys2/setup-msys2@v2
|
uses: msys2/setup-msys2@v2
|
||||||
if: ${{ matrix.target == 'msys2' }}
|
if: ${{ matrix.target == 'msys2' }}
|
||||||
|
@ -161,8 +168,10 @@ jobs:
|
||||||
update: true
|
update: true
|
||||||
install: git make p7zip
|
install: git make p7zip
|
||||||
pacboy: >-
|
pacboy: >-
|
||||||
toolchain:p ccache:p cmake:p ninja:p
|
toolchain:p ccache:p cmake:p ninja:p glslang:p
|
||||||
qt6-base:p qt6-multimedia:p qt6-multimedia-wmf:p qt6-tools:p qt6-translations: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
|
- name: Disable line ending translation
|
||||||
run: git config --global core.autocrlf input
|
run: git config --global core.autocrlf input
|
||||||
- name: Build
|
- name: Build
|
||||||
|
@ -170,7 +179,7 @@ jobs:
|
||||||
- name: Pack
|
- name: Pack
|
||||||
run: ./.ci/pack.sh
|
run: ./.ci/pack.sh
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||||
path: artifacts/
|
path: artifacts/
|
||||||
|
@ -179,15 +188,15 @@ jobs:
|
||||||
env:
|
env:
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
CCACHE_COMPILERCHECK: content
|
CCACHE_COMPILERCHECK: content
|
||||||
CCACHE_SLOPPINESS: time_macros
|
CCACHE_SLOPPINESS: pch_defines,time_macros
|
||||||
OS: android
|
OS: android
|
||||||
TARGET: universal
|
TARGET: universal
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Set up cache
|
- name: Set up cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
|
@ -206,7 +215,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
sudo add-apt-repository -y ppa:theofficialgman/gpu-tools
|
sudo add-apt-repository -y ppa:theofficialgman/gpu-tools
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install ccache apksigner -y
|
sudo apt-get install ccache glslang-dev glslang-tools apksigner -y
|
||||||
- name: Build
|
- name: Build
|
||||||
run: JAVA_HOME=$JAVA_HOME_17_X64 ./.ci/android.sh
|
run: JAVA_HOME=$JAVA_HOME_17_X64 ./.ci/android.sh
|
||||||
env:
|
env:
|
||||||
|
@ -217,34 +226,35 @@ jobs:
|
||||||
run: ../../../.ci/pack.sh
|
run: ../../../.ci/pack.sh
|
||||||
working-directory: src/android/app
|
working-directory: src/android/app
|
||||||
env:
|
env:
|
||||||
UNPACKED: 1
|
PACK_INDIVIDUALLY: 1
|
||||||
|
SKIP_7Z: 1
|
||||||
- name: Upload
|
- name: Upload
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: ${{ env.OS }}-${{ env.TARGET }}
|
name: ${{ env.OS }}-${{ env.TARGET }}
|
||||||
path: src/android/app/artifacts/
|
path: src/android/app/artifacts/
|
||||||
ios:
|
ios:
|
||||||
runs-on: macos-14
|
runs-on: macos-13
|
||||||
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
|
||||||
env:
|
env:
|
||||||
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
CCACHE_DIR: ${{ github.workspace }}/.ccache
|
||||||
CCACHE_COMPILERCHECK: content
|
CCACHE_COMPILERCHECK: content
|
||||||
CCACHE_SLOPPINESS: time_macros
|
CCACHE_SLOPPINESS: pch_defines,time_macros
|
||||||
OS: ios
|
OS: ios
|
||||||
TARGET: arm64
|
TARGET: arm64
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
- name: Set up cache
|
- name: Set up cache
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: ${{ env.CCACHE_DIR }}
|
path: ${{ env.CCACHE_DIR }}
|
||||||
key: ${{ runner.os }}-ios-${{ github.sha }}
|
key: ${{ runner.os }}-ios-${{ github.sha }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-ios-
|
${{ runner.os }}-ios-
|
||||||
- name: Install tools
|
- name: Install tools
|
||||||
run: brew install ccache ninja
|
run: brew install ccache glslang ninja
|
||||||
- name: Build
|
- name: Build
|
||||||
run: ./.ci/ios.sh
|
run: ./.ci/ios.sh
|
||||||
release:
|
release:
|
||||||
|
@ -252,7 +262,7 @@ jobs:
|
||||||
needs: [windows, linux, macos-universal, android, source]
|
needs: [windows, linux, macos-universal, android, source]
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
if: ${{ startsWith(github.ref, 'refs/tags/') }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/download-artifact@v4
|
- uses: actions/download-artifact@v3
|
||||||
- name: Create release
|
- name: Create release
|
||||||
uses: actions/create-release@v1
|
uses: actions/create-release@v1
|
||||||
env:
|
env:
|
||||||
|
|
2
.github/workflows/format.yml
vendored
2
.github/workflows/format.yml
vendored
|
@ -13,7 +13,7 @@ jobs:
|
||||||
image: citraemu/build-environments:linux-fresh
|
image: citraemu/build-environments:linux-fresh
|
||||||
options: -u 1001
|
options: -u 1001
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- name: Build
|
- name: Build
|
||||||
|
|
16
.github/workflows/publish.yml
vendored
16
.github/workflows/publish.yml
vendored
|
@ -20,11 +20,11 @@ jobs:
|
||||||
if: ${{ github.event.inputs.nightly != 'false' && github.repository == 'citra-emu/citra' }}
|
if: ${{ github.event.inputs.nightly != 'false' && github.repository == 'citra-emu/citra' }}
|
||||||
steps:
|
steps:
|
||||||
# this checkout is required to make sure the GitHub Actions scripts are available
|
# this checkout is required to make sure the GitHub Actions scripts are available
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
name: Pre-checkout
|
name: Pre-checkout
|
||||||
with:
|
with:
|
||||||
submodules: false
|
submodules: false
|
||||||
- uses: actions/github-script@v7
|
- uses: actions/github-script@v6
|
||||||
id: check-changes
|
id: check-changes
|
||||||
name: 'Check for new changes'
|
name: 'Check for new changes'
|
||||||
env:
|
env:
|
||||||
|
@ -38,7 +38,7 @@ jobs:
|
||||||
return checkBaseChanges(github, context);
|
return checkBaseChanges(github, context);
|
||||||
- run: npm install execa@5
|
- run: npm install execa@5
|
||||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
name: Checkout
|
name: Checkout
|
||||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||||
with:
|
with:
|
||||||
|
@ -46,7 +46,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.ALT_GITHUB_TOKEN }}
|
token: ${{ secrets.ALT_GITHUB_TOKEN }}
|
||||||
- uses: actions/github-script@v7
|
- uses: actions/github-script@v6
|
||||||
name: 'Update and tag new commits'
|
name: 'Update and tag new commits'
|
||||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||||
env:
|
env:
|
||||||
|
@ -62,11 +62,11 @@ jobs:
|
||||||
if: ${{ github.event.inputs.canary != 'false' && github.repository == 'citra-emu/citra' }}
|
if: ${{ github.event.inputs.canary != 'false' && github.repository == 'citra-emu/citra' }}
|
||||||
steps:
|
steps:
|
||||||
# this checkout is required to make sure the GitHub Actions scripts are available
|
# this checkout is required to make sure the GitHub Actions scripts are available
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
name: Pre-checkout
|
name: Pre-checkout
|
||||||
with:
|
with:
|
||||||
submodules: false
|
submodules: false
|
||||||
- uses: actions/github-script@v7
|
- uses: actions/github-script@v6
|
||||||
id: check-changes
|
id: check-changes
|
||||||
name: 'Check for new changes'
|
name: 'Check for new changes'
|
||||||
env:
|
env:
|
||||||
|
@ -79,7 +79,7 @@ jobs:
|
||||||
return checkCanaryChanges(github, context);
|
return checkCanaryChanges(github, context);
|
||||||
- run: npm install execa@5
|
- run: npm install execa@5
|
||||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
name: Checkout
|
name: Checkout
|
||||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||||
with:
|
with:
|
||||||
|
@ -87,7 +87,7 @@ jobs:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
submodules: true
|
submodules: true
|
||||||
token: ${{ secrets.ALT_GITHUB_TOKEN }}
|
token: ${{ secrets.ALT_GITHUB_TOKEN }}
|
||||||
- uses: actions/github-script@v7
|
- uses: actions/github-script@v6
|
||||||
name: 'Check and merge canary changes'
|
name: 'Check and merge canary changes'
|
||||||
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
if: ${{ steps.check-changes.outputs.result == 'true' }}
|
||||||
env:
|
env:
|
||||||
|
|
2
.github/workflows/transifex.yml
vendored
2
.github/workflows/transifex.yml
vendored
|
@ -10,7 +10,7 @@ jobs:
|
||||||
container: citraemu/build-environments:linux-fresh
|
container: citraemu/build-environments:linux-fresh
|
||||||
if: ${{ github.repository == 'citra-emu/citra' }}
|
if: ${{ github.repository == 'citra-emu/citra' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
|
@ -79,12 +79,10 @@ 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_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)
|
option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
|
||||||
|
|
||||||
|
CMAKE_DEPENDENT_OPTION(CITRA_ENABLE_BUNDLE_TARGET "Enable the distribution bundling target." ON "NOT ANDROID AND NOT IOS" OFF)
|
||||||
|
|
||||||
# Compile options
|
# Compile options
|
||||||
CMAKE_DEPENDENT_OPTION(COMPILE_WITH_DWARF "Add DWARF debugging information" ${IS_DEBUG_BUILD} "MINGW" OFF)
|
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})
|
option(ENABLE_LTO "Enable link time optimization" ${DEFAULT_ENABLE_LTO})
|
||||||
|
@ -247,26 +245,6 @@ if (ENABLE_QT)
|
||||||
if (ENABLE_QT_TRANSLATION)
|
if (ENABLE_QT_TRANSLATION)
|
||||||
find_package(Qt6 REQUIRED COMPONENTS LinguistTools)
|
find_package(Qt6 REQUIRED COMPONENTS LinguistTools)
|
||||||
endif()
|
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()
|
endif()
|
||||||
|
|
||||||
# Use system tsl::robin_map if available (otherwise we fallback to version bundled with dynarmic)
|
# Use system tsl::robin_map if available (otherwise we fallback to version bundled with dynarmic)
|
||||||
|
@ -276,22 +254,20 @@ find_package(tsl-robin-map QUIET)
|
||||||
# ======================================
|
# ======================================
|
||||||
|
|
||||||
if (APPLE)
|
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)
|
if (NOT IOS)
|
||||||
# Umbrella framework for everything GUI-related
|
# Umbrella framework for everything GUI-related
|
||||||
find_library(COCOA_LIBRARY Cocoa REQUIRED)
|
find_library(COCOA_LIBRARY Cocoa REQUIRED)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_library(AVFOUNDATION_LIBRARY AVFoundation REQUIRED)
|
find_library(AVFOUNDATION_LIBRARY AVFoundation REQUIRED)
|
||||||
find_library(IOSURFACE_LIBRARY IOSurface REQUIRED)
|
find_library(IOSURFACE_LIBRARY IOSurface REQUIRED)
|
||||||
set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${AVFOUNDATION_LIBRARY} ${IOSURFACE_LIBRARY} ${MOLTENVK_LIBRARY})
|
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)
|
elseif (WIN32)
|
||||||
set(PLATFORM_LIBRARIES winmm ws2_32)
|
set(PLATFORM_LIBRARIES winmm ws2_32)
|
||||||
if (MINGW)
|
if (MINGW)
|
||||||
|
@ -442,8 +418,7 @@ else()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Create target for outputting distributable bundles.
|
# Create target for outputting distributable bundles.
|
||||||
# Not supported for mobile platforms as distributables are built differently.
|
if (CITRA_ENABLE_BUNDLE_TARGET)
|
||||||
if (NOT ANDROID AND NOT IOS)
|
|
||||||
include(BundleTarget)
|
include(BundleTarget)
|
||||||
if (ENABLE_SDL2_FRONTEND)
|
if (ENABLE_SDL2_FRONTEND)
|
||||||
bundle_target(citra)
|
bundle_target(citra)
|
||||||
|
|
|
@ -2,104 +2,37 @@
|
||||||
if (BUNDLE_TARGET_EXECUTE)
|
if (BUNDLE_TARGET_EXECUTE)
|
||||||
# --- Bundling method logic ---
|
# --- 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)
|
function(bundle_qt executable_path)
|
||||||
if (WIN32)
|
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)
|
get_filename_component(executable_parent_dir "${executable_path}" DIRECTORY)
|
||||||
|
find_program(windeployqt_executable windeployqt6)
|
||||||
|
|
||||||
# Create a qt.conf file pointing to the app directory.
|
# Create a qt.conf file pointing to the app directory.
|
||||||
# This ensures Qt can find its plugins.
|
# 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}")
|
message(STATUS "Executing windeployqt for executable ${executable_path}")
|
||||||
execute_process(COMMAND "${windeployqt_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
|
--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.
|
# Remove the FFmpeg multimedia plugin as we don't include FFmpeg.
|
||||||
# We want to use the Windows media plugin instead, which is also included.
|
# We want to use the Windows media plugin instead, which is also included.
|
||||||
file(REMOVE "${executable_parent_dir}/plugins/multimedia/ffmpegmediaplugin.dll")
|
file(REMOVE "${executable_parent_dir}/plugins/multimedia/ffmpegmediaplugin.dll")
|
||||||
elseif (APPLE)
|
elseif (APPLE)
|
||||||
get_filename_component(executable_name "${executable_path}" NAME_WE)
|
get_filename_component(executable_name "${executable_path}" NAME_WE)
|
||||||
find_program(macdeployqt_executable macdeployqt6 PATHS "${QT_HOST_PATH}/bin")
|
find_program(MACDEPLOYQT_EXECUTABLE macdeployqt6)
|
||||||
|
|
||||||
message(STATUS "Executing macdeployqt at \"${macdeployqt_executable}\" for executable \"${executable_path}\"")
|
message(STATUS "Executing macdeployqt for executable ${executable_path}")
|
||||||
execute_process(
|
execute_process(
|
||||||
COMMAND "${macdeployqt_executable}"
|
COMMAND "${MACDEPLOYQT_EXECUTABLE}"
|
||||||
"${executable_path}"
|
"${executable_path}"
|
||||||
"-executable=${executable_path}/Contents/MacOS/${executable_name}"
|
"-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.
|
# 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.
|
# 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()
|
else()
|
||||||
message(FATAL_ERROR "Unsupported OS for Qt bundling.")
|
message(FATAL_ERROR "Unsupported OS for Qt bundling.")
|
||||||
endif()
|
endif()
|
||||||
|
@ -111,9 +44,9 @@ if (BUNDLE_TARGET_EXECUTE)
|
||||||
|
|
||||||
if (enable_qt)
|
if (enable_qt)
|
||||||
# Find qmake to make sure the plugin uses the right version of Qt.
|
# Find qmake to make sure the plugin uses the right version of Qt.
|
||||||
find_program(qmake_executable qmake6 PATHS "${QT_HOST_PATH}/bin")
|
find_program(QMAKE_EXECUTABLE qmake6)
|
||||||
|
|
||||||
set(extra_linuxdeploy_env "QMAKE=${qmake_executable}")
|
set(extra_linuxdeploy_env "QMAKE=${QMAKE_EXECUTABLE}")
|
||||||
set(extra_linuxdeploy_args --plugin qt)
|
set(extra_linuxdeploy_args --plugin qt)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
@ -126,11 +59,7 @@ if (BUNDLE_TARGET_EXECUTE)
|
||||||
--executable "${executable_path}"
|
--executable "${executable_path}"
|
||||||
--icon-file "${source_path}/dist/citra.svg"
|
--icon-file "${source_path}/dist/citra.svg"
|
||||||
--desktop-file "${source_path}/dist/${executable_name}.desktop"
|
--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)
|
if (enable_qt)
|
||||||
set(qt_hook_file "${appdir_path}/apprun-hooks/linuxdeploy-plugin-qt-hook.sh")
|
set(qt_hook_file "${appdir_path}/apprun-hooks/linuxdeploy-plugin-qt-hook.sh")
|
||||||
|
@ -153,11 +82,7 @@ if (BUNDLE_TARGET_EXECUTE)
|
||||||
"OUTPUT=${bundle_dir}/${executable_name}.AppImage"
|
"OUTPUT=${bundle_dir}/${executable_name}.AppImage"
|
||||||
"${linuxdeploy_executable}"
|
"${linuxdeploy_executable}"
|
||||||
--output appimage
|
--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()
|
endfunction()
|
||||||
|
|
||||||
function(bundle_standalone executable_path original_executable_path bundle_library_paths)
|
function(bundle_standalone executable_path original_executable_path bundle_library_paths)
|
||||||
|
@ -184,23 +109,16 @@ if (BUNDLE_TARGET_EXECUTE)
|
||||||
file(MAKE_DIRECTORY ${lib_dir})
|
file(MAKE_DIRECTORY ${lib_dir})
|
||||||
foreach (lib_file IN LISTS resolved_deps)
|
foreach (lib_file IN LISTS resolved_deps)
|
||||||
message(STATUS "Bundling library ${lib_file}")
|
message(STATUS "Bundling library ${lib_file}")
|
||||||
symlink_safe_copy("${lib_file}" "${lib_dir}")
|
# Use native copy to turn symlinks into normal files.
|
||||||
|
execute_process(COMMAND cp -L "${lib_file}" "${lib_dir}")
|
||||||
endforeach()
|
endforeach()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Add libs directory to executable rpath where applicable.
|
# Add libs directory to executable rpath where applicable.
|
||||||
if (APPLE)
|
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)
|
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()
|
endif()
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
||||||
|
@ -209,7 +127,7 @@ if (BUNDLE_TARGET_EXECUTE)
|
||||||
set(bundle_dir ${BINARY_PATH}/bundle)
|
set(bundle_dir ${BINARY_PATH}/bundle)
|
||||||
|
|
||||||
# On Linux, always bundle an AppImage.
|
# On Linux, always bundle an AppImage.
|
||||||
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
|
if (DEFINED LINUXDEPLOY)
|
||||||
if (IN_PLACE)
|
if (IN_PLACE)
|
||||||
message(FATAL_ERROR "Cannot bundle for Linux in-place.")
|
message(FATAL_ERROR "Cannot bundle for Linux in-place.")
|
||||||
endif()
|
endif()
|
||||||
|
@ -228,12 +146,14 @@ if (BUNDLE_TARGET_EXECUTE)
|
||||||
|
|
||||||
if (BUNDLE_QT)
|
if (BUNDLE_QT)
|
||||||
bundle_qt("${bundled_executable_path}")
|
bundle_qt("${bundled_executable_path}")
|
||||||
else()
|
endif()
|
||||||
|
|
||||||
|
if (WIN32 OR NOT BUNDLE_QT)
|
||||||
bundle_standalone("${bundled_executable_path}" "${EXECUTABLE_PATH}" "${BUNDLE_LIBRARY_PATHS}")
|
bundle_standalone("${bundled_executable_path}" "${EXECUTABLE_PATH}" "${BUNDLE_LIBRARY_PATHS}")
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
elseif (BUNDLE_TARGET_DOWNLOAD_LINUXDEPLOY)
|
else()
|
||||||
# --- linuxdeploy download logic ---
|
# --- Bundling target creation logic ---
|
||||||
|
|
||||||
# Downloads and extracts a linuxdeploy component.
|
# Downloads and extracts a linuxdeploy component.
|
||||||
function(download_linuxdeploy_component base_dir name executable_name)
|
function(download_linuxdeploy_component base_dir name executable_name)
|
||||||
|
@ -241,7 +161,7 @@ elseif (BUNDLE_TARGET_DOWNLOAD_LINUXDEPLOY)
|
||||||
if (NOT EXISTS "${executable_file}")
|
if (NOT EXISTS "${executable_file}")
|
||||||
message(STATUS "Downloading ${executable_name}")
|
message(STATUS "Downloading ${executable_name}")
|
||||||
file(DOWNLOAD
|
file(DOWNLOAD
|
||||||
"https://github.com/${name}/releases/download/continuous/${executable_name}"
|
"https://github.com/linuxdeploy/${name}/releases/download/continuous/${executable_name}"
|
||||||
"${executable_file}" SHOW_PROGRESS)
|
"${executable_file}" SHOW_PROGRESS)
|
||||||
file(CHMOD "${executable_file}" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE)
|
file(CHMOD "${executable_file}" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE)
|
||||||
|
|
||||||
|
@ -250,11 +170,7 @@ elseif (BUNDLE_TARGET_DOWNLOAD_LINUXDEPLOY)
|
||||||
message(STATUS "Extracting ${executable_name}")
|
message(STATUS "Extracting ${executable_name}")
|
||||||
execute_process(
|
execute_process(
|
||||||
COMMAND "${executable_file}" --appimage-extract
|
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()
|
else()
|
||||||
message(STATUS "Copying ${executable_name}")
|
message(STATUS "Copying ${executable_name}")
|
||||||
file(COPY "${executable_file}" DESTINATION "${base_dir}/squashfs-root/usr/bin/")
|
file(COPY "${executable_file}" DESTINATION "${base_dir}/squashfs-root/usr/bin/")
|
||||||
|
@ -262,102 +178,89 @@ elseif (BUNDLE_TARGET_DOWNLOAD_LINUXDEPLOY)
|
||||||
endif()
|
endif()
|
||||||
endfunction()
|
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.
|
# 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.
|
# 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)
|
function(bundle_target_internal target_name in_place)
|
||||||
# Create base bundle target if it does not exist.
|
# Create base bundle target if it does not exist.
|
||||||
if (NOT in_place AND NOT TARGET bundle)
|
if (NOT in_place AND NOT TARGET bundle)
|
||||||
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")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(bundle_executable_path "$<TARGET_FILE:${target_name}>")
|
set(BUNDLE_EXECUTABLE_PATH "$<TARGET_FILE:${target_name}>")
|
||||||
if (target_name MATCHES ".*qt")
|
if (target_name MATCHES ".*qt")
|
||||||
set(bundle_qt ON)
|
set(BUNDLE_QT ON)
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
# For Qt targets on Apple, expect an app bundle.
|
# 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()
|
endif()
|
||||||
else()
|
else()
|
||||||
set(bundle_qt OFF)
|
set(BUNDLE_QT OFF)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Build a list of library search paths from prefix paths.
|
# Build a list of library search paths from prefix paths.
|
||||||
foreach(prefix_path IN LISTS CMAKE_FIND_ROOT_PATH CMAKE_PREFIX_PATH CMAKE_SYSTEM_PREFIX_PATH)
|
foreach(prefix_path IN LISTS CMAKE_PREFIX_PATH CMAKE_SYSTEM_PREFIX_PATH)
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
list(APPEND bundle_library_paths "${prefix_path}/bin")
|
list(APPEND BUNDLE_LIBRARY_PATHS "${prefix_path}/bin")
|
||||||
endif()
|
endif()
|
||||||
list(APPEND bundle_library_paths "${prefix_path}/lib")
|
list(APPEND BUNDLE_LIBRARY_PATHS "${prefix_path}/lib")
|
||||||
endforeach()
|
endforeach()
|
||||||
foreach(library_path IN LISTS CMAKE_SYSTEM_LIBRARY_PATH)
|
foreach(library_path IN LISTS CMAKE_SYSTEM_LIBRARY_PATH)
|
||||||
list(APPEND bundle_library_paths "${library_path}")
|
list(APPEND BUNDLE_LIBRARY_PATHS "${library_path}")
|
||||||
endforeach()
|
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)
|
if (in_place)
|
||||||
message(STATUS "Adding in-place bundling to ${target_name}")
|
message(STATUS "Adding in-place bundling to ${target_name}")
|
||||||
set(dest_target ${target_name})
|
set(DEST_TARGET ${target_name})
|
||||||
else()
|
else()
|
||||||
message(STATUS "Adding ${target_name} to bundle target")
|
message(STATUS "Adding ${target_name} to bundle target")
|
||||||
set(dest_target bundle)
|
set(DEST_TARGET bundle)
|
||||||
add_dependencies(bundle ${target_name})
|
add_dependencies(bundle ${target_name})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
add_custom_command(TARGET ${dest_target} POST_BUILD
|
add_custom_command(TARGET ${DEST_TARGET} POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND}
|
COMMAND ${CMAKE_COMMAND}
|
||||||
"-DQT_HOST_PATH=\"${QT_HOST_PATH}\""
|
"-DCMAKE_PREFIX_PATH=\"${CMAKE_PREFIX_PATH}\""
|
||||||
"-DQT_TARGET_PATH=\"${QT_TARGET_PATH}\""
|
|
||||||
"-DBUNDLE_TARGET_EXECUTE=1"
|
"-DBUNDLE_TARGET_EXECUTE=1"
|
||||||
"-DTARGET=${target_name}"
|
"-DTARGET=${target_name}"
|
||||||
"-DSOURCE_PATH=${CMAKE_SOURCE_DIR}"
|
"-DSOURCE_PATH=${CMAKE_SOURCE_DIR}"
|
||||||
"-DBINARY_PATH=${CMAKE_BINARY_DIR}"
|
"-DBINARY_PATH=${CMAKE_BINARY_DIR}"
|
||||||
"-DEXECUTABLE_PATH=${bundle_executable_path}"
|
"-DEXECUTABLE_PATH=${BUNDLE_EXECUTABLE_PATH}"
|
||||||
"-DBUNDLE_LIBRARY_PATHS=\"${bundle_library_paths}\""
|
"-DBUNDLE_LIBRARY_PATHS=\"${BUNDLE_LIBRARY_PATHS}\""
|
||||||
"-DBUNDLE_QT=${bundle_qt}"
|
"-DBUNDLE_QT=${BUNDLE_QT}"
|
||||||
"-DIN_PLACE=${in_place}"
|
"-DIN_PLACE=${in_place}"
|
||||||
"-DLINUXDEPLOY=${CMAKE_BINARY_DIR}/externals/linuxdeploy/squashfs-root/AppRun"
|
${EXTRA_BUNDLE_ARGS}
|
||||||
-P "${CMAKE_SOURCE_DIR}/CMakeModules/BundleTarget.cmake"
|
-P "${CMAKE_SOURCE_DIR}/CMakeModules/BundleTarget.cmake"
|
||||||
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}")
|
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}")
|
||||||
endfunction()
|
endfunction()
|
||||||
|
|
|
@ -1,20 +1,21 @@
|
||||||
|
|
||||||
set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR})
|
set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR})
|
||||||
|
|
||||||
# Determines parameters based on the host and target for downloading the right Qt binaries.
|
# This function downloads Qt using aqt. The path of the downloaded content will be added to the CMAKE_PREFIX_PATH.
|
||||||
function(determine_qt_parameters target host_out type_out arch_out arch_path_out host_type_out host_arch_out host_arch_path_out)
|
# 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)
|
||||||
if (target MATCHES "tools_.*")
|
if (target MATCHES "tools_.*")
|
||||||
set(tool ON)
|
set(DOWNLOAD_QT_TOOL ON)
|
||||||
else()
|
else()
|
||||||
set(tool OFF)
|
set(DOWNLOAD_QT_TOOL OFF)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Determine installation parameters for OS, architecture, and compiler
|
# Determine installation parameters for OS, architecture, and compiler
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
set(host "windows")
|
set(host "windows")
|
||||||
set(type "desktop")
|
set(type "desktop")
|
||||||
|
if (NOT DOWNLOAD_QT_TOOL)
|
||||||
if (NOT tool)
|
|
||||||
if (MINGW)
|
if (MINGW)
|
||||||
set(arch "win64_mingw")
|
set(arch "win64_mingw")
|
||||||
set(arch_path "mingw_64")
|
set(arch_path "mingw_64")
|
||||||
|
@ -27,35 +28,21 @@ function(determine_qt_parameters target host_out type_out arch_out arch_path_out
|
||||||
message(FATAL_ERROR "Unsupported bundled Qt architecture. Enable USE_SYSTEM_QT and provide your own.")
|
message(FATAL_ERROR "Unsupported bundled Qt architecture. Enable USE_SYSTEM_QT and provide your own.")
|
||||||
endif()
|
endif()
|
||||||
set(arch "win64_${arch_path}")
|
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()
|
else()
|
||||||
message(FATAL_ERROR "Unsupported bundled Qt toolchain. Enable USE_SYSTEM_QT and provide your own.")
|
message(FATAL_ERROR "Unsupported bundled Qt toolchain. Enable USE_SYSTEM_QT and provide your own.")
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
elseif (APPLE)
|
elseif (APPLE)
|
||||||
set(host "mac")
|
set(host "mac")
|
||||||
set(type "desktop")
|
if (IOS AND NOT DOWNLOAD_QT_TOOL)
|
||||||
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(type "ios")
|
||||||
set(arch "ios")
|
set(arch "ios")
|
||||||
set(arch_path "ios")
|
set(arch_path "ios")
|
||||||
|
set(host_arch_path "macos")
|
||||||
|
else()
|
||||||
|
set(type "desktop")
|
||||||
|
set(arch "clang_64")
|
||||||
|
set(arch_path "macos")
|
||||||
endif()
|
endif()
|
||||||
else()
|
else()
|
||||||
set(host "linux")
|
set(host "linux")
|
||||||
|
@ -64,64 +51,38 @@ function(determine_qt_parameters target host_out type_out arch_out arch_path_out
|
||||||
set(arch_path "linux")
|
set(arch_path "linux")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(${host_out} "${host}" PARENT_SCOPE)
|
get_external_prefix(qt base_path)
|
||||||
set(${type_out} "${type}" PARENT_SCOPE)
|
file(MAKE_DIRECTORY "${base_path}")
|
||||||
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()
|
|
||||||
|
|
||||||
# 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")
|
set(install_args -c "${CURRENT_MODULE_DIR}/aqt_config.ini")
|
||||||
if (tool)
|
if (DOWNLOAD_QT_TOOL)
|
||||||
set(prefix "${base_path}/Tools")
|
set(prefix "${base_path}/Tools")
|
||||||
set(install_args ${install_args} install-tool --outputdir ${base_path} ${host} desktop ${target})
|
set(install_args ${install_args} install-tool --outputdir ${base_path} ${host} desktop ${target})
|
||||||
else()
|
else()
|
||||||
set(prefix "${base_path}/${target}/${arch_path}")
|
set(prefix "${base_path}/${target}/${arch_path}")
|
||||||
set(install_args ${install_args} install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch}
|
if (host_arch_path)
|
||||||
-m qtmultimedia --archives qttranslations qttools qtsvg qtbase)
|
set(host_flag "--autodesktop")
|
||||||
|
set(host_prefix "${base_path}/${target}/${host_arch_path}")
|
||||||
|
endif()
|
||||||
|
set(install_args ${install_args} install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch} ${host_flag}
|
||||||
|
-m qtmultimedia --archives qttranslations qttools qtsvg qtbase)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if (NOT EXISTS "${prefix}")
|
if (NOT EXISTS "${prefix}")
|
||||||
message(STATUS "Downloading Qt binaries for ${target}:${host}:${type}:${arch}:${arch_path}")
|
message(STATUS "Downloading binaries for Qt...")
|
||||||
set(AQT_PREBUILD_BASE_URL "https://github.com/miurahr/aqtinstall/releases/download/v3.1.9")
|
set(AQT_PREBUILD_BASE_URL "https://github.com/miurahr/aqtinstall/releases/download/v3.1.9")
|
||||||
if (WIN32)
|
if (WIN32)
|
||||||
set(aqt_path "${base_path}/aqt.exe")
|
set(aqt_path "${base_path}/aqt.exe")
|
||||||
if (NOT EXISTS "${aqt_path}")
|
file(DOWNLOAD
|
||||||
file(DOWNLOAD
|
${AQT_PREBUILD_BASE_URL}/aqt.exe
|
||||||
${AQT_PREBUILD_BASE_URL}/aqt.exe
|
${aqt_path} SHOW_PROGRESS)
|
||||||
${aqt_path} SHOW_PROGRESS)
|
|
||||||
endif()
|
|
||||||
execute_process(COMMAND ${aqt_path} ${install_args}
|
execute_process(COMMAND ${aqt_path} ${install_args}
|
||||||
WORKING_DIRECTORY ${base_path})
|
WORKING_DIRECTORY ${base_path})
|
||||||
elseif (APPLE)
|
elseif (APPLE)
|
||||||
set(aqt_path "${base_path}/aqt-macos")
|
set(aqt_path "${base_path}/aqt-macos")
|
||||||
if (NOT EXISTS "${aqt_path}")
|
file(DOWNLOAD
|
||||||
file(DOWNLOAD
|
${AQT_PREBUILD_BASE_URL}/aqt-macos
|
||||||
${AQT_PREBUILD_BASE_URL}/aqt-macos
|
${aqt_path} SHOW_PROGRESS)
|
||||||
${aqt_path} SHOW_PROGRESS)
|
|
||||||
endif()
|
|
||||||
execute_process(COMMAND chmod +x ${aqt_path})
|
execute_process(COMMAND chmod +x ${aqt_path})
|
||||||
execute_process(COMMAND ${aqt_path} ${install_args}
|
execute_process(COMMAND ${aqt_path} ${install_args}
|
||||||
WORKING_DIRECTORY ${base_path})
|
WORKING_DIRECTORY ${base_path})
|
||||||
|
@ -135,38 +96,18 @@ function(download_qt_configuration prefix_out target host type arch arch_path ba
|
||||||
execute_process(COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${aqt_install_path} python3 -m aqt ${install_args}
|
execute_process(COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${aqt_install_path} python3 -m aqt ${install_args}
|
||||||
WORKING_DIRECTORY ${base_path})
|
WORKING_DIRECTORY ${base_path})
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
message(STATUS "Downloaded Qt binaries for ${target}:${host}:${type}:${arch}:${arch_path} to ${prefix}")
|
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
set(${prefix_out} "${prefix}" PARENT_SCOPE)
|
message(STATUS "Using downloaded Qt binaries at ${prefix}")
|
||||||
endfunction()
|
|
||||||
|
|
||||||
# This function downloads Qt using aqt.
|
# Add the Qt prefix path so CMake can locate it.
|
||||||
# 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}")
|
list(APPEND CMAKE_PREFIX_PATH "${prefix}")
|
||||||
set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE)
|
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()
|
endfunction()
|
||||||
|
|
||||||
function(download_moltenvk)
|
function(download_moltenvk)
|
||||||
|
@ -180,7 +121,7 @@ function(download_moltenvk)
|
||||||
set(MOLTENVK_TAR "${CMAKE_BINARY_DIR}/externals/MoltenVK.tar")
|
set(MOLTENVK_TAR "${CMAKE_BINARY_DIR}/externals/MoltenVK.tar")
|
||||||
if (NOT EXISTS ${MOLTENVK_DIR})
|
if (NOT EXISTS ${MOLTENVK_DIR})
|
||||||
if (NOT EXISTS ${MOLTENVK_TAR})
|
if (NOT EXISTS ${MOLTENVK_TAR})
|
||||||
file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/download/v1.2.7-rc2/MoltenVK-all.tar
|
file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/latest/download/MoltenVK-all.tar
|
||||||
${MOLTENVK_TAR} SHOW_PROGRESS)
|
${MOLTENVK_TAR} SHOW_PROGRESS)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|
|
@ -26,14 +26,16 @@ set(HASH_FILES
|
||||||
"${VIDEO_CORE}/shader/generator/spv_fs_shader_gen.h"
|
"${VIDEO_CORE}/shader/generator/spv_fs_shader_gen.h"
|
||||||
"${VIDEO_CORE}/shader/shader.cpp"
|
"${VIDEO_CORE}/shader/shader.cpp"
|
||||||
"${VIDEO_CORE}/shader/shader.h"
|
"${VIDEO_CORE}/shader/shader.h"
|
||||||
"${VIDEO_CORE}/pica/regs_framebuffer.h"
|
"${VIDEO_CORE}/pica.cpp"
|
||||||
"${VIDEO_CORE}/pica/regs_lighting.h"
|
"${VIDEO_CORE}/pica.h"
|
||||||
"${VIDEO_CORE}/pica/regs_pipeline.h"
|
"${VIDEO_CORE}/regs_framebuffer.h"
|
||||||
"${VIDEO_CORE}/pica/regs_rasterizer.h"
|
"${VIDEO_CORE}/regs_lighting.h"
|
||||||
"${VIDEO_CORE}/pica/regs_shader.h"
|
"${VIDEO_CORE}/regs_pipeline.h"
|
||||||
"${VIDEO_CORE}/pica/regs_texturing.h"
|
"${VIDEO_CORE}/regs_rasterizer.h"
|
||||||
"${VIDEO_CORE}/pica/regs_internal.cpp"
|
"${VIDEO_CORE}/regs_shader.h"
|
||||||
"${VIDEO_CORE}/pica/regs_internal.h"
|
"${VIDEO_CORE}/regs_texturing.h"
|
||||||
|
"${VIDEO_CORE}/regs.cpp"
|
||||||
|
"${VIDEO_CORE}/regs.h"
|
||||||
)
|
)
|
||||||
set(COMBINED "")
|
set(COMBINED "")
|
||||||
foreach (F IN LISTS HASH_FILES)
|
foreach (F IN LISTS HASH_FILES)
|
||||||
|
|
34
dist/apple/Info.plist.in
vendored
34
dist/apple/Info.plist.in
vendored
|
@ -21,43 +21,9 @@
|
||||||
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
|
<string>${MACOSX_BUNDLE_INFO_STRING}</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
|
<string>${MACOSX_BUNDLE_COPYRIGHT}</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
|
||||||
<string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
|
|
||||||
<!-- Fixed -->
|
<!-- Fixed -->
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
<string>public.app-category.games</string>
|
<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>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>This app requires camera access to emulate the 3DS's cameras.</string>
|
<string>This app requires camera access to emulate the 3DS's cameras.</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
|
8
dist/dumpkeys/DumpKeys.gm9
vendored
8
dist/dumpkeys/DumpKeys.gm9
vendored
|
@ -287,13 +287,5 @@ dumptxt -p $[OUT] "nfcSecret1Seed=$[NFC_SEED_1]"
|
||||||
dumptxt -p $[OUT] "nfcSecret1HmacKey=$[NFC_HMAC_KEY_1]"
|
dumptxt -p $[OUT] "nfcSecret1HmacKey=$[NFC_HMAC_KEY_1]"
|
||||||
dumptxt -p $[OUT] "nfcIv=$[NFC_IV]"
|
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
|
@Exit
|
||||||
|
|
||||||
|
|
2
dist/dumpkeys/README.md
vendored
2
dist/dumpkeys/README.md
vendored
|
@ -6,5 +6,5 @@ Usage:
|
||||||
1. Copy "DumpKeys.gm9" into the "gm9/scripts/" directory on your SD card.
|
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.
|
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.
|
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" and "gm9/seeddb.bin" files off of your SD card into "(Citra directory)/sysdata/".
|
4. Power off your system and copy the "gm9/aes_keys.txt" file off of your SD card into "(Citra directory)/sysdata/".
|
||||||
|
|
||||||
|
|
5
dist/languages/.tx/config
vendored
5
dist/languages/.tx/config
vendored
|
@ -7,8 +7,3 @@ source_file = en.ts
|
||||||
source_lang = en
|
source_lang = en
|
||||||
type = QT
|
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
|
|
||||||
|
|
1075
dist/languages/da_DK.ts
vendored
1075
dist/languages/da_DK.ts
vendored
File diff suppressed because it is too large
Load diff
1658
dist/languages/de.ts
vendored
1658
dist/languages/de.ts
vendored
File diff suppressed because it is too large
Load diff
1079
dist/languages/el.ts
vendored
1079
dist/languages/el.ts
vendored
File diff suppressed because it is too large
Load diff
1128
dist/languages/es_ES.ts
vendored
1128
dist/languages/es_ES.ts
vendored
File diff suppressed because it is too large
Load diff
1075
dist/languages/fi.ts
vendored
1075
dist/languages/fi.ts
vendored
File diff suppressed because it is too large
Load diff
5293
dist/languages/hu_HU.ts → dist/languages/fi_FI.ts
vendored
5293
dist/languages/hu_HU.ts → dist/languages/fi_FI.ts
vendored
File diff suppressed because it is too large
Load diff
1129
dist/languages/fr.ts
vendored
1129
dist/languages/fr.ts
vendored
File diff suppressed because it is too large
Load diff
1075
dist/languages/id.ts
vendored
1075
dist/languages/id.ts
vendored
File diff suppressed because it is too large
Load diff
1307
dist/languages/it.ts
vendored
1307
dist/languages/it.ts
vendored
File diff suppressed because it is too large
Load diff
1079
dist/languages/ja_JP.ts
vendored
1079
dist/languages/ja_JP.ts
vendored
File diff suppressed because it is too large
Load diff
1083
dist/languages/ko_KR.ts
vendored
1083
dist/languages/ko_KR.ts
vendored
File diff suppressed because it is too large
Load diff
1075
dist/languages/lt_LT.ts
vendored
1075
dist/languages/lt_LT.ts
vendored
File diff suppressed because it is too large
Load diff
1079
dist/languages/nb.ts
vendored
1079
dist/languages/nb.ts
vendored
File diff suppressed because it is too large
Load diff
1083
dist/languages/nl.ts
vendored
1083
dist/languages/nl.ts
vendored
File diff suppressed because it is too large
Load diff
1075
dist/languages/pl_PL.ts
vendored
1075
dist/languages/pl_PL.ts
vendored
File diff suppressed because it is too large
Load diff
1158
dist/languages/pt_BR.ts
vendored
1158
dist/languages/pt_BR.ts
vendored
File diff suppressed because it is too large
Load diff
1130
dist/languages/ro_RO.ts
vendored
1130
dist/languages/ro_RO.ts
vendored
File diff suppressed because it is too large
Load diff
1079
dist/languages/ru_RU.ts
vendored
1079
dist/languages/ru_RU.ts
vendored
File diff suppressed because it is too large
Load diff
1079
dist/languages/tr_TR.ts
vendored
1079
dist/languages/tr_TR.ts
vendored
File diff suppressed because it is too large
Load diff
1079
dist/languages/vi_VN.ts
vendored
1079
dist/languages/vi_VN.ts
vendored
File diff suppressed because it is too large
Load diff
1089
dist/languages/zh_CN.ts
vendored
1089
dist/languages/zh_CN.ts
vendored
File diff suppressed because it is too large
Load diff
1075
dist/languages/zh_TW.ts
vendored
1075
dist/languages/zh_TW.ts
vendored
File diff suppressed because it is too large
Load diff
13
dist/qt_themes/default/style.qss
vendored
13
dist/qt_themes/default/style.qss
vendored
|
@ -12,19 +12,18 @@ QPushButton#GraphicsAPIStatusBarButton:hover {
|
||||||
border: 1px solid #76797C;
|
border: 1px solid #76797C;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton#TogglableStatusBarButton {
|
QPushButton#3DOptionStatusBarButton {
|
||||||
color: #959595;
|
color: #A5A5A5;
|
||||||
|
font-weight: bold;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
padding: 0px 3px 0px 3px;
|
padding: 0px 3px 0px 3px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
min-width: 60px;
|
||||||
|
min-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
QPushButton#TogglableStatusBarButton:checked {
|
QPushButton#3DOptionStatusBarButton:hover {
|
||||||
color: #00FF00;
|
|
||||||
}
|
|
||||||
|
|
||||||
QPushButton#TogglableStatusBarButton:hover {
|
|
||||||
border: 1px solid #76797C;
|
border: 1px solid #76797C;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
33
dist/qt_themes/qdarkstyle/style.qss
vendored
33
dist/qt_themes/qdarkstyle/style.qss
vendored
|
@ -1,3 +1,19 @@
|
||||||
|
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 {
|
QPushButton#GraphicsAPIStatusBarButton {
|
||||||
color: #656565;
|
color: #656565;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
@ -10,23 +26,6 @@ QPushButton#GraphicsAPIStatusBarButton:hover {
|
||||||
border: 1px solid #76797C;
|
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 {
|
QToolTip {
|
||||||
border: 1px solid #76797C;
|
border: 1px solid #76797C;
|
||||||
background-color: #5A7566;
|
background-color: #5A7566;
|
||||||
|
|
157
externals/CMakeLists.txt
vendored
157
externals/CMakeLists.txt
vendored
|
@ -41,15 +41,9 @@ else()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# Catch2
|
# Catch2
|
||||||
add_library(catch2 INTERFACE)
|
set(CATCH_INSTALL_DOCS OFF CACHE BOOL "")
|
||||||
if(USE_SYSTEM_CATCH2)
|
set(CATCH_INSTALL_EXTRAS OFF CACHE BOOL "")
|
||||||
find_package(Catch2 3.0.0 REQUIRED)
|
add_subdirectory(catch2)
|
||||||
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++
|
# Crypto++
|
||||||
if(USE_SYSTEM_CRYPTOPP)
|
if(USE_SYSTEM_CRYPTOPP)
|
||||||
|
@ -57,12 +51,6 @@ if(USE_SYSTEM_CRYPTOPP)
|
||||||
add_library(cryptopp INTERFACE)
|
add_library(cryptopp INTERFACE)
|
||||||
target_link_libraries(cryptopp INTERFACE cryptopp::cryptopp)
|
target_link_libraries(cryptopp INTERFACE cryptopp::cryptopp)
|
||||||
else()
|
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_DOCUMENTATION OFF CACHE BOOL "")
|
||||||
set(CRYPTOPP_BUILD_TESTING OFF CACHE BOOL "")
|
set(CRYPTOPP_BUILD_TESTING OFF CACHE BOOL "")
|
||||||
set(CRYPTOPP_INSTALL OFF CACHE BOOL "")
|
set(CRYPTOPP_INSTALL OFF CACHE BOOL "")
|
||||||
|
@ -126,6 +114,29 @@ if (MSVC)
|
||||||
add_subdirectory(getopt)
|
add_subdirectory(getopt)
|
||||||
endif()
|
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
|
# inih
|
||||||
if(USE_SYSTEM_INIH)
|
if(USE_SYSTEM_INIH)
|
||||||
find_package(inih REQUIRED COMPONENTS inih inir)
|
find_package(inih REQUIRED COMPONENTS inih inir)
|
||||||
|
@ -180,6 +191,9 @@ if(NOT USE_SYSTEM_SOUNDTOUCH)
|
||||||
target_compile_definitions(SoundTouch PUBLIC SOUNDTOUCH_INTEGER_SAMPLES)
|
target_compile_definitions(SoundTouch PUBLIC SOUNDTOUCH_INTEGER_SAMPLES)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# sirit
|
||||||
|
add_subdirectory(sirit EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
# Teakra
|
# Teakra
|
||||||
add_subdirectory(teakra EXCLUDE_FROM_ALL)
|
add_subdirectory(teakra EXCLUDE_FROM_ALL)
|
||||||
|
|
||||||
|
@ -241,18 +255,6 @@ endif()
|
||||||
|
|
||||||
# DiscordRPC
|
# DiscordRPC
|
||||||
if (USE_DISCORD_PRESENCE)
|
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)
|
add_subdirectory(discord-rpc EXCLUDE_FROM_ALL)
|
||||||
target_include_directories(discord-rpc INTERFACE ./discord-rpc/include)
|
target_include_directories(discord-rpc INTERFACE ./discord-rpc/include)
|
||||||
endif()
|
endif()
|
||||||
|
@ -294,20 +296,11 @@ endif()
|
||||||
add_library(httplib INTERFACE)
|
add_library(httplib INTERFACE)
|
||||||
if(USE_SYSTEM_CPP_HTTPLIB)
|
if(USE_SYSTEM_CPP_HTTPLIB)
|
||||||
find_package(CppHttp 0.14.1)
|
find_package(CppHttp 0.14.1)
|
||||||
# Detect if system cpphttplib is a shared library
|
if(CppHttp_FOUND)
|
||||||
# this breaks building as Citra relies on functions that are moved
|
target_link_libraries(httplib INTERFACE httplib::httplib)
|
||||||
# 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()
|
else()
|
||||||
if(CppHttp_FOUND)
|
message(STATUS "Cpp-httplib not found or not suitable version! Falling back to bundled...")
|
||||||
target_link_libraries(httplib INTERFACE httplib::httplib)
|
target_include_directories(httplib SYSTEM INTERFACE ./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()
|
endif()
|
||||||
else()
|
else()
|
||||||
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
|
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
|
||||||
|
@ -315,8 +308,9 @@ endif()
|
||||||
target_compile_options(httplib INTERFACE -DCPPHTTPLIB_OPENSSL_SUPPORT)
|
target_compile_options(httplib INTERFACE -DCPPHTTPLIB_OPENSSL_SUPPORT)
|
||||||
target_link_libraries(httplib INTERFACE ${OPENSSL_LIBRARIES})
|
target_link_libraries(httplib INTERFACE ${OPENSSL_LIBRARIES})
|
||||||
|
|
||||||
if (UNIX AND NOT APPLE)
|
if(ANDROID)
|
||||||
add_subdirectory(gamemode)
|
add_subdirectory(android-ifaddrs)
|
||||||
|
target_link_libraries(httplib INTERFACE ifaddrs)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# cpp-jwt
|
# cpp-jwt
|
||||||
|
@ -367,64 +361,27 @@ if (ENABLE_OPENAL)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
# OpenGL dependencies
|
# VMA
|
||||||
if (ENABLE_OPENGL)
|
if(USE_SYSTEM_VMA)
|
||||||
# Glad
|
add_library(vma INTERFACE)
|
||||||
add_subdirectory(glad)
|
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()
|
endif()
|
||||||
|
|
||||||
# Vulkan dependencies
|
# vulkan-headers
|
||||||
if (ENABLE_VULKAN)
|
add_library(vulkan-headers INTERFACE)
|
||||||
# glslang
|
target_include_directories(vulkan-headers SYSTEM INTERFACE ./vulkan-headers/include)
|
||||||
if(USE_SYSTEM_GLSLANG)
|
if (APPLE)
|
||||||
find_package(glslang REQUIRED)
|
target_include_directories(vulkan-headers SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/MoltenVK)
|
||||||
add_library(glslang INTERFACE)
|
endif()
|
||||||
add_library(SPIRV INTERFACE)
|
|
||||||
target_link_libraries(glslang INTERFACE glslang::glslang)
|
# adrenotools
|
||||||
target_link_libraries(SPIRV INTERFACE glslang::SPIRV)
|
if (ANDROID AND "arm64" IN_LIST ARCHITECTURE)
|
||||||
# System include path is different from submodule include path
|
add_subdirectory(libadrenotools)
|
||||||
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()
|
endif()
|
||||||
|
|
8
externals/android-ifaddrs/CMakeLists.txt
vendored
Normal file
8
externals/android-ifaddrs/CMakeLists.txt
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
add_library(ifaddrs
|
||||||
|
ifaddrs.c
|
||||||
|
ifaddrs.h
|
||||||
|
)
|
||||||
|
|
||||||
|
create_target_directory_groups(ifaddrs)
|
||||||
|
|
||||||
|
target_include_directories(ifaddrs INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})
|
600
externals/android-ifaddrs/ifaddrs.c
vendored
Normal file
600
externals/android-ifaddrs/ifaddrs.c
vendored
Normal file
|
@ -0,0 +1,600 @@
|
||||||
|
/*
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
54
externals/android-ifaddrs/ifaddrs.h
vendored
Normal file
54
externals/android-ifaddrs/ifaddrs.h
vendored
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
/*
|
||||||
|
* 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
|
|
@ -24,8 +24,6 @@ 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_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_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_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
|
# Qt and MoltenVK are handled separately
|
||||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_SDL2 "Disable system SDL2" OFF "USE_SYSTEM_LIBS" OFF)
|
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_SDL2 "Disable system SDL2" OFF "USE_SYSTEM_LIBS" OFF)
|
||||||
|
@ -49,8 +47,6 @@ CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_CUBEB "Disable system cubeb" OFF "USE_SYST
|
||||||
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_LODEPNG "Disable system lodepng" 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_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_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
|
set(LIB_VAR_LIST
|
||||||
SDL2
|
SDL2
|
||||||
|
@ -74,8 +70,6 @@ set(LIB_VAR_LIST
|
||||||
LODEPNG
|
LODEPNG
|
||||||
OPENAL
|
OPENAL
|
||||||
VMA
|
VMA
|
||||||
VULKAN_HEADERS
|
|
||||||
CATCH2
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# First, check that USE_SYSTEM_XXX is not used with USE_SYSTEM_LIBS
|
# First, check that USE_SYSTEM_XXX is not used with USE_SYSTEM_LIBS
|
||||||
|
|
2
externals/cmake-modules/FindOpenAL.cmake
vendored
2
externals/cmake-modules/FindOpenAL.cmake
vendored
|
@ -1,5 +1,5 @@
|
||||||
if(NOT OPENAL_FOUND)
|
if(NOT OPENAL_FOUND)
|
||||||
pkg_check_modules(OPENAL_TMP openal)
|
pkg_check_modules(OOPENAL_TMP libopanal)
|
||||||
|
|
||||||
find_path(OPENAL_INCLUDE_DIRS NAMES al.h
|
find_path(OPENAL_INCLUDE_DIRS NAMES al.h
|
||||||
PATHS
|
PATHS
|
||||||
|
|
9
externals/cmake-modules/Findcryptopp.cmake
vendored
9
externals/cmake-modules/Findcryptopp.cmake
vendored
|
@ -1,19 +1,20 @@
|
||||||
if(NOT CRYPTOPP_FOUND)
|
if(NOT CRYPTOPP_FOUND)
|
||||||
pkg_search_module(CRYPTOPP_TMP crypto++ cryptopp)
|
pkg_check_modules(CRYPTOPP_TMP libcrypto++)
|
||||||
|
|
||||||
find_path(CRYPTOPP_INCLUDE_DIRS NAMES cryptlib.h
|
find_path(CRYPTOPP_INCLUDE_DIRS NAMES cryptlib.h
|
||||||
PATHS
|
PATHS
|
||||||
${CRYPTOPP_TMP_INCLUDE_DIRS}
|
${CRYPTOPP_TMP_INCLUDE_DIRS}
|
||||||
/usr/include
|
/usr/include
|
||||||
|
/usr/include/crypto++
|
||||||
/usr/local/include
|
/usr/local/include
|
||||||
PATH_SUFFIXES crypto++ cryptopp
|
/usr/local/include/crypto++
|
||||||
)
|
)
|
||||||
|
|
||||||
find_library(CRYPTOPP_LIBRARY_DIRS NAMES crypto++ cryptopp
|
find_library(CRYPTOPP_LIBRARY_DIRS NAMES crypto++
|
||||||
PATHS
|
PATHS
|
||||||
${CRYPTOPP_TMP_LIBRARY_DIRS}
|
${CRYPTOPP_TMP_LIBRARY_DIRS}
|
||||||
/usr/lib
|
/usr/lib
|
||||||
/usr/local/lib
|
/usr/locallib
|
||||||
)
|
)
|
||||||
|
|
||||||
if(CRYPTOPP_INCLUDE_DIRS AND CRYPTOPP_LIBRARY_DIRS)
|
if(CRYPTOPP_INCLUDE_DIRS AND CRYPTOPP_LIBRARY_DIRS)
|
||||||
|
|
2
externals/cryptopp-cmake
vendored
2
externals/cryptopp-cmake
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit a99c80c26686e44eddf0432140ae397f3efbd0b3
|
Subproject commit 9327192b0095dc1f420b2082d37bd427b5750d48
|
2
externals/cubeb
vendored
2
externals/cubeb
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit 799e775484b8fce7e986ee7a4f4b651fec2bca07
|
Subproject commit 48689ae7a73caeb747953f9ed664dc71d2f918d8
|
2
externals/dynarmic
vendored
2
externals/dynarmic
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit 30f1a3c6289075ef4af08f5ec502be2fc8627a0c
|
Subproject commit d333a09b3b9152af3cb442902ae8ea18d8416470
|
9
externals/gamemode/CMakeLists.txt
vendored
9
externals/gamemode/CMakeLists.txt
vendored
|
@ -1,9 +0,0 @@
|
||||||
# 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)
|
|
379
externals/gamemode/include/gamemode_client.h
vendored
379
externals/gamemode/include/gamemode_client.h
vendored
|
@ -1,379 +0,0 @@
|
||||||
// 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
|
|
1071
externals/moltenvk/mvk_config.h
vendored
Normal file
1071
externals/moltenvk/mvk_config.h
vendored
Normal file
File diff suppressed because it is too large
Load diff
2
externals/oaknut
vendored
2
externals/oaknut
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit 6b1d57ea7ed4882d32a91eeaa6557b0ecb4da152
|
Subproject commit e6eecc3f9460728be0a8d3f63e66d31c0362f472
|
2
externals/vulkan-headers
vendored
2
externals/vulkan-headers
vendored
|
@ -1 +1 @@
|
||||||
Subproject commit 217e93c664ec6704ec2d8c36fa116c1a4a1e2d40
|
Subproject commit 85c2334e92e215cce34e8e0ed8b2dce4700f4a50
|
BIN
keys.tar.enc
Normal file
BIN
keys.tar.enc
Normal file
Binary file not shown.
|
@ -51,10 +51,6 @@ if (MSVC)
|
||||||
/Zc:throwingNew
|
/Zc:throwingNew
|
||||||
/GT
|
/GT
|
||||||
|
|
||||||
# Some flags for more deterministic builds, to aid caching.
|
|
||||||
/experimental:deterministic
|
|
||||||
/d1trimfile:"${CMAKE_SOURCE_DIR}"
|
|
||||||
|
|
||||||
# External headers diagnostics
|
# External headers diagnostics
|
||||||
/experimental:external # Enables the external headers options. This option isn't required in Visual Studio 2019 version 16.10 and later
|
/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
|
/external:anglebrackets # Treats all headers included by #include <header>, where the header file is enclosed in angle brackets (< >), as external headers
|
||||||
|
@ -91,8 +87,7 @@ if (MSVC)
|
||||||
|
|
||||||
# Since MSVC's debugging information is not very deterministic, so we have to disable it
|
# Since MSVC's debugging information is not very deterministic, so we have to disable it
|
||||||
# when using ccache or other caching tools
|
# when using ccache or other caching tools
|
||||||
if (CMAKE_C_COMPILER_LAUNCHER STREQUAL "ccache" OR CMAKE_CXX_COMPILER_LAUNCHER STREQUAL "ccache"
|
if (CITRA_USE_CCACHE OR CITRA_USE_PRECOMPILED_HEADERS)
|
||||||
OR CITRA_USE_PRECOMPILED_HEADERS)
|
|
||||||
# Precompiled headers are deleted if not using /Z7. See https://github.com/nanoant/CMakePCHCompiler/issues/21
|
# Precompiled headers are deleted if not using /Z7. See https://github.com/nanoant/CMakePCHCompiler/issues/21
|
||||||
add_compile_options(/Z7)
|
add_compile_options(/Z7)
|
||||||
else()
|
else()
|
||||||
|
@ -103,23 +98,19 @@ if (MSVC)
|
||||||
add_compile_options("$<$<CONFIG:Release>:/GS->")
|
add_compile_options("$<$<CONFIG:Release>:/GS->")
|
||||||
|
|
||||||
set(CMAKE_EXE_LINKER_FLAGS_DEBUG "/DEBUG /MANIFEST:NO" CACHE STRING "" FORCE)
|
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 /PDBALTPATH:%_PDB%" CACHE STRING "" FORCE)
|
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "/DEBUG /MANIFEST:NO /INCREMENTAL:NO /OPT:REF,ICF" CACHE STRING "" FORCE)
|
||||||
else()
|
else()
|
||||||
add_compile_options(
|
add_compile_options(
|
||||||
-Wall
|
-Wall
|
||||||
# In case a flag isn't supported on e.g. a certain architecture, don't error.
|
# In case a flag isn't supported on e.g. a certain architecture, don't error.
|
||||||
-Wno-unused-command-line-argument
|
-Wno-unused-command-line-argument
|
||||||
# Build fortification options
|
# Build fortification options
|
||||||
|
-Wp,-D_FORTIFY_SOURCE=2
|
||||||
-Wp,-D_GLIBCXX_ASSERTIONS
|
-Wp,-D_GLIBCXX_ASSERTIONS
|
||||||
-fstack-protector-strong
|
-fstack-protector-strong
|
||||||
-fstack-clash-protection
|
-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)
|
if (CITRA_WARNINGS_AS_ERRORS)
|
||||||
add_compile_options(-Werror)
|
add_compile_options(-Werror)
|
||||||
endif()
|
endif()
|
||||||
|
@ -128,13 +119,6 @@ else()
|
||||||
add_compile_options("-stdlib=libc++")
|
add_compile_options("-stdlib=libc++")
|
||||||
endif()
|
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)
|
if (MINGW)
|
||||||
add_definitions(-DMINGW_HAS_SECURE_API)
|
add_definitions(-DMINGW_HAS_SECURE_API)
|
||||||
if (COMPILE_WITH_DWARF)
|
if (COMPILE_WITH_DWARF)
|
||||||
|
@ -159,16 +143,6 @@ else()
|
||||||
endif()
|
endif()
|
||||||
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(common)
|
||||||
add_subdirectory(core)
|
add_subdirectory(core)
|
||||||
add_subdirectory(video_core)
|
add_subdirectory(video_core)
|
||||||
|
|
|
@ -10,7 +10,7 @@ plugins {
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("de.undercouch.download") version "5.5.0"
|
id("de.undercouch.download") version "5.5.0"
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
kotlin("plugin.serialization") version "1.9.22"
|
kotlin("plugin.serialization") version "1.8.21"
|
||||||
id("androidx.navigation.safeargs.kotlin")
|
id("androidx.navigation.safeargs.kotlin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,7 +29,7 @@ android {
|
||||||
namespace = "org.citra.citra_emu"
|
namespace = "org.citra.citra_emu"
|
||||||
|
|
||||||
compileSdkVersion = "android-34"
|
compileSdkVersion = "android-34"
|
||||||
ndkVersion = "26.1.10909125"
|
ndkVersion = "25.2.9519653"
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility = JavaVersion.VERSION_17
|
sourceCompatibility = JavaVersion.VERSION_17
|
||||||
|
@ -40,10 +40,6 @@ android {
|
||||||
jvmTarget = "17"
|
jvmTarget = "17"
|
||||||
}
|
}
|
||||||
|
|
||||||
androidResources {
|
|
||||||
generateLocaleConfig = true
|
|
||||||
}
|
|
||||||
|
|
||||||
packaging {
|
packaging {
|
||||||
// This is necessary for libadrenotools custom driver loading
|
// This is necessary for libadrenotools custom driver loading
|
||||||
jniLibs.useLegacyPackaging = true
|
jniLibs.useLegacyPackaging = true
|
||||||
|
@ -173,23 +169,27 @@ android {
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
implementation("androidx.recyclerview:recyclerview:1.3.2")
|
||||||
implementation("androidx.activity:activity-ktx:1.8.2")
|
implementation("androidx.activity:activity-ktx:1.8.0")
|
||||||
implementation("androidx.fragment:fragment-ktx:1.6.2")
|
implementation("androidx.fragment:fragment-ktx:1.6.2")
|
||||||
implementation("androidx.appcompat:appcompat:1.6.1")
|
implementation("androidx.appcompat:appcompat:1.6.1")
|
||||||
implementation("androidx.documentfile:documentfile:1.0.1")
|
implementation("androidx.documentfile:documentfile:1.0.1")
|
||||||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
|
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
|
||||||
implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
|
implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
|
||||||
implementation("com.google.android.material:material:1.9.0")
|
implementation("com.google.android.material:material:1.9.0")
|
||||||
implementation("androidx.core:core-splashscreen:1.0.1")
|
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||||
implementation("androidx.work:work-runtime:2.9.0")
|
implementation("androidx.work:work-runtime:2.8.1")
|
||||||
|
|
||||||
|
// For loading huge screenshots from the disk.
|
||||||
|
implementation("com.squareup.picasso:picasso:2.71828")
|
||||||
|
|
||||||
implementation("org.ini4j:ini4j:0.5.4")
|
implementation("org.ini4j:ini4j:0.5.4")
|
||||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||||
implementation("androidx.navigation:navigation-fragment-ktx:2.7.6")
|
implementation("androidx.navigation:navigation-fragment-ktx:2.7.5")
|
||||||
implementation("androidx.navigation:navigation-ui-ktx:2.7.6")
|
implementation("androidx.navigation:navigation-ui-ktx:2.7.5")
|
||||||
implementation("info.debatty:java-string-similarity:2.0.0")
|
implementation("info.debatty:java-string-similarity:2.0.0")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
|
||||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||||
implementation("io.coil-kt:coil:2.5.0")
|
implementation("io.coil-kt:coil:2.2.2")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download Vulkan Validation Layers from the KhronosGroup GitHub.
|
// Download Vulkan Validation Layers from the KhronosGroup GitHub.
|
||||||
|
|
|
@ -42,9 +42,6 @@
|
||||||
android:banner="@mipmap/ic_launcher"
|
android:banner="@mipmap/ic_launcher"
|
||||||
android:requestLegacyExternalStorage="true">
|
android:requestLegacyExternalStorage="true">
|
||||||
|
|
||||||
<meta-data android:name="android.game_mode_config"
|
|
||||||
android:resource="@xml/game_mode_config" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name="org.citra.citra_emu.ui.main.MainActivity"
|
android:name="org.citra.citra_emu.ui.main.MainActivity"
|
||||||
android:theme="@style/Theme.Citra.Splash.Main"
|
android:theme="@style/Theme.Citra.Splash.Main"
|
||||||
|
@ -67,18 +64,9 @@
|
||||||
<activity
|
<activity
|
||||||
android:name="org.citra.citra_emu.activities.EmulationActivity"
|
android:name="org.citra.citra_emu.activities.EmulationActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
android:resizeableActivity="false"
|
||||||
android:theme="@style/Theme.Citra.Main"
|
android:theme="@style/Theme.Citra.Main"
|
||||||
android:launchMode="singleTop">
|
android:launchMode="singleTop"/>
|
||||||
|
|
||||||
<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">
|
<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"/>
|
<property android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE" android:value="Keep emulation running in background"/>
|
||||||
|
|
|
@ -9,13 +9,10 @@ import android.app.Application
|
||||||
import android.app.NotificationChannel
|
import android.app.NotificationChannel
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
|
||||||
import org.citra.citra_emu.utils.DirectoryInitialization
|
import org.citra.citra_emu.utils.DirectoryInitialization
|
||||||
import org.citra.citra_emu.utils.DocumentsTree
|
import org.citra.citra_emu.utils.DocumentsTree
|
||||||
import org.citra.citra_emu.utils.GpuDriverHelper
|
import org.citra.citra_emu.utils.GpuDriverHelper
|
||||||
import org.citra.citra_emu.utils.PermissionsHandler
|
import org.citra.citra_emu.utils.PermissionsHandler
|
||||||
import org.citra.citra_emu.utils.Log
|
|
||||||
import org.citra.citra_emu.utils.MemoryUtil
|
|
||||||
|
|
||||||
class CitraApplication : Application() {
|
class CitraApplication : Application() {
|
||||||
private fun createNotificationChannel() {
|
private fun createNotificationChannel() {
|
||||||
|
@ -56,20 +53,9 @@ class CitraApplication : Application() {
|
||||||
}
|
}
|
||||||
|
|
||||||
NativeLibrary.logDeviceInfo()
|
NativeLibrary.logDeviceInfo()
|
||||||
logDeviceInfo()
|
|
||||||
createNotificationChannel()
|
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 {
|
companion object {
|
||||||
private var application: CitraApplication? = null
|
private var application: CitraApplication? = null
|
||||||
|
|
||||||
|
|
|
@ -252,7 +252,7 @@ object NativeLibrary {
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun landscapeScreenLayout(): Int = EmulationMenuSettings.landscapeScreenLayout
|
fun landscapeScreenLayout(): Int = EmulationMenuSettings.getLandscapeScreenLayout()
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
@ -413,12 +413,12 @@ object NativeLibrary {
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setEmulationActivity(emulationActivity: EmulationActivity?) {
|
fun setEmulationActivity(emulationActivity: EmulationActivity?) {
|
||||||
Log.debug("[NativeLibrary] Registering EmulationActivity.")
|
Log.verbose("[NativeLibrary] Registering EmulationActivity.")
|
||||||
sEmulationActivity = WeakReference(emulationActivity)
|
sEmulationActivity = WeakReference(emulationActivity)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearEmulationActivity() {
|
fun clearEmulationActivity() {
|
||||||
Log.debug("[NativeLibrary] Unregistering EmulationActivity.")
|
Log.verbose("[NativeLibrary] Unregistering EmulationActivity.")
|
||||||
sEmulationActivity.clear()
|
sEmulationActivity.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -515,6 +515,14 @@ object NativeLibrary {
|
||||||
*/
|
*/
|
||||||
external fun logDeviceInfo()
|
external fun logDeviceInfo()
|
||||||
|
|
||||||
|
external fun loadSystemConfig()
|
||||||
|
|
||||||
|
external fun saveSystemConfig()
|
||||||
|
|
||||||
|
external fun setSystemSetupNeeded(needed: Boolean)
|
||||||
|
|
||||||
|
external fun getIsSystemSetupNeeded(): Boolean
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun createFile(directory: String, filename: String): Boolean =
|
fun createFile(directory: String, filename: String): Boolean =
|
||||||
|
|
|
@ -0,0 +1,788 @@
|
||||||
|
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.view.WindowManager;
|
||||||
|
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.Log;
|
||||||
|
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) {
|
||||||
|
Log.gameLaunched = true;
|
||||||
|
ThemeUtil.INSTANCE.setTheme(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.INSTANCE.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRestart() {
|
||||||
|
super.onRestart();
|
||||||
|
NativeLibrary.INSTANCE.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.INSTANCE.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.INSTANCE.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() {
|
||||||
|
// TODO: Remove this once we properly account for display insets in the input overlay
|
||||||
|
getWindow().getAttributes().layoutInDisplayCutoutMode =
|
||||||
|
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
|
||||||
|
|
||||||
|
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.Companion.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.INSTANCE.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.INSTANCE.saveState(slot);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
loadStateMenu.add(text).setEnabled(false).setOnMenuItemClickListener((item) -> {
|
||||||
|
NativeLibrary.INSTANCE.loadState(slot);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (final NativeLibrary.SaveStateInfo info : savestates) {
|
||||||
|
final String text = getString(R.string.emulation_occupied_state_slot, info.getSlot(), info.getTime());
|
||||||
|
saveStateMenu.getItem(info.getSlot() - 1).setTitle(text);
|
||||||
|
loadStateMenu.getItem(info.getSlot() - 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.INSTANCE.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.INSTANCE.getRunningTitleId());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MENU_ACTION_CLOSE_GAME:
|
||||||
|
NativeLibrary.INSTANCE.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.INSTANCE.unPauseEmulation())
|
||||||
|
.setOnCancelListener(dialogInterface -> NativeLibrary.INSTANCE.unPauseEmulation())
|
||||||
|
.show();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void changeScreenOrientation(int layoutOption, MenuItem item) {
|
||||||
|
item.setChecked(true);
|
||||||
|
NativeLibrary.INSTANCE.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.INSTANCE.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.INSTANCE.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.INSTANCE.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.INSTANCE.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_LEFT, axisValuesCirclePad[0], axisValuesCirclePad[1]);
|
||||||
|
NativeLibrary.INSTANCE.onGamePadMoveEvent(input.getDescriptor(), NativeLibrary.ButtonType.STICK_C, axisValuesCStick[0], axisValuesCStick[1]);
|
||||||
|
|
||||||
|
// Triggers L/R and ZL/ZR
|
||||||
|
if (isTriggerPressedLMapped) {
|
||||||
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_L, isTriggerPressedL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||||
|
}
|
||||||
|
if (isTriggerPressedRMapped) {
|
||||||
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.TRIGGER_R, isTriggerPressedR ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||||
|
}
|
||||||
|
if (isTriggerPressedZLMapped) {
|
||||||
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.BUTTON_ZL, isTriggerPressedZL ? NativeLibrary.ButtonState.PRESSED : NativeLibrary.ButtonState.RELEASED);
|
||||||
|
}
|
||||||
|
if (isTriggerPressedZRMapped) {
|
||||||
|
NativeLibrary.INSTANCE.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.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
|
||||||
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
|
||||||
|
}
|
||||||
|
if (axisValuesDPad[0] < 0.f) {
|
||||||
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.PRESSED);
|
||||||
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.RELEASED);
|
||||||
|
}
|
||||||
|
if (axisValuesDPad[0] > 0.f) {
|
||||||
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_LEFT, NativeLibrary.ButtonState.RELEASED);
|
||||||
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_RIGHT, NativeLibrary.ButtonState.PRESSED);
|
||||||
|
}
|
||||||
|
if (axisValuesDPad[1] == 0.f) {
|
||||||
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
|
||||||
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
|
||||||
|
}
|
||||||
|
if (axisValuesDPad[1] < 0.f) {
|
||||||
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.PRESSED);
|
||||||
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_DOWN, NativeLibrary.ButtonState.RELEASED);
|
||||||
|
}
|
||||||
|
if (axisValuesDPad[1] > 0.f) {
|
||||||
|
NativeLibrary.INSTANCE.onGamePadEvent(NativeLibrary.TouchScreenDevice, NativeLibrary.ButtonType.DPAD_UP, NativeLibrary.ButtonState.RELEASED);
|
||||||
|
NativeLibrary.INSTANCE.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 {
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,464 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,7 +15,6 @@ import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.navigation.findNavController
|
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.AsyncDifferConfig
|
import androidx.recyclerview.widget.AsyncDifferConfig
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
@ -23,12 +22,12 @@ import androidx.recyclerview.widget.ListAdapter
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import org.citra.citra_emu.HomeNavigationDirections
|
|
||||||
import org.citra.citra_emu.CitraApplication
|
import org.citra.citra_emu.CitraApplication
|
||||||
import org.citra.citra_emu.R
|
import org.citra.citra_emu.R
|
||||||
|
import org.citra.citra_emu.activities.EmulationActivity
|
||||||
import org.citra.citra_emu.adapters.GameAdapter.GameViewHolder
|
import org.citra.citra_emu.adapters.GameAdapter.GameViewHolder
|
||||||
import org.citra.citra_emu.databinding.CardGameBinding
|
import org.citra.citra_emu.databinding.CardGameBinding
|
||||||
import org.citra.citra_emu.features.cheats.ui.CheatsFragmentDirections
|
import org.citra.citra_emu.features.cheats.ui.CheatsActivity
|
||||||
import org.citra.citra_emu.model.Game
|
import org.citra.citra_emu.model.Game
|
||||||
import org.citra.citra_emu.utils.GameIconUtils
|
import org.citra.citra_emu.utils.GameIconUtils
|
||||||
import org.citra.citra_emu.viewmodel.GamesViewModel
|
import org.citra.citra_emu.viewmodel.GamesViewModel
|
||||||
|
@ -78,8 +77,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
||||||
)
|
)
|
||||||
.apply()
|
.apply()
|
||||||
|
|
||||||
val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game)
|
EmulationActivity.launch(activity, holder.game.path, holder.game.title)
|
||||||
view.findNavController().navigate(action)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -99,8 +97,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
|
||||||
.setPositiveButton(android.R.string.ok, null)
|
.setPositiveButton(android.R.string.ok, null)
|
||||||
.show()
|
.show()
|
||||||
} else {
|
} else {
|
||||||
val action = CheatsFragmentDirections.actionGlobalCheatsFragment(holder.game.titleId)
|
CheatsActivity.launch(view.context, holder.game.titleId)
|
||||||
view.findNavController().navigate(action)
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,129 @@
|
||||||
|
// 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.Keep;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
public final class MiiSelector {
|
||||||
|
@Keep
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,47 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
|
@ -0,0 +1,279 @@
|
||||||
|
// 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.content.res.Resources;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.InputFilter;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
|
import androidx.annotation.ColorInt;
|
||||||
|
import androidx.annotation.Keep;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
@Keep
|
||||||
|
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.Companion.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.Companion.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)});
|
||||||
|
|
||||||
|
TypedValue typedValue = new TypedValue();
|
||||||
|
Resources.Theme theme = requireContext().getTheme();
|
||||||
|
theme.resolveAttribute(R.attr.colorOnSurface, typedValue, true);
|
||||||
|
@ColorInt int color = typedValue.data;
|
||||||
|
editText.setHintTextColor(color);
|
||||||
|
editText.setTextColor(color);
|
||||||
|
|
||||||
|
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.Companion.getAppContext().getResources().getString(R.string.software_keyboard),
|
||||||
|
error, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static native ValidationError ValidateFilters(String text);
|
||||||
|
|
||||||
|
private static native ValidationError ValidateInput(String text);
|
||||||
|
}
|
|
@ -1,152 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,68 @@
|
||||||
|
// 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.Keep;
|
||||||
|
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.
|
||||||
|
@Keep
|
||||||
|
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.
|
||||||
|
@Keep
|
||||||
|
@Nullable
|
||||||
|
public static Bitmap LoadImageFromFile(String uri, int width, int height) {
|
||||||
|
return PicassoUtils.LoadBitmapFromFile(uri, width, height);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,67 +0,0 @@
|
||||||
// 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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
|
@ -0,0 +1,140 @@
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,42 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
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);
|
||||||
|
}
|
|
@ -1,48 +0,0 @@
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
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();
|
||||||
|
}
|
|
@ -1,19 +0,0 @@
|
||||||
// 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?)
|
|
||||||
}
|
|
|
@ -0,0 +1,187 @@
|
||||||
|
package org.citra.citra_emu.features.cheats.model;
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData;
|
||||||
|
import androidx.lifecycle.MutableLiveData;
|
||||||
|
import androidx.lifecycle.ViewModel;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
public class CheatsViewModel extends ViewModel {
|
||||||
|
private int mSelectedCheatPosition = -1;
|
||||||
|
private final MutableLiveData<Cheat> mSelectedCheat = new MutableLiveData<>(null);
|
||||||
|
private final MutableLiveData<Boolean> mIsAdding = new MutableLiveData<>(false);
|
||||||
|
private final MutableLiveData<Boolean> mIsEditing = new MutableLiveData<>(false);
|
||||||
|
|
||||||
|
private final MutableLiveData<Integer> mCheatAddedEvent = new MutableLiveData<>(null);
|
||||||
|
private final MutableLiveData<Integer> mCheatChangedEvent = new MutableLiveData<>(null);
|
||||||
|
private final MutableLiveData<Integer> mCheatDeletedEvent = new MutableLiveData<>(null);
|
||||||
|
private final MutableLiveData<Boolean> mOpenDetailsViewEvent = new MutableLiveData<>(false);
|
||||||
|
|
||||||
|
private CheatEngine mCheatEngine;
|
||||||
|
private Cheat[] mCheats;
|
||||||
|
private boolean mCheatsNeedSaving = false;
|
||||||
|
|
||||||
|
public void initialize(long titleId) {
|
||||||
|
mCheatEngine = new CheatEngine(titleId);
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void load() {
|
||||||
|
mCheats = mCheatEngine.getCheats();
|
||||||
|
|
||||||
|
for (int i = 0; i < mCheats.length; i++) {
|
||||||
|
int position = i;
|
||||||
|
mCheats[i].setEnabledChangedCallback(() -> {
|
||||||
|
mCheatsNeedSaving = true;
|
||||||
|
notifyCheatUpdated(position);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void saveIfNeeded() {
|
||||||
|
if (mCheatsNeedSaving) {
|
||||||
|
mCheatEngine.saveCheatFile();
|
||||||
|
mCheatsNeedSaving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Cheat[] getCheats() {
|
||||||
|
return mCheats;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<Cheat> getSelectedCheat() {
|
||||||
|
return mSelectedCheat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelectedCheat(Cheat cheat, int position) {
|
||||||
|
if (mIsEditing.getValue()) {
|
||||||
|
setIsEditing(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
mSelectedCheat.setValue(cheat);
|
||||||
|
mSelectedCheatPosition = position;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<Boolean> getIsAdding() {
|
||||||
|
return mIsAdding;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<Boolean> getIsEditing() {
|
||||||
|
return mIsEditing;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setIsEditing(boolean isEditing) {
|
||||||
|
mIsEditing.setValue(isEditing);
|
||||||
|
|
||||||
|
if (mIsAdding.getValue() && !isEditing) {
|
||||||
|
mIsAdding.setValue(false);
|
||||||
|
setSelectedCheat(null, -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a cheat is added, the integer stored in the returned LiveData
|
||||||
|
* changes to the position of that cheat, then changes back to null.
|
||||||
|
*/
|
||||||
|
public LiveData<Integer> getCheatAddedEvent() {
|
||||||
|
return mCheatAddedEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyCheatAdded(int position) {
|
||||||
|
mCheatAddedEvent.setValue(position);
|
||||||
|
mCheatAddedEvent.setValue(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startAddingCheat() {
|
||||||
|
mSelectedCheat.setValue(null);
|
||||||
|
mSelectedCheatPosition = -1;
|
||||||
|
|
||||||
|
mIsAdding.setValue(true);
|
||||||
|
mIsEditing.setValue(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void finishAddingCheat(Cheat cheat) {
|
||||||
|
if (!mIsAdding.getValue()) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
|
||||||
|
mIsAdding.setValue(false);
|
||||||
|
mIsEditing.setValue(false);
|
||||||
|
|
||||||
|
int position = mCheats.length;
|
||||||
|
|
||||||
|
mCheatEngine.addCheat(cheat);
|
||||||
|
|
||||||
|
mCheatsNeedSaving = true;
|
||||||
|
load();
|
||||||
|
|
||||||
|
notifyCheatAdded(position);
|
||||||
|
setSelectedCheat(mCheats[position], position);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a cheat is edited, the integer stored in the returned LiveData
|
||||||
|
* changes to the position of that cheat, then changes back to null.
|
||||||
|
*/
|
||||||
|
public LiveData<Integer> getCheatUpdatedEvent() {
|
||||||
|
return mCheatChangedEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies that an edit has been made to the contents of the cheat at the given position.
|
||||||
|
*/
|
||||||
|
private void notifyCheatUpdated(int position) {
|
||||||
|
mCheatChangedEvent.setValue(position);
|
||||||
|
mCheatChangedEvent.setValue(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateSelectedCheat(Cheat newCheat) {
|
||||||
|
mCheatEngine.updateCheat(mSelectedCheatPosition, newCheat);
|
||||||
|
|
||||||
|
mCheatsNeedSaving = true;
|
||||||
|
load();
|
||||||
|
|
||||||
|
notifyCheatUpdated(mSelectedCheatPosition);
|
||||||
|
setSelectedCheat(mCheats[mSelectedCheatPosition], mSelectedCheatPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a cheat is deleted, the integer stored in the returned LiveData
|
||||||
|
* changes to the position of that cheat, then changes back to null.
|
||||||
|
*/
|
||||||
|
public LiveData<Integer> getCheatDeletedEvent() {
|
||||||
|
return mCheatDeletedEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies that the cheat at the given position has been deleted.
|
||||||
|
*/
|
||||||
|
private void notifyCheatDeleted(int position) {
|
||||||
|
mCheatDeletedEvent.setValue(position);
|
||||||
|
mCheatDeletedEvent.setValue(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteSelectedCheat() {
|
||||||
|
int position = mSelectedCheatPosition;
|
||||||
|
|
||||||
|
setSelectedCheat(null, -1);
|
||||||
|
|
||||||
|
mCheatEngine.removeCheat(position);
|
||||||
|
|
||||||
|
mCheatsNeedSaving = true;
|
||||||
|
load();
|
||||||
|
|
||||||
|
notifyCheatDeleted(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LiveData<Boolean> getOpenDetailsViewEvent() {
|
||||||
|
return mOpenDetailsViewEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void openDetailsView() {
|
||||||
|
mOpenDetailsViewEvent.setValue(true);
|
||||||
|
mOpenDetailsViewEvent.setValue(false);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,170 +0,0 @@
|
||||||
// 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.lifecycle.ViewModel
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
|
|
||||||
class CheatsViewModel : ViewModel() {
|
|
||||||
val selectedCheat get() = _selectedCheat.asStateFlow()
|
|
||||||
private val _selectedCheat = MutableStateFlow<Cheat?>(null)
|
|
||||||
|
|
||||||
val isAdding get() = _isAdding.asStateFlow()
|
|
||||||
private val _isAdding = MutableStateFlow(false)
|
|
||||||
|
|
||||||
val isEditing get() = _isEditing.asStateFlow()
|
|
||||||
private val _isEditing = MutableStateFlow(false)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When a cheat is added, the integer stored in the returned StateFlow
|
|
||||||
* changes to the position of that cheat, then changes back to null.
|
|
||||||
*/
|
|
||||||
val cheatAddedEvent get() = _cheatAddedEvent.asStateFlow()
|
|
||||||
private val _cheatAddedEvent = MutableStateFlow<Int?>(null)
|
|
||||||
|
|
||||||
val cheatChangedEvent get() = _cheatChangedEvent.asStateFlow()
|
|
||||||
private val _cheatChangedEvent = MutableStateFlow<Int?>(null)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When a cheat is deleted, the integer stored in the returned StateFlow
|
|
||||||
* changes to the position of that cheat, then changes back to null.
|
|
||||||
*/
|
|
||||||
val cheatDeletedEvent get() = _cheatDeletedEvent.asStateFlow()
|
|
||||||
private val _cheatDeletedEvent = MutableStateFlow<Int?>(null)
|
|
||||||
|
|
||||||
val openDetailsViewEvent get() = _openDetailsViewEvent.asStateFlow()
|
|
||||||
private val _openDetailsViewEvent = MutableStateFlow(false)
|
|
||||||
|
|
||||||
val closeDetailsViewEvent get() = _closeDetailsViewEvent.asStateFlow()
|
|
||||||
private val _closeDetailsViewEvent = MutableStateFlow(false)
|
|
||||||
|
|
||||||
val listViewFocusChange get() = _listViewFocusChange.asStateFlow()
|
|
||||||
private val _listViewFocusChange = MutableStateFlow(false)
|
|
||||||
|
|
||||||
val detailsViewFocusChange get() = _detailsViewFocusChange.asStateFlow()
|
|
||||||
private val _detailsViewFocusChange = MutableStateFlow(false)
|
|
||||||
|
|
||||||
private var titleId: Long = 0
|
|
||||||
lateinit var cheats: Array<Cheat>
|
|
||||||
private var cheatsNeedSaving = false
|
|
||||||
private var selectedCheatPosition = -1
|
|
||||||
|
|
||||||
fun initialize(titleId_: Long) {
|
|
||||||
titleId = titleId_;
|
|
||||||
load()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun load() {
|
|
||||||
CheatEngine.loadCheatFile(titleId)
|
|
||||||
cheats = CheatEngine.getCheats()
|
|
||||||
for (i in cheats.indices) {
|
|
||||||
cheats[i].setEnabledChangedCallback {
|
|
||||||
cheatsNeedSaving = true
|
|
||||||
notifyCheatUpdated(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveIfNeeded() {
|
|
||||||
if (cheatsNeedSaving) {
|
|
||||||
CheatEngine.saveCheatFile(titleId)
|
|
||||||
cheatsNeedSaving = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setSelectedCheat(cheat: Cheat?, position: Int) {
|
|
||||||
if (isEditing.value) {
|
|
||||||
setIsEditing(false)
|
|
||||||
}
|
|
||||||
_selectedCheat.value = cheat
|
|
||||||
selectedCheatPosition = position
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setIsEditing(value: Boolean) {
|
|
||||||
_isEditing.value = value
|
|
||||||
if (isAdding.value && !value) {
|
|
||||||
_isAdding.value = false
|
|
||||||
setSelectedCheat(null, -1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun notifyCheatAdded(position: Int) {
|
|
||||||
_cheatAddedEvent.value = position
|
|
||||||
_cheatAddedEvent.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun startAddingCheat() {
|
|
||||||
_selectedCheat.value = null
|
|
||||||
selectedCheatPosition = -1
|
|
||||||
_isAdding.value = true
|
|
||||||
_isEditing.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun finishAddingCheat(cheat: Cheat?) {
|
|
||||||
check(isAdding.value)
|
|
||||||
_isAdding.value = false
|
|
||||||
_isEditing.value = false
|
|
||||||
val position = cheats.size
|
|
||||||
CheatEngine.addCheat(cheat)
|
|
||||||
cheatsNeedSaving = true
|
|
||||||
load()
|
|
||||||
notifyCheatAdded(position)
|
|
||||||
setSelectedCheat(cheats[position], position)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies that an edit has been made to the contents of the cheat at the given position.
|
|
||||||
*/
|
|
||||||
private fun notifyCheatUpdated(position: Int) {
|
|
||||||
_cheatChangedEvent.value = position
|
|
||||||
_cheatChangedEvent.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateSelectedCheat(newCheat: Cheat?) {
|
|
||||||
CheatEngine.updateCheat(selectedCheatPosition, newCheat)
|
|
||||||
cheatsNeedSaving = true
|
|
||||||
load()
|
|
||||||
notifyCheatUpdated(selectedCheatPosition)
|
|
||||||
setSelectedCheat(cheats[selectedCheatPosition], selectedCheatPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies that the cheat at the given position has been deleted.
|
|
||||||
*/
|
|
||||||
private fun notifyCheatDeleted(position: Int) {
|
|
||||||
_cheatDeletedEvent.value = position
|
|
||||||
_cheatDeletedEvent.value = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteSelectedCheat() {
|
|
||||||
val position = selectedCheatPosition
|
|
||||||
setSelectedCheat(null, -1)
|
|
||||||
CheatEngine.removeCheat(position)
|
|
||||||
cheatsNeedSaving = true
|
|
||||||
load()
|
|
||||||
notifyCheatDeleted(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openDetailsView() {
|
|
||||||
_openDetailsViewEvent.value = true
|
|
||||||
_openDetailsViewEvent.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun closeDetailsView() {
|
|
||||||
_closeDetailsViewEvent.value = true
|
|
||||||
_closeDetailsViewEvent.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onListViewFocusChanged(changed: Boolean) {
|
|
||||||
_listViewFocusChange.value = changed
|
|
||||||
_listViewFocusChange.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDetailsViewFocusChanged(changed: Boolean) {
|
|
||||||
_detailsViewFocusChange.value = changed
|
|
||||||
_detailsViewFocusChange.value = false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,175 @@
|
||||||
|
package org.citra.citra_emu.features.cheats.ui;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.ScrollView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
|
import org.citra.citra_emu.R;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.Cheat;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
||||||
|
|
||||||
|
public class CheatDetailsFragment extends Fragment {
|
||||||
|
private View mRoot;
|
||||||
|
private ScrollView mScrollView;
|
||||||
|
private TextView mLabelName;
|
||||||
|
private EditText mEditName;
|
||||||
|
private EditText mEditNotes;
|
||||||
|
private EditText mEditCode;
|
||||||
|
private Button mButtonDelete;
|
||||||
|
private Button mButtonEdit;
|
||||||
|
private Button mButtonCancel;
|
||||||
|
private Button mButtonOk;
|
||||||
|
|
||||||
|
private CheatsViewModel mViewModel;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_cheat_details, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
mRoot = view.findViewById(R.id.root);
|
||||||
|
mScrollView = view.findViewById(R.id.scroll_view);
|
||||||
|
mLabelName = view.findViewById(R.id.label_name);
|
||||||
|
mEditName = view.findViewById(R.id.edit_name);
|
||||||
|
mEditNotes = view.findViewById(R.id.edit_notes);
|
||||||
|
mEditCode = view.findViewById(R.id.edit_code);
|
||||||
|
mButtonDelete = view.findViewById(R.id.button_delete);
|
||||||
|
mButtonEdit = view.findViewById(R.id.button_edit);
|
||||||
|
mButtonCancel = view.findViewById(R.id.button_cancel);
|
||||||
|
mButtonOk = view.findViewById(R.id.button_ok);
|
||||||
|
|
||||||
|
CheatsActivity activity = (CheatsActivity) requireActivity();
|
||||||
|
mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
|
||||||
|
|
||||||
|
mViewModel.getSelectedCheat().observe(getViewLifecycleOwner(),
|
||||||
|
this::onSelectedCheatUpdated);
|
||||||
|
mViewModel.getIsEditing().observe(getViewLifecycleOwner(), this::onIsEditingUpdated);
|
||||||
|
|
||||||
|
mButtonDelete.setOnClickListener(this::onDeleteClicked);
|
||||||
|
mButtonEdit.setOnClickListener(this::onEditClicked);
|
||||||
|
mButtonCancel.setOnClickListener(this::onCancelClicked);
|
||||||
|
mButtonOk.setOnClickListener(this::onOkClicked);
|
||||||
|
|
||||||
|
// On a portrait phone screen (or other narrow screen), only one of the two panes are shown
|
||||||
|
// at the same time. If the user is navigating using a d-pad and moves focus to an element
|
||||||
|
// in the currently hidden pane, we need to manually show that pane.
|
||||||
|
CheatsActivity.setOnFocusChangeListenerRecursively(view,
|
||||||
|
(v, hasFocus) -> activity.onDetailsViewFocusChange(hasFocus));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearEditErrors() {
|
||||||
|
mEditName.setError(null);
|
||||||
|
mEditCode.setError(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onDeleteClicked(View view) {
|
||||||
|
String name = mEditName.getText().toString();
|
||||||
|
|
||||||
|
new MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setMessage(getString(R.string.cheats_delete_confirmation, name))
|
||||||
|
.setPositiveButton(android.R.string.yes,
|
||||||
|
(dialog, i) -> mViewModel.deleteSelectedCheat())
|
||||||
|
.setNegativeButton(android.R.string.no, null)
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onEditClicked(View view) {
|
||||||
|
mViewModel.setIsEditing(true);
|
||||||
|
mButtonOk.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onCancelClicked(View view) {
|
||||||
|
mViewModel.setIsEditing(false);
|
||||||
|
onSelectedCheatUpdated(mViewModel.getSelectedCheat().getValue());
|
||||||
|
mButtonDelete.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onOkClicked(View view) {
|
||||||
|
clearEditErrors();
|
||||||
|
|
||||||
|
String name = mEditName.getText().toString();
|
||||||
|
String notes = mEditNotes.getText().toString();
|
||||||
|
String code = mEditCode.getText().toString();
|
||||||
|
|
||||||
|
if (name.isEmpty()) {
|
||||||
|
mEditName.setError(getString(R.string.cheats_error_no_name));
|
||||||
|
mScrollView.smoothScrollTo(0, mLabelName.getTop());
|
||||||
|
return;
|
||||||
|
} else if (code.isEmpty()) {
|
||||||
|
mEditCode.setError(getString(R.string.cheats_error_no_code_lines));
|
||||||
|
mScrollView.smoothScrollTo(0, mEditCode.getBottom());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int validityResult = Cheat.isValidGatewayCode(code);
|
||||||
|
|
||||||
|
if (validityResult != 0) {
|
||||||
|
mEditCode.setError(getString(R.string.cheats_error_on_line, validityResult));
|
||||||
|
mScrollView.smoothScrollTo(0, mEditCode.getBottom());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cheat newCheat = Cheat.createGatewayCode(name, notes, code);
|
||||||
|
|
||||||
|
if (mViewModel.getIsAdding().getValue()) {
|
||||||
|
mViewModel.finishAddingCheat(newCheat);
|
||||||
|
} else {
|
||||||
|
mViewModel.updateSelectedCheat(newCheat);
|
||||||
|
}
|
||||||
|
|
||||||
|
mButtonEdit.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSelectedCheatUpdated(@Nullable Cheat cheat) {
|
||||||
|
clearEditErrors();
|
||||||
|
|
||||||
|
boolean isEditing = mViewModel.getIsEditing().getValue();
|
||||||
|
|
||||||
|
mRoot.setVisibility(isEditing || cheat != null ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
// If the fragment was recreated while editing a cheat, it's vital that we
|
||||||
|
// don't repopulate the fields, otherwise the user's changes will be lost
|
||||||
|
if (!isEditing) {
|
||||||
|
if (cheat == null) {
|
||||||
|
mEditName.setText("");
|
||||||
|
mEditNotes.setText("");
|
||||||
|
mEditCode.setText("");
|
||||||
|
} else {
|
||||||
|
mEditName.setText(cheat.getName());
|
||||||
|
mEditNotes.setText(cheat.getNotes());
|
||||||
|
mEditCode.setText(cheat.getCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onIsEditingUpdated(boolean isEditing) {
|
||||||
|
if (isEditing) {
|
||||||
|
mRoot.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
mEditName.setEnabled(isEditing);
|
||||||
|
mEditNotes.setEnabled(isEditing);
|
||||||
|
mEditCode.setEnabled(isEditing);
|
||||||
|
|
||||||
|
mButtonDelete.setVisibility(isEditing ? View.GONE : View.VISIBLE);
|
||||||
|
mButtonEdit.setVisibility(isEditing ? View.GONE : View.VISIBLE);
|
||||||
|
mButtonCancel.setVisibility(isEditing ? View.VISIBLE : View.GONE);
|
||||||
|
mButtonOk.setVisibility(isEditing ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,193 +0,0 @@
|
||||||
// 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.ui
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.citra.citra_emu.R
|
|
||||||
import org.citra.citra_emu.databinding.FragmentCheatDetailsBinding
|
|
||||||
import org.citra.citra_emu.features.cheats.model.Cheat
|
|
||||||
import org.citra.citra_emu.features.cheats.model.CheatsViewModel
|
|
||||||
|
|
||||||
class CheatDetailsFragment : Fragment() {
|
|
||||||
private val cheatsViewModel: CheatsViewModel by activityViewModels()
|
|
||||||
|
|
||||||
private var _binding: FragmentCheatDetailsBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentCheatDetailsBinding.inflate(layoutInflater)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is using the correct scope, lint is just acting up
|
|
||||||
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
viewLifecycleOwner.lifecycleScope.apply {
|
|
||||||
launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
|
||||||
cheatsViewModel.selectedCheat.collect { onSelectedCheatUpdated(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
|
||||||
cheatsViewModel.isEditing.collect { onIsEditingUpdated(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.buttonDelete.setOnClickListener { onDeleteClicked() }
|
|
||||||
binding.buttonEdit.setOnClickListener { onEditClicked() }
|
|
||||||
binding.buttonCancel.setOnClickListener { onCancelClicked() }
|
|
||||||
binding.buttonOk.setOnClickListener { onOkClicked() }
|
|
||||||
|
|
||||||
// On a portrait phone screen (or other narrow screen), only one of the two panes are shown
|
|
||||||
// at the same time. If the user is navigating using a d-pad and moves focus to an element
|
|
||||||
// in the currently hidden pane, we need to manually show that pane.
|
|
||||||
CheatsActivity.setOnFocusChangeListenerRecursively(view) { _, hasFocus ->
|
|
||||||
cheatsViewModel.onDetailsViewFocusChanged(hasFocus)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.toolbarCheatDetails.setNavigationOnClickListener {
|
|
||||||
cheatsViewModel.closeDetailsView()
|
|
||||||
}
|
|
||||||
|
|
||||||
setInsets()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearEditErrors() {
|
|
||||||
binding.editName.error = null
|
|
||||||
binding.editCode.error = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onDeleteClicked() {
|
|
||||||
val name = binding.editNameInput.text.toString()
|
|
||||||
MaterialAlertDialogBuilder(requireContext())
|
|
||||||
.setMessage(getString(R.string.cheats_delete_confirmation, name))
|
|
||||||
.setPositiveButton(
|
|
||||||
android.R.string.ok
|
|
||||||
) { _: DialogInterface?, _: Int -> cheatsViewModel.deleteSelectedCheat() }
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onEditClicked() {
|
|
||||||
cheatsViewModel.setIsEditing(true)
|
|
||||||
binding.buttonOk.requestFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onCancelClicked() {
|
|
||||||
cheatsViewModel.setIsEditing(false)
|
|
||||||
onSelectedCheatUpdated(cheatsViewModel.selectedCheat.value)
|
|
||||||
binding.buttonDelete.requestFocus()
|
|
||||||
cheatsViewModel.closeDetailsView()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onOkClicked() {
|
|
||||||
clearEditErrors()
|
|
||||||
val name = binding.editNameInput.text.toString()
|
|
||||||
val notes = binding.editNotesInput.text.toString()
|
|
||||||
val code = binding.editCodeInput.text.toString()
|
|
||||||
if (name.isEmpty()) {
|
|
||||||
binding.editName.error = getString(R.string.cheats_error_no_name)
|
|
||||||
binding.scrollView.smoothScrollTo(0, binding.editNameInput.top)
|
|
||||||
return
|
|
||||||
} else if (code.isEmpty()) {
|
|
||||||
binding.editCode.error = getString(R.string.cheats_error_no_code_lines)
|
|
||||||
binding.scrollView.smoothScrollTo(0, binding.editCodeInput.bottom)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val validityResult = Cheat.isValidGatewayCode(code)
|
|
||||||
if (validityResult != 0) {
|
|
||||||
binding.editCode.error = getString(R.string.cheats_error_on_line, validityResult)
|
|
||||||
binding.scrollView.smoothScrollTo(0, binding.editCodeInput.bottom)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val newCheat = Cheat.createGatewayCode(name, notes, code)
|
|
||||||
if (cheatsViewModel.isAdding.value == true) {
|
|
||||||
cheatsViewModel.finishAddingCheat(newCheat)
|
|
||||||
} else {
|
|
||||||
cheatsViewModel.updateSelectedCheat(newCheat)
|
|
||||||
}
|
|
||||||
binding.buttonEdit.requestFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSelectedCheatUpdated(cheat: Cheat?) {
|
|
||||||
clearEditErrors()
|
|
||||||
val isEditing: Boolean = cheatsViewModel.isEditing.value == true
|
|
||||||
|
|
||||||
// If the fragment was recreated while editing a cheat, it's vital that we
|
|
||||||
// don't repopulate the fields, otherwise the user's changes will be lost
|
|
||||||
if (!isEditing) {
|
|
||||||
if (cheat == null) {
|
|
||||||
binding.editNameInput.setText("")
|
|
||||||
binding.editNotesInput.setText("")
|
|
||||||
binding.editCodeInput.setText("")
|
|
||||||
} else {
|
|
||||||
binding.editNameInput.setText(cheat.getName())
|
|
||||||
binding.editNotesInput.setText(cheat.getNotes())
|
|
||||||
binding.editCodeInput.setText(cheat.getCode())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onIsEditingUpdated(isEditing: Boolean) {
|
|
||||||
if (isEditing) {
|
|
||||||
binding.root.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
binding.editNameInput.isEnabled = isEditing
|
|
||||||
binding.editNotesInput.isEnabled = isEditing
|
|
||||||
binding.editCodeInput.isEnabled = isEditing
|
|
||||||
|
|
||||||
binding.buttonDelete.visibility = if (isEditing) View.GONE else View.VISIBLE
|
|
||||||
binding.buttonEdit.visibility = if (isEditing) View.GONE else View.VISIBLE
|
|
||||||
binding.buttonCancel.visibility = if (isEditing) View.VISIBLE else View.GONE
|
|
||||||
binding.buttonOk.visibility = if (isEditing) View.VISIBLE else View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setInsets() =
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(
|
|
||||||
binding.root
|
|
||||||
) { _: View?, windowInsets: WindowInsetsCompat ->
|
|
||||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
|
||||||
|
|
||||||
val leftInsets = barInsets.left + cutoutInsets.left
|
|
||||||
val rightInsets = barInsets.right + cutoutInsets.right
|
|
||||||
|
|
||||||
val mlpAppBar = binding.toolbarCheatDetails.layoutParams as ViewGroup.MarginLayoutParams
|
|
||||||
mlpAppBar.leftMargin = leftInsets
|
|
||||||
mlpAppBar.rightMargin = rightInsets
|
|
||||||
binding.toolbarCheatDetails.layoutParams = mlpAppBar
|
|
||||||
|
|
||||||
binding.scrollView.updatePadding(left = leftInsets, right = rightInsets)
|
|
||||||
binding.buttonContainer.updatePadding(left = leftInsets, right = rightInsets)
|
|
||||||
|
|
||||||
windowInsets
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,71 @@
|
||||||
|
package org.citra.citra_emu.features.cheats.ui;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.graphics.Insets;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||||
|
|
||||||
|
import org.citra.citra_emu.R;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
||||||
|
import org.citra.citra_emu.ui.DividerItemDecoration;
|
||||||
|
|
||||||
|
public class CheatListFragment extends Fragment {
|
||||||
|
private RecyclerView mRecyclerView;
|
||||||
|
private FloatingActionButton mFab;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
|
||||||
|
@Nullable Bundle savedInstanceState) {
|
||||||
|
return inflater.inflate(R.layout.fragment_cheat_list, container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
|
mRecyclerView = view.findViewById(R.id.cheat_list);
|
||||||
|
mFab = view.findViewById(R.id.fab);
|
||||||
|
|
||||||
|
CheatsActivity activity = (CheatsActivity) requireActivity();
|
||||||
|
CheatsViewModel viewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
|
||||||
|
|
||||||
|
mRecyclerView.setAdapter(new CheatsAdapter(activity, viewModel));
|
||||||
|
mRecyclerView.setLayoutManager(new LinearLayoutManager(activity));
|
||||||
|
mRecyclerView.addItemDecoration(new DividerItemDecoration(activity, null));
|
||||||
|
|
||||||
|
mFab.setOnClickListener(v -> {
|
||||||
|
viewModel.startAddingCheat();
|
||||||
|
viewModel.openDetailsView();
|
||||||
|
});
|
||||||
|
|
||||||
|
setInsets();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setInsets() {
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> {
|
||||||
|
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||||
|
v.setPadding(0, 0, 0, insets.bottom + getResources().getDimensionPixelSize(R.dimen.spacing_fab_list));
|
||||||
|
|
||||||
|
ViewGroup.MarginLayoutParams mlpFab =
|
||||||
|
(ViewGroup.MarginLayoutParams) mFab.getLayoutParams();
|
||||||
|
int fabPadding = getResources().getDimensionPixelSize(R.dimen.spacing_large);
|
||||||
|
mlpFab.leftMargin = insets.left + fabPadding;
|
||||||
|
mlpFab.bottomMargin = insets.bottom + fabPadding;
|
||||||
|
mlpFab.rightMargin = insets.right + fabPadding;
|
||||||
|
mFab.setLayoutParams(mlpFab);
|
||||||
|
|
||||||
|
return windowInsets;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,143 +0,0 @@
|
||||||
// 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.ui
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.ViewGroup.MarginLayoutParams
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.updatePadding
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.navigation.findNavController
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.google.android.material.divider.MaterialDividerItemDecoration
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.citra.citra_emu.R
|
|
||||||
import org.citra.citra_emu.databinding.FragmentCheatListBinding
|
|
||||||
import org.citra.citra_emu.features.cheats.model.CheatsViewModel
|
|
||||||
import org.citra.citra_emu.ui.main.MainActivity
|
|
||||||
|
|
||||||
class CheatListFragment : Fragment() {
|
|
||||||
private var _binding: FragmentCheatListBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private val cheatsViewModel: CheatsViewModel by activityViewModels()
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentCheatListBinding.inflate(layoutInflater)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is using the correct scope, lint is just acting up
|
|
||||||
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
binding.cheatList.adapter = CheatsAdapter(requireActivity(), cheatsViewModel)
|
|
||||||
binding.cheatList.layoutManager = LinearLayoutManager(requireContext())
|
|
||||||
binding.cheatList.addItemDecoration(
|
|
||||||
MaterialDividerItemDecoration(
|
|
||||||
requireContext(),
|
|
||||||
MaterialDividerItemDecoration.VERTICAL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.apply {
|
|
||||||
launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
|
||||||
cheatsViewModel.cheatAddedEvent.collect { position: Int? ->
|
|
||||||
position?.let {
|
|
||||||
binding.cheatList.apply {
|
|
||||||
post { (adapter as CheatsAdapter).notifyItemInserted(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
|
||||||
cheatsViewModel.cheatChangedEvent.collect { position: Int? ->
|
|
||||||
position?.let {
|
|
||||||
binding.cheatList.apply {
|
|
||||||
post { (adapter as CheatsAdapter).notifyItemChanged(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
|
||||||
cheatsViewModel.cheatDeletedEvent.collect { position: Int? ->
|
|
||||||
position?.let {
|
|
||||||
binding.cheatList.apply {
|
|
||||||
post { (adapter as CheatsAdapter).notifyItemRemoved(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.fab.setOnClickListener {
|
|
||||||
cheatsViewModel.startAddingCheat()
|
|
||||||
cheatsViewModel.openDetailsView()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.toolbarCheatList.setNavigationOnClickListener {
|
|
||||||
if (requireActivity() is MainActivity) {
|
|
||||||
view.findNavController().popBackStack()
|
|
||||||
} else {
|
|
||||||
requireActivity().finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setInsets()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setInsets() {
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(
|
|
||||||
binding.root
|
|
||||||
) { _: View, windowInsets: WindowInsetsCompat ->
|
|
||||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
|
|
||||||
|
|
||||||
val leftInsets = barInsets.left + cutoutInsets.left
|
|
||||||
val rightInsets = barInsets.right + cutoutInsets.right
|
|
||||||
|
|
||||||
val mlpAppBar = binding.toolbarCheatList.layoutParams as MarginLayoutParams
|
|
||||||
mlpAppBar.leftMargin = leftInsets
|
|
||||||
mlpAppBar.rightMargin = rightInsets
|
|
||||||
binding.toolbarCheatList.layoutParams = mlpAppBar
|
|
||||||
|
|
||||||
binding.cheatList.updatePadding(
|
|
||||||
left = leftInsets,
|
|
||||||
right = rightInsets,
|
|
||||||
bottom = barInsets.bottom +
|
|
||||||
resources.getDimensionPixelSize(R.dimen.spacing_fab_list)
|
|
||||||
)
|
|
||||||
|
|
||||||
val mlpFab = binding.fab.layoutParams as MarginLayoutParams
|
|
||||||
val fabPadding = resources.getDimensionPixelSize(R.dimen.spacing_large)
|
|
||||||
mlpFab.leftMargin = leftInsets + fabPadding
|
|
||||||
mlpFab.bottomMargin = barInsets.bottom + fabPadding
|
|
||||||
mlpFab.rightMargin = rightInsets + fabPadding
|
|
||||||
binding.fab.layoutParams = mlpFab
|
|
||||||
windowInsets
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,56 @@
|
||||||
|
package org.citra.citra_emu.features.cheats.ui;
|
||||||
|
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.CheckBox;
|
||||||
|
import android.widget.CompoundButton;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.citra.citra_emu.R;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.Cheat;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
||||||
|
|
||||||
|
public class CheatViewHolder extends RecyclerView.ViewHolder
|
||||||
|
implements View.OnClickListener, CompoundButton.OnCheckedChangeListener {
|
||||||
|
private final View mRoot;
|
||||||
|
private final TextView mName;
|
||||||
|
private final CheckBox mCheckbox;
|
||||||
|
|
||||||
|
private CheatsViewModel mViewModel;
|
||||||
|
private Cheat mCheat;
|
||||||
|
private int mPosition;
|
||||||
|
|
||||||
|
public CheatViewHolder(@NonNull View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
|
||||||
|
mRoot = itemView.findViewById(R.id.root);
|
||||||
|
mName = itemView.findViewById(R.id.text_name);
|
||||||
|
mCheckbox = itemView.findViewById(R.id.checkbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void bind(CheatsActivity activity, Cheat cheat, int position) {
|
||||||
|
mCheckbox.setOnCheckedChangeListener(null);
|
||||||
|
|
||||||
|
mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
|
||||||
|
mCheat = cheat;
|
||||||
|
mPosition = position;
|
||||||
|
|
||||||
|
mName.setText(mCheat.getName());
|
||||||
|
mCheckbox.setChecked(mCheat.getEnabled());
|
||||||
|
|
||||||
|
mRoot.setOnClickListener(this);
|
||||||
|
mCheckbox.setOnCheckedChangeListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onClick(View root) {
|
||||||
|
mViewModel.setSelectedCheat(mCheat, mPosition);
|
||||||
|
mViewModel.openDetailsView();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
||||||
|
mCheat.setEnabled(isChecked);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,235 @@
|
||||||
|
package org.citra.citra_emu.features.cheats.ui;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.core.graphics.Insets;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.core.view.WindowCompat;
|
||||||
|
import androidx.core.view.WindowInsetsAnimationCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.slidingpanelayout.widget.SlidingPaneLayout;
|
||||||
|
|
||||||
|
import com.google.android.material.appbar.AppBarLayout;
|
||||||
|
import com.google.android.material.appbar.MaterialToolbar;
|
||||||
|
|
||||||
|
import org.citra.citra_emu.R;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.Cheat;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
||||||
|
import org.citra.citra_emu.ui.TwoPaneOnBackPressedCallback;
|
||||||
|
import org.citra.citra_emu.utils.InsetsHelper;
|
||||||
|
import org.citra.citra_emu.utils.ThemeUtil;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class CheatsActivity extends AppCompatActivity
|
||||||
|
implements SlidingPaneLayout.PanelSlideListener {
|
||||||
|
private static String ARG_TITLE_ID = "title_id";
|
||||||
|
|
||||||
|
private CheatsViewModel mViewModel;
|
||||||
|
|
||||||
|
private SlidingPaneLayout mSlidingPaneLayout;
|
||||||
|
private View mCheatList;
|
||||||
|
private View mCheatDetails;
|
||||||
|
|
||||||
|
private View mCheatListLastFocus;
|
||||||
|
private View mCheatDetailsLastFocus;
|
||||||
|
|
||||||
|
public static void launch(Context context, long titleId) {
|
||||||
|
Intent intent = new Intent(context, CheatsActivity.class);
|
||||||
|
intent.putExtra(ARG_TITLE_ID, titleId);
|
||||||
|
context.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
ThemeUtil.INSTANCE.setTheme(this);
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
|
||||||
|
|
||||||
|
long titleId = getIntent().getLongExtra(ARG_TITLE_ID, -1);
|
||||||
|
|
||||||
|
mViewModel = new ViewModelProvider(this).get(CheatsViewModel.class);
|
||||||
|
mViewModel.initialize(titleId);
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_cheats);
|
||||||
|
|
||||||
|
mSlidingPaneLayout = findViewById(R.id.sliding_pane_layout);
|
||||||
|
mCheatList = findViewById(R.id.cheat_list_container);
|
||||||
|
mCheatDetails = findViewById(R.id.cheat_details_container);
|
||||||
|
|
||||||
|
mCheatListLastFocus = mCheatList;
|
||||||
|
mCheatDetailsLastFocus = mCheatDetails;
|
||||||
|
|
||||||
|
mSlidingPaneLayout.addPanelSlideListener(this);
|
||||||
|
|
||||||
|
getOnBackPressedDispatcher().addCallback(this,
|
||||||
|
new TwoPaneOnBackPressedCallback(mSlidingPaneLayout));
|
||||||
|
|
||||||
|
mViewModel.getSelectedCheat().observe(this, this::onSelectedCheatChanged);
|
||||||
|
mViewModel.getIsEditing().observe(this, this::onIsEditingChanged);
|
||||||
|
onSelectedCheatChanged(mViewModel.getSelectedCheat().getValue());
|
||||||
|
|
||||||
|
mViewModel.getOpenDetailsViewEvent().observe(this, this::openDetailsView);
|
||||||
|
|
||||||
|
// Show "Up" button in the action bar for navigation
|
||||||
|
MaterialToolbar toolbar = findViewById(R.id.toolbar_cheats);
|
||||||
|
setSupportActionBar(toolbar);
|
||||||
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
|
||||||
|
setInsets();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
|
MenuInflater inflater = getMenuInflater();
|
||||||
|
inflater.inflate(R.menu.menu_settings, menu);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
|
||||||
|
mViewModel.saveIfNeeded();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPanelSlide(@NonNull View panel, float slideOffset) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPanelOpened(@NonNull View panel) {
|
||||||
|
boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
|
||||||
|
mCheatDetailsLastFocus.requestFocus(rtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPanelClosed(@NonNull View panel) {
|
||||||
|
boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
|
||||||
|
mCheatListLastFocus.requestFocus(rtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onIsEditingChanged(boolean isEditing) {
|
||||||
|
if (isEditing) {
|
||||||
|
mSlidingPaneLayout.setLockMode(SlidingPaneLayout.LOCK_MODE_UNLOCKED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSelectedCheatChanged(Cheat selectedCheat) {
|
||||||
|
boolean cheatSelected = selectedCheat != null || mViewModel.getIsEditing().getValue();
|
||||||
|
|
||||||
|
if (!cheatSelected && mSlidingPaneLayout.isOpen()) {
|
||||||
|
mSlidingPaneLayout.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
mSlidingPaneLayout.setLockMode(cheatSelected ?
|
||||||
|
SlidingPaneLayout.LOCK_MODE_UNLOCKED : SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onListViewFocusChange(boolean hasFocus) {
|
||||||
|
if (hasFocus) {
|
||||||
|
mCheatListLastFocus = mCheatList.findFocus();
|
||||||
|
if (mCheatListLastFocus == null)
|
||||||
|
throw new NullPointerException();
|
||||||
|
|
||||||
|
mSlidingPaneLayout.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDetailsViewFocusChange(boolean hasFocus) {
|
||||||
|
if (hasFocus) {
|
||||||
|
mCheatDetailsLastFocus = mCheatDetails.findFocus();
|
||||||
|
if (mCheatDetailsLastFocus == null)
|
||||||
|
throw new NullPointerException();
|
||||||
|
|
||||||
|
mSlidingPaneLayout.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onSupportNavigateUp() {
|
||||||
|
onBackPressed();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openDetailsView(boolean open) {
|
||||||
|
if (open) {
|
||||||
|
mSlidingPaneLayout.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setOnFocusChangeListenerRecursively(@NonNull View view, View.OnFocusChangeListener listener) {
|
||||||
|
view.setOnFocusChangeListener(listener);
|
||||||
|
|
||||||
|
if (view instanceof ViewGroup) {
|
||||||
|
ViewGroup viewGroup = (ViewGroup) view;
|
||||||
|
for (int i = 0; i < viewGroup.getChildCount(); i++) {
|
||||||
|
View child = viewGroup.getChildAt(i);
|
||||||
|
setOnFocusChangeListenerRecursively(child, listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setInsets() {
|
||||||
|
AppBarLayout appBarLayout = findViewById(R.id.appbar_cheats);
|
||||||
|
ViewCompat.setOnApplyWindowInsetsListener(mSlidingPaneLayout, (v, windowInsets) -> {
|
||||||
|
Insets barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
|
||||||
|
Insets keyboardInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime());
|
||||||
|
|
||||||
|
InsetsHelper.insetAppBar(barInsets, appBarLayout);
|
||||||
|
mSlidingPaneLayout.setPadding(barInsets.left, 0, barInsets.right, 0);
|
||||||
|
|
||||||
|
// Set keyboard insets if the system supports smooth keyboard animations
|
||||||
|
ViewGroup.MarginLayoutParams mlpDetails =
|
||||||
|
(ViewGroup.MarginLayoutParams) mCheatDetails.getLayoutParams();
|
||||||
|
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.R) {
|
||||||
|
if (keyboardInsets.bottom > 0) {
|
||||||
|
mlpDetails.bottomMargin = keyboardInsets.bottom;
|
||||||
|
} else {
|
||||||
|
mlpDetails.bottomMargin = barInsets.bottom;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (mlpDetails.bottomMargin == 0) {
|
||||||
|
mlpDetails.bottomMargin = barInsets.bottom;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mCheatDetails.setLayoutParams(mlpDetails);
|
||||||
|
|
||||||
|
return windowInsets;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update the layout for every frame that the keyboard animates in
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
ViewCompat.setWindowInsetsAnimationCallback(mCheatDetails,
|
||||||
|
new WindowInsetsAnimationCompat.Callback(
|
||||||
|
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP) {
|
||||||
|
int keyboardInsets = 0;
|
||||||
|
int barInsets = 0;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets,
|
||||||
|
@NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
|
||||||
|
ViewGroup.MarginLayoutParams mlpDetails =
|
||||||
|
(ViewGroup.MarginLayoutParams) mCheatDetails.getLayoutParams();
|
||||||
|
keyboardInsets = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
|
||||||
|
barInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
|
||||||
|
mlpDetails.bottomMargin = Math.max(keyboardInsets, barInsets);
|
||||||
|
mCheatDetails.setLayoutParams(mlpDetails);
|
||||||
|
return insets;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,63 +0,0 @@
|
||||||
// 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.ui
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.View.OnFocusChangeListener
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.WindowCompat
|
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
|
||||||
import com.google.android.material.color.MaterialColors
|
|
||||||
import org.citra.citra_emu.R
|
|
||||||
import org.citra.citra_emu.databinding.ActivityCheatsBinding
|
|
||||||
import org.citra.citra_emu.utils.InsetsHelper
|
|
||||||
import org.citra.citra_emu.utils.ThemeUtil
|
|
||||||
|
|
||||||
class CheatsActivity : AppCompatActivity() {
|
|
||||||
private lateinit var binding: ActivityCheatsBinding
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
ThemeUtil.setTheme(this)
|
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
|
|
||||||
binding = ActivityCheatsBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
|
||||||
if (InsetsHelper.getSystemGestureType(applicationContext) !=
|
|
||||||
InsetsHelper.GESTURE_NAVIGATION
|
|
||||||
) {
|
|
||||||
binding.navigationBarShade.setBackgroundColor(
|
|
||||||
ThemeUtil.getColorWithOpacity(
|
|
||||||
MaterialColors.getColor(
|
|
||||||
binding.navigationBarShade,
|
|
||||||
com.google.android.material.R.attr.colorSurface
|
|
||||||
),
|
|
||||||
ThemeUtil.SYSTEM_BAR_ALPHA
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val navHostFragment =
|
|
||||||
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
|
|
||||||
val navController = navHostFragment.navController
|
|
||||||
navController.setGraph(R.navigation.cheats_navigation, intent.extras)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun setOnFocusChangeListenerRecursively(view: View, listener: OnFocusChangeListener?) {
|
|
||||||
view.onFocusChangeListener = listener
|
|
||||||
if (view is ViewGroup) {
|
|
||||||
for (i in 0 until view.childCount) {
|
|
||||||
val child = view.getChildAt(i)
|
|
||||||
setOnFocusChangeListenerRecursively(child, listener)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
package org.citra.citra_emu.features.cheats.ui;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import org.citra.citra_emu.R;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.Cheat;
|
||||||
|
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
|
||||||
|
|
||||||
|
public class CheatsAdapter extends RecyclerView.Adapter<CheatViewHolder> {
|
||||||
|
private final CheatsActivity mActivity;
|
||||||
|
private final CheatsViewModel mViewModel;
|
||||||
|
|
||||||
|
public CheatsAdapter(CheatsActivity activity, CheatsViewModel viewModel) {
|
||||||
|
mActivity = activity;
|
||||||
|
mViewModel = viewModel;
|
||||||
|
|
||||||
|
mViewModel.getCheatAddedEvent().observe(activity, (position) -> {
|
||||||
|
if (position != null) {
|
||||||
|
notifyItemInserted(position);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mViewModel.getCheatUpdatedEvent().observe(activity, (position) -> {
|
||||||
|
if (position != null) {
|
||||||
|
notifyItemChanged(position);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mViewModel.getCheatDeletedEvent().observe(activity, (position) -> {
|
||||||
|
if (position != null) {
|
||||||
|
notifyItemRemoved(position);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public CheatViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
|
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||||
|
|
||||||
|
View cheatView = inflater.inflate(R.layout.list_item_cheat, parent, false);
|
||||||
|
addViewListeners(cheatView);
|
||||||
|
return new CheatViewHolder(cheatView);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull CheatViewHolder holder, int position) {
|
||||||
|
holder.bind(mActivity, getItemAt(position), position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return mViewModel.getCheats().length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addViewListeners(View view) {
|
||||||
|
// On a portrait phone screen (or other narrow screen), only one of the two panes are shown
|
||||||
|
// at the same time. If the user is navigating using a d-pad and moves focus to an element
|
||||||
|
// in the currently hidden pane, we need to manually show that pane.
|
||||||
|
CheatsActivity.setOnFocusChangeListenerRecursively(view,
|
||||||
|
(v, hasFocus) -> mActivity.onListViewFocusChange(hasFocus));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Cheat getItemAt(int position) {
|
||||||
|
return mViewModel.getCheats()[position];
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,69 +0,0 @@
|
||||||
// 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.ui
|
|
||||||
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.CompoundButton
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.lifecycle.ViewModelProvider
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import org.citra.citra_emu.databinding.ListItemCheatBinding
|
|
||||||
import org.citra.citra_emu.features.cheats.model.Cheat
|
|
||||||
import org.citra.citra_emu.features.cheats.model.CheatsViewModel
|
|
||||||
|
|
||||||
class CheatsAdapter(
|
|
||||||
private val activity: FragmentActivity,
|
|
||||||
private val viewModel: CheatsViewModel
|
|
||||||
) : RecyclerView.Adapter<CheatsAdapter.CheatViewHolder>() {
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheatViewHolder {
|
|
||||||
val binding =
|
|
||||||
ListItemCheatBinding.inflate(LayoutInflater.from(parent.context), parent, false)
|
|
||||||
addViewListeners(binding.root)
|
|
||||||
return CheatViewHolder(binding)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: CheatViewHolder, position: Int) =
|
|
||||||
holder.bind(activity, viewModel.cheats[position], position)
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = viewModel.cheats.size
|
|
||||||
|
|
||||||
private fun addViewListeners(view: View) {
|
|
||||||
// On a portrait phone screen (or other narrow screen), only one of the two panes are shown
|
|
||||||
// at the same time. If the user is navigating using a d-pad and moves focus to an element
|
|
||||||
// in the currently hidden pane, we need to manually show that pane.
|
|
||||||
CheatsActivity.setOnFocusChangeListenerRecursively(view) { _, hasFocus ->
|
|
||||||
viewModel.onListViewFocusChanged(hasFocus)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class CheatViewHolder(private val binding: ListItemCheatBinding) :
|
|
||||||
RecyclerView.ViewHolder(binding.root), View.OnClickListener,
|
|
||||||
CompoundButton.OnCheckedChangeListener {
|
|
||||||
private lateinit var viewModel: CheatsViewModel
|
|
||||||
private lateinit var cheat: Cheat
|
|
||||||
private var position = 0
|
|
||||||
|
|
||||||
fun bind(activity: FragmentActivity, cheat: Cheat, position: Int) {
|
|
||||||
viewModel = ViewModelProvider(activity)[CheatsViewModel::class.java]
|
|
||||||
this.cheat = cheat
|
|
||||||
this.position = position
|
|
||||||
binding.textName.text = this.cheat.getName()
|
|
||||||
binding.cheatSwitch.isChecked = this.cheat.getEnabled()
|
|
||||||
binding.cheatContainer.setOnClickListener(this)
|
|
||||||
binding.cheatSwitch.setOnCheckedChangeListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(root: View) {
|
|
||||||
viewModel.setSelectedCheat(cheat, position)
|
|
||||||
viewModel.openDetailsView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
|
|
||||||
cheat.setEnabled(isChecked)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,244 +0,0 @@
|
||||||
// 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.ui
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsAnimationCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.navigation.findNavController
|
|
||||||
import androidx.navigation.fragment.navArgs
|
|
||||||
import androidx.slidingpanelayout.widget.SlidingPaneLayout
|
|
||||||
import com.google.android.material.transition.MaterialSharedAxis
|
|
||||||
import kotlinx.coroutines.flow.collect
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.citra.citra_emu.databinding.FragmentCheatsBinding
|
|
||||||
import org.citra.citra_emu.features.cheats.model.Cheat
|
|
||||||
import org.citra.citra_emu.features.cheats.model.CheatsViewModel
|
|
||||||
import org.citra.citra_emu.ui.TwoPaneOnBackPressedCallback
|
|
||||||
import org.citra.citra_emu.ui.main.MainActivity
|
|
||||||
import org.citra.citra_emu.viewmodel.HomeViewModel
|
|
||||||
|
|
||||||
class CheatsFragment : Fragment(), SlidingPaneLayout.PanelSlideListener {
|
|
||||||
private var cheatListLastFocus: View? = null
|
|
||||||
private var cheatDetailsLastFocus: View? = null
|
|
||||||
|
|
||||||
private var _binding: FragmentCheatsBinding? = null
|
|
||||||
private val binding get() = _binding!!
|
|
||||||
|
|
||||||
private val cheatsViewModel: CheatsViewModel by activityViewModels()
|
|
||||||
private val homeViewModel: HomeViewModel by activityViewModels()
|
|
||||||
|
|
||||||
private val args by navArgs<CheatsFragmentArgs>()
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
|
|
||||||
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
_binding = FragmentCheatsBinding.inflate(inflater)
|
|
||||||
return binding.root
|
|
||||||
}
|
|
||||||
|
|
||||||
// This is using the correct scope, lint is just acting up
|
|
||||||
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
|
|
||||||
homeViewModel.setNavigationVisibility(visible = false, animated = true)
|
|
||||||
homeViewModel.setStatusBarShadeVisibility(visible = false)
|
|
||||||
|
|
||||||
cheatsViewModel.initialize(args.titleId)
|
|
||||||
|
|
||||||
cheatListLastFocus = binding.cheatListContainer
|
|
||||||
cheatDetailsLastFocus = binding.cheatDetailsContainer
|
|
||||||
binding.slidingPaneLayout.addPanelSlideListener(this)
|
|
||||||
requireActivity().onBackPressedDispatcher.addCallback(
|
|
||||||
viewLifecycleOwner,
|
|
||||||
TwoPaneOnBackPressedCallback(binding.slidingPaneLayout)
|
|
||||||
)
|
|
||||||
requireActivity().onBackPressedDispatcher.addCallback(
|
|
||||||
viewLifecycleOwner,
|
|
||||||
object : OnBackPressedCallback(true) {
|
|
||||||
override fun handleOnBackPressed() {
|
|
||||||
if (binding.slidingPaneLayout.isOpen) {
|
|
||||||
binding.slidingPaneLayout.close()
|
|
||||||
} else {
|
|
||||||
if (requireActivity() is MainActivity) {
|
|
||||||
view.findNavController().popBackStack()
|
|
||||||
} else {
|
|
||||||
requireActivity().finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
viewLifecycleOwner.lifecycleScope.apply {
|
|
||||||
launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
|
||||||
cheatsViewModel.selectedCheat.collect { onSelectedCheatChanged(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
|
||||||
cheatsViewModel.isEditing.collect { onIsEditingChanged(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
|
||||||
cheatsViewModel.openDetailsViewEvent.collect { openDetailsView(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
|
||||||
cheatsViewModel.closeDetailsViewEvent.collect { closeDetailsView(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
|
||||||
cheatsViewModel.listViewFocusChange.collect { onListViewFocusChange(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.CREATED) {
|
|
||||||
cheatsViewModel.detailsViewFocusChange.collect { onDetailsViewFocusChange(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setInsets()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStop() {
|
|
||||||
super.onStop()
|
|
||||||
cheatsViewModel.saveIfNeeded()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
_binding = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPanelSlide(panel: View, slideOffset: Float) {}
|
|
||||||
override fun onPanelOpened(panel: View) {
|
|
||||||
val rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL
|
|
||||||
cheatDetailsLastFocus!!.requestFocus(if (rtl) View.FOCUS_LEFT else View.FOCUS_RIGHT)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPanelClosed(panel: View) {
|
|
||||||
val rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL
|
|
||||||
cheatListLastFocus!!.requestFocus(if (rtl) View.FOCUS_RIGHT else View.FOCUS_LEFT)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onIsEditingChanged(isEditing: Boolean) {
|
|
||||||
if (isEditing) {
|
|
||||||
binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_UNLOCKED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSelectedCheatChanged(selectedCheat: Cheat?) {
|
|
||||||
val cheatSelected = selectedCheat != null || cheatsViewModel.isEditing.value!!
|
|
||||||
if (!cheatSelected && binding.slidingPaneLayout.isOpen) {
|
|
||||||
binding.slidingPaneLayout.close()
|
|
||||||
}
|
|
||||||
binding.slidingPaneLayout.lockMode =
|
|
||||||
if (cheatSelected) SlidingPaneLayout.LOCK_MODE_UNLOCKED else SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onListViewFocusChange(hasFocus: Boolean) {
|
|
||||||
if (hasFocus) {
|
|
||||||
cheatListLastFocus = binding.cheatListContainer.findFocus()
|
|
||||||
if (cheatListLastFocus == null) throw NullPointerException()
|
|
||||||
binding.slidingPaneLayout.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onDetailsViewFocusChange(hasFocus: Boolean) {
|
|
||||||
if (hasFocus) {
|
|
||||||
cheatDetailsLastFocus = binding.cheatDetailsContainer.findFocus()
|
|
||||||
if (cheatDetailsLastFocus == null) {
|
|
||||||
throw NullPointerException()
|
|
||||||
}
|
|
||||||
binding.slidingPaneLayout.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openDetailsView(open: Boolean) {
|
|
||||||
if (open) {
|
|
||||||
binding.slidingPaneLayout.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun closeDetailsView(close: Boolean) {
|
|
||||||
if (close) {
|
|
||||||
binding.slidingPaneLayout.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setInsets() {
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(
|
|
||||||
binding.slidingPaneLayout
|
|
||||||
) { _: View?, windowInsets: WindowInsetsCompat ->
|
|
||||||
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
val keyboardInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
|
|
||||||
|
|
||||||
// Set keyboard insets if the system supports smooth keyboard animations
|
|
||||||
val mlpDetails = binding.cheatDetailsContainer.layoutParams as ViewGroup.MarginLayoutParams
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
|
||||||
if (keyboardInsets.bottom > 0) {
|
|
||||||
mlpDetails.bottomMargin = keyboardInsets.bottom
|
|
||||||
} else {
|
|
||||||
mlpDetails.bottomMargin = barInsets.bottom
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (mlpDetails.bottomMargin == 0) {
|
|
||||||
mlpDetails.bottomMargin = barInsets.bottom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.cheatDetailsContainer.layoutParams = mlpDetails
|
|
||||||
windowInsets
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the layout for every frame that the keyboard animates in
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
ViewCompat.setWindowInsetsAnimationCallback(
|
|
||||||
binding.cheatDetailsContainer,
|
|
||||||
object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
|
|
||||||
var keyboardInsets = 0
|
|
||||||
var barInsets = 0
|
|
||||||
override fun onProgress(
|
|
||||||
insets: WindowInsetsCompat,
|
|
||||||
runningAnimations: List<WindowInsetsAnimationCompat>
|
|
||||||
): WindowInsetsCompat {
|
|
||||||
val mlpDetails =
|
|
||||||
binding.cheatDetailsContainer.layoutParams as ViewGroup.MarginLayoutParams
|
|
||||||
keyboardInsets = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
|
|
||||||
barInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
|
|
||||||
mlpDetails.bottomMargin = keyboardInsets.coerceAtLeast(barInsets)
|
|
||||||
binding.cheatDetailsContainer.layoutParams = mlpDetails
|
|
||||||
return insets
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
// 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.hotkeys
|
|
||||||
|
|
||||||
enum class Hotkey(val button: Int) {
|
|
||||||
SWAP_SCREEN(10001),
|
|
||||||
CYCLE_LAYOUT(10002),
|
|
||||||
CLOSE_GAME(10003),
|
|
||||||
PAUSE_OR_RESUME(10004);
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
// 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.hotkeys
|
|
||||||
|
|
||||||
import org.citra.citra_emu.utils.EmulationLifecycleUtil
|
|
||||||
import org.citra.citra_emu.display.ScreenAdjustmentUtil
|
|
||||||
|
|
||||||
class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil) {
|
|
||||||
|
|
||||||
val hotkeyButtons = Hotkey.entries.map { it.button }
|
|
||||||
|
|
||||||
fun handleHotkey(bindedButton: Int): Boolean {
|
|
||||||
if(hotkeyButtons.contains(bindedButton)) {
|
|
||||||
when (bindedButton) {
|
|
||||||
Hotkey.SWAP_SCREEN.button -> screenAdjustmentUtil.swapScreen()
|
|
||||||
Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts()
|
|
||||||
Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame()
|
|
||||||
Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume()
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
// 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.settings.model
|
|
||||||
|
|
||||||
interface AbstractBooleanSetting : AbstractSetting {
|
|
||||||
var boolean: Boolean
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
// 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.settings.model
|
|
||||||
|
|
||||||
interface AbstractFloatSetting : AbstractSetting {
|
|
||||||
var float: Float
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
// 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.settings.model
|
|
||||||
|
|
||||||
interface AbstractIntSetting : AbstractSetting {
|
|
||||||
var int: Int
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
// 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.settings.model
|
|
||||||
|
|
||||||
interface AbstractSetting {
|
|
||||||
val key: String?
|
|
||||||
val section: String?
|
|
||||||
val isRuntimeEditable: Boolean
|
|
||||||
val valueAsString: String
|
|
||||||
val defaultValue: Any
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
// 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.settings.model
|
|
||||||
|
|
||||||
interface AbstractShortSetting : AbstractSetting {
|
|
||||||
var short: Short
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
// 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.settings.model
|
|
||||||
|
|
||||||
interface AbstractStringSetting : AbstractSetting {
|
|
||||||
var string: String
|
|
||||||
}
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package org.citra.citra_emu.features.settings.model;
|
||||||
|
|
||||||
|
public final class BooleanSetting extends Setting {
|
||||||
|
private boolean mValue;
|
||||||
|
|
||||||
|
public BooleanSetting(String key, String section, boolean value) {
|
||||||
|
super(key, section);
|
||||||
|
mValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean getValue() {
|
||||||
|
return mValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValue(boolean value) {
|
||||||
|
mValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getValueAsString() {
|
||||||
|
return mValue ? "True" : "False";
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,44 +0,0 @@
|
||||||
// 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.settings.model
|
|
||||||
|
|
||||||
enum class BooleanSetting(
|
|
||||||
override val key: String,
|
|
||||||
override val section: String,
|
|
||||||
override val defaultValue: Boolean
|
|
||||||
) : AbstractBooleanSetting {
|
|
||||||
SPIRV_SHADER_GEN("spirv_shader_gen", Settings.SECTION_RENDERER, true),
|
|
||||||
ASYNC_SHADERS("async_shader_compilation", Settings.SECTION_RENDERER, false),
|
|
||||||
PLUGIN_LOADER("plugin_loader", Settings.SECTION_SYSTEM, false),
|
|
||||||
ALLOW_PLUGIN_LOADER("allow_plugin_loader", Settings.SECTION_SYSTEM, true),
|
|
||||||
SWAP_SCREEN("swap_screen", Settings.SECTION_LAYOUT, false);
|
|
||||||
|
|
||||||
override var boolean: Boolean = defaultValue
|
|
||||||
|
|
||||||
override val valueAsString: String
|
|
||||||
get() = boolean.toString()
|
|
||||||
|
|
||||||
override val isRuntimeEditable: Boolean
|
|
||||||
get() {
|
|
||||||
for (setting in NOT_RUNTIME_EDITABLE) {
|
|
||||||
if (setting == this) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private val NOT_RUNTIME_EDITABLE = listOf(
|
|
||||||
PLUGIN_LOADER,
|
|
||||||
ALLOW_PLUGIN_LOADER
|
|
||||||
)
|
|
||||||
|
|
||||||
fun from(key: String): BooleanSetting? =
|
|
||||||
BooleanSetting.values().firstOrNull { it.key == key }
|
|
||||||
|
|
||||||
fun clear() = BooleanSetting.values().forEach { it.boolean = it.defaultValue }
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package org.citra.citra_emu.features.settings.model;
|
||||||
|
|
||||||
|
public final class FloatSetting extends Setting {
|
||||||
|
private float mValue;
|
||||||
|
|
||||||
|
public FloatSetting(String key, String section, float value) {
|
||||||
|
super(key, section);
|
||||||
|
mValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getValue() {
|
||||||
|
return mValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValue(float value) {
|
||||||
|
mValue = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getValueAsString() {
|
||||||
|
return Float.toString(mValue);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue