mirror of
https://github.com/stenzek/duckstation.git
synced 2026-02-07 14:54:32 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e9413f6d94 |
110
.clang-format
110
.clang-format
@@ -10,115 +10,7 @@ AlignTrailingComments: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortBlocksOnASingleLine: false
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: InlineOnly
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: true
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
BraceWrapping:
|
||||
AfterCaseLabel: true
|
||||
AfterClass: true
|
||||
AfterControlStatement: true
|
||||
AfterEnum: true
|
||||
AfterFunction: true
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: true
|
||||
AfterStruct: true
|
||||
AfterUnion: true
|
||||
BeforeCatch: true
|
||||
BeforeElse: true
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: true
|
||||
SplitEmptyRecord: true
|
||||
SplitEmptyNamespace: true
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeBraces: Custom
|
||||
BreakBeforeInheritanceComma: false
|
||||
BreakBeforeTernaryOperators: false
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
BreakConstructorInitializers: BeforeColon
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakStringLiterals: true
|
||||
BreakAfterAttributes: Leave
|
||||
ColumnLimit: 120
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
ConstructorInitializerIndentWidth: 2
|
||||
ContinuationIndentWidth: 2
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros:
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IncludeCategories:
|
||||
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
|
||||
Priority: 2
|
||||
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
|
||||
Priority: 3
|
||||
- Regex: '.*'
|
||||
Priority: 1
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IndentCaseLabels: true
|
||||
IndentWidth: 2
|
||||
IndentWrappedFunctionNames: false
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBlockIndentWidth: 2
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakAssignment: 2
|
||||
PenaltyBreakBeforeFirstCallParameter: 19
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 60
|
||||
PointerAlignment: Left
|
||||
ReflowComments: true
|
||||
SortIncludes: true
|
||||
SortUsingDeclarations: true
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterTemplateKeyword: false
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: false
|
||||
SpacesInContainerLiterals: true
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
Standard: Cpp11
|
||||
TabWidth: 2
|
||||
UseTab: Never
|
||||
...
|
||||
---
|
||||
Language: ObjC
|
||||
AccessModifierOffset: -2
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignEscapedNewlines: Right
|
||||
AlignOperands: true
|
||||
AlignTrailingComments: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortBlocksOnASingleLine: false
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: InlineOnly
|
||||
AllowShortFunctionsOnASingleLine: Inline
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
|
||||
55
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
55
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Please read before creating a new issue:
|
||||
|
||||
1. Make sure there is not already an open issue for this bug.
|
||||
2. All enhancements **must** be off. To quickly disable all enhancements with affecting your normal config, in advanced options you can check "Disable All Enhancements".
|
||||
3. All advanced options **must** be at their default values.
|
||||
4. No cheats may be active. If you were using cheats, they should be disabled, and the game rebooted before reporting the bug.
|
||||
5. Do not share save state files. Memory cards are okay.
|
||||
6. Verify your dump, as we can not assist with issues resulting from bad dumps.
|
||||
7. If playing PAL region software, please check whether or not the game has LibCrypt protection and that if it does, you have a correct SBI file (see https://github.com/stenzek/duckstation#libcrypt-protection-and-sbi-files for more information).
|
||||
8. Please post your issue report in English as unfortunately this is the only language spoken by the developers. The Discord server has many helpful people if you need help translating.
|
||||
|
||||
**Remove everything before and including this line before submitting.**
|
||||
|
||||
**Game details**
|
||||
[Serial Code, Region]
|
||||
[i.e SLUS-00404 Ace Combat 2 (USA)]
|
||||
|
||||
**Description of the issue / bug**
|
||||
[Describe what you are seeing and/or hearing during gameplay]
|
||||
|
||||
**Controller Troubleshoot Report**
|
||||
1. Have you installed any drivers or wrappers on your system, or do you have any programs like Steam open?
|
||||
2. Which controller backend are you using in general settings
|
||||
|
||||
**Note:**
|
||||
If you are using Duckstation on Android device please consider the following before report:
|
||||
Are you using any kind of "memory optimizer" program such as: Ccleaner, Wisecleaner, Clean Master, Boost Android etc. ?
|
||||
(if so, please, consider create an exception on it first and test; or even remove / uninstall it retest and then, if the problem persists continue with the report.
|
||||
|
||||
**Steps to reproduce**
|
||||
[Try to provide as much detail as possible to reproduce the issue]
|
||||
|
||||
**Enhancements information**
|
||||
[Make sure they are all turned off before report]
|
||||
|
||||
**Hardware/software**
|
||||
[If Android, which phone and Android version]
|
||||
[If desktop, your CPU, graphics card, and operating system]
|
||||
[GPU Renderer - D3D11/OpenGL/Vulkan]
|
||||
|
||||
**Emulator version**
|
||||
[Shown in the title bar of the emulator]
|
||||
|
||||
**Additional context**
|
||||
[Add any other context about the problem here]
|
||||
|
||||
94
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
94
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,94 +0,0 @@
|
||||
name: Bug report
|
||||
description: Report a bug in DuckStation
|
||||
body:
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Please read before creating a new bug report:**
|
||||
1. Make sure there is not already an issue for this bug by [searching in **both** open and closed issues](https://github.com/stenzek/duckstation/issues?q=is%3Aissue+sort%3Aupdated-desc+).
|
||||
2. All enhancements **must** be off. To quickly disable all enhancements with affecting your normal configuration, you can check **Disable All Enhancements** in Advanced Options.
|
||||
3. All advanced options **must** be at their default values.
|
||||
4. No cheats may be active. If you were using cheats, they should be disabled, and the game must be rebooted before reporting the bug.
|
||||
5. Do not share save state files. (Sharing memory card files is OK.)
|
||||
6. Verify your BIOS and game dumps, as we can not assist with issues resulting from bad dumps.
|
||||
7. If playing PAL region software, please check whether or not the game has LibCrypt protection. If it does, make sure you have a [correct SBI file](https://github.com/stenzek/duckstation#libcrypt-protection-and-sbi-files).
|
||||
8. Please post your bug report in English, as this is the only language spoken by the developers. The [Discord server](https://discord.gg/Buktv3t) has many helpful people if you need help translating.
|
||||
9. Issues about the libretro core will be deleted (you will be blocked from the repository if you create such an issue). That core is not DuckStation, it is a broken fork, and has nothing to do with us.
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Game details
|
||||
description: |
|
||||
Specify the game's serial code, full name and region (USA, Europe, Japan).
|
||||
placeholder: SLUS-00404 Ace Combat 2 (USA)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Description of the issue/bug
|
||||
description: |
|
||||
Describe what you are seeing and/or hearing during gameplay. What doesn't work, and how do you expect it to work instead?
|
||||
You can include images or videos with drag and drop, and format code blocks or logs with <code>```</code> tags.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to reproduce
|
||||
description: |
|
||||
Try to provide as much detail as possible to reproduce the issue.
|
||||
Having reproducible issues is a *prerequisite* for contributors to be able to solve them.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: Software and hardware information
|
||||
description: |
|
||||
For desktops and laptops, specify your OS version, CPU and graphics card information (model and driver version).
|
||||
For mobile devices, specify your OS version and device model name.
|
||||
placeholder: Windows 10, Intel Core i7-7500U, Intel HD Graphics 620 (27.20.100.9616)
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: DuckStation version
|
||||
description: |
|
||||
Specify your DuckStation version and how you installed it (GitHub Releases, GitHub Actions, compiled from source, …).
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
attributes:
|
||||
label: DuckStation rendering backend
|
||||
description: |
|
||||
Specify the DuckStation rendering backend you were using when reporting this issue.
|
||||
If you can reproduce this issue using more than one rendering backend, mention it in the **Description of the issue/bug** section above.
|
||||
When reporting a graphics-related issue, please test all the rendering backends you can before submitting the issue.
|
||||
options:
|
||||
- Software
|
||||
- Direct3D 11
|
||||
- Direct3D 12
|
||||
- OpenGL
|
||||
- Vulkan
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
attributes:
|
||||
label: DuckStation controller backend, drivers and wrappers
|
||||
description: |
|
||||
Which controller backend are you using in DuckStation's General Settings?
|
||||
Have you installed any drivers or wrappers on your system, or do you have any programs like Steam open?
|
||||
If so, specify which drivers/wrappers you are using.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: |
|
||||
Add any other context about the problem here.
|
||||
10
.github/ISSUE_TEMPLATE/config.yml
vendored
10
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,10 +0,0 @@
|
||||
blank_issues_enabled: false
|
||||
|
||||
contact_links:
|
||||
- name: Game compatibility list
|
||||
url: https://docs.google.com/spreadsheets/d/1H66MxViRjjE5f8hOl5RQmF5woS1murio2dsLn14kEqo/edit
|
||||
about: Please refer to the game compatibility list before opening an issue.
|
||||
|
||||
- name: Discord server
|
||||
- url: https://discord.gg/Buktv3t
|
||||
- about: Please ask support questions on the Discord server, not here.
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
43
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
43
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,43 +0,0 @@
|
||||
name: Feature request
|
||||
description: Request a feature to be added or improved in DuckStation
|
||||
body:
|
||||
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
**Please read before creating a new feature request:**
|
||||
1. Make sure there is not already an issue for this feature request by [searching in **both** open and closed issues](https://github.com/stenzek/duckstation/issues?q=is%3Aissue+sort%3Aupdated-desc+).
|
||||
2. Please open **one issue per requested feature**. Do not cram several unrelated feature requests in a single issue, as this makes it harder for contributors to track what's being worked on.
|
||||
3. Please post your feature request in English, as this is the only language spoken by the developers. The [Discord server](https://discord.gg/Buktv3t) has many helpful people if you need help translating.
|
||||
4. Issues about the libretro core will be deleted (you will be blocked from the repository if you create such an issue). That core is not DuckStation, it is a broken fork, and has nothing to do with us.
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Problem statement
|
||||
description: |
|
||||
Is your feature request related to a problem? Please describe.
|
||||
placeholder: Example - "I'm always frustrated when […]"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Proposed solution
|
||||
description: |
|
||||
A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Alternatives considered
|
||||
description: |
|
||||
Describe alternatives you've considered.
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: |
|
||||
Add any other context about the problem or proposed feature here.
|
||||
38
.github/workflows/gamedb-lint.yml
vendored
38
.github/workflows/gamedb-lint.yml
vendored
@@ -1,38 +0,0 @@
|
||||
name: GameDB Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'data/resources/gamedb.yaml'
|
||||
- 'data/resources/discdb.yaml'
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- dev
|
||||
paths:
|
||||
- 'data/resources/gamedb.yaml'
|
||||
- 'data/resources/discdb.yaml'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
gamedb-lint:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install Packages
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install yamllint
|
||||
|
||||
- name: Check GameDB
|
||||
shell: bash
|
||||
run: yamllint -c extras/yamllint-config.yaml -s -f github data/resources/gamedb.yaml
|
||||
|
||||
- name: Check DiscDB
|
||||
shell: bash
|
||||
run: yamllint -c extras/yamllint-config.yaml -s -f github data/resources/discdb.yaml
|
||||
410
.github/workflows/rolling-release.yml
vendored
410
.github/workflows/rolling-release.yml
vendored
@@ -20,33 +20,12 @@ on:
|
||||
|
||||
jobs:
|
||||
windows-build:
|
||||
runs-on: windows-2022
|
||||
timeout-minutes: 120
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.6
|
||||
- uses: actions/checkout@v2.3.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Cache Dependencies
|
||||
id: cache-deps
|
||||
uses: actions/cache@v4.0.2
|
||||
with:
|
||||
path: |
|
||||
dep/msvc/deps-arm64
|
||||
dep/msvc/deps-x64
|
||||
key: deps ${{ hashFiles('scripts/build-dependencies-windows-arm64.bat', 'scripts/build-dependencies-windows-x64.bat') }}
|
||||
|
||||
- name: Build X64 Dependencies
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
env:
|
||||
DEBUG: 0
|
||||
run: scripts/build-dependencies-windows-x64.bat
|
||||
|
||||
- name: Build ARM64 Dependencies
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
env:
|
||||
DEBUG: 0
|
||||
run: scripts/build-dependencies-windows-arm64.bat
|
||||
submodules: true
|
||||
|
||||
- name: Tag as preview build
|
||||
if: github.ref == 'refs/heads/master'
|
||||
@@ -67,29 +46,33 @@ jobs:
|
||||
echo #define SCM_RELEASE_TAGS {"latest", "preview"} >> src/scmversion/tag.h
|
||||
echo #define SCM_RELEASE_TAG "latest" >> src/scmversion/tag.h
|
||||
|
||||
- name: Update RC version fields
|
||||
shell: cmd
|
||||
run: |
|
||||
cd src\scmversion
|
||||
call update_rc_version.bat
|
||||
cd ..\..
|
||||
git update-index --assume-unchanged src/duckstation-qt/duckstation-qt.rc
|
||||
|
||||
- name: Compile x64 release build
|
||||
shell: cmd
|
||||
run: |
|
||||
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64
|
||||
msbuild duckstation.sln -t:Build -p:Platform=x64;Configuration=ReleaseLTCG-Clang
|
||||
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64
|
||||
msbuild duckstation.sln -t:Build -p:Platform=x64;Configuration=ReleaseLTCG
|
||||
|
||||
- name: Create x64 symbols archive
|
||||
shell: cmd
|
||||
run: |
|
||||
"C:\Program Files\7-Zip\7z.exe" a -r duckstation-windows-x64-release-symbols.zip ./bin/x64/*.pdb
|
||||
|
||||
- name: Upload x64 release symbols artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: "windows"
|
||||
path: "duckstation-windows-x64-release-symbols.zip"
|
||||
|
||||
- name: Remove extra bloat before archiving
|
||||
shell: cmd
|
||||
run: |
|
||||
del /Q bin\x64\*.pdb bin\x64\*.exp bin\x64\*.lib bin\x64\*.iobj bin\x64\*.ipdb bin\x64\common-tests*
|
||||
del /Q bin\x64\*.pdb
|
||||
del /Q bin\x64\*.exp
|
||||
del /Q bin\x64\*.lib
|
||||
del /Q bin\x64\*.iobj
|
||||
del /Q bin\x64\*.ipdb
|
||||
del /Q bin\x64\common-tests*
|
||||
rename bin\x64\updater-x64-ReleaseLTCG.exe updater.exe
|
||||
|
||||
- name: Create x64 release archive
|
||||
@@ -98,311 +81,243 @@ jobs:
|
||||
"C:\Program Files\7-Zip\7z.exe" a -r duckstation-windows-x64-release.zip ./bin/x64/*
|
||||
|
||||
- name: Upload x64 release artifact
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: "windows"
|
||||
path: "duckstation-windows-x64-release*.zip"
|
||||
path: "duckstation-windows-x64-release.zip"
|
||||
|
||||
|
||||
windows-arm64-build:
|
||||
runs-on: windows-2022
|
||||
timeout-minutes: 120
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.6
|
||||
- uses: actions/checkout@v2.3.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: Cache Dependencies
|
||||
id: cache-deps
|
||||
uses: actions/cache@v4.0.2
|
||||
with:
|
||||
path: |
|
||||
dep/msvc/deps-arm64
|
||||
dep/msvc/deps-x64
|
||||
key: deps ${{ hashFiles('scripts/build-dependencies-windows-arm64.bat', 'scripts/build-dependencies-windows-x64.bat') }}
|
||||
|
||||
- name: Build X64 Dependencies
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
env:
|
||||
DEBUG: 0
|
||||
run: scripts/build-dependencies-windows-x64.bat
|
||||
|
||||
- name: Build ARM64 Dependencies
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
env:
|
||||
DEBUG: 0
|
||||
run: scripts/build-dependencies-windows-arm64.bat
|
||||
|
||||
- name: Tag as preview build
|
||||
if: github.ref == 'refs/heads/master'
|
||||
shell: cmd
|
||||
run: |
|
||||
echo #pragma once > src/scmversion/tag.h
|
||||
echo #define SCM_RELEASE_ASSET "duckstation-windows-arm64-release.zip" >> src/scmversion/tag.h
|
||||
echo #define SCM_RELEASE_ASSET "duckstation-windows-x64-release.zip" >> src/scmversion/tag.h
|
||||
echo #define SCM_RELEASE_TAGS {"latest", "preview"} >> src/scmversion/tag.h
|
||||
echo #define SCM_RELEASE_TAG "preview" >> src/scmversion/tag.h
|
||||
|
||||
|
||||
- name: Tag as dev build
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
shell: cmd
|
||||
run: |
|
||||
echo #pragma once > src/scmversion/tag.h
|
||||
echo #define SCM_RELEASE_ASSET "duckstation-windows-arm64-release.zip" >> src/scmversion/tag.h
|
||||
echo #define SCM_RELEASE_ASSET "duckstation-windows-x64-release.zip" >> src/scmversion/tag.h
|
||||
echo #define SCM_RELEASE_TAGS {"latest", "preview"} >> src/scmversion/tag.h
|
||||
echo #define SCM_RELEASE_TAG "latest" >> src/scmversion/tag.h
|
||||
|
||||
- name: Update RC version fields
|
||||
shell: cmd
|
||||
run: |
|
||||
cd src\scmversion
|
||||
call update_rc_version.bat
|
||||
cd ..\..
|
||||
git update-index --assume-unchanged src/duckstation-qt/duckstation-qt.rc
|
||||
|
||||
- name: Compile arm64 release build
|
||||
shell: cmd
|
||||
run: |
|
||||
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" amd64_arm64
|
||||
msbuild duckstation.sln -t:Build -p:Platform=ARM64;Configuration=ReleaseLTCG-Clang
|
||||
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" amd64_arm64
|
||||
msbuild duckstation.sln -t:Build -p:Platform=ARM64;Configuration=ReleaseLTCG
|
||||
|
||||
- name: Create arm64 symbols archive
|
||||
shell: cmd
|
||||
run: |
|
||||
"C:\Program Files\7-Zip\7z.exe" a -r duckstation-windows-arm64-release-symbols.zip ./bin/ARM64/*.pdb
|
||||
|
||||
- name: Upload arm64 release symbols artifact
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: "windows-arm64"
|
||||
path: "duckstation-windows-arm64-release-symbols.zip"
|
||||
|
||||
- name: Remove extra bloat before archiving
|
||||
shell: cmd
|
||||
run: |
|
||||
del /Q bin\ARM64\*.pdb bin\ARM64\*.exp bin\ARM64\*.lib bin\ARM64\*.iobj bin\ARM64\*.ipdb bin\ARM64\common-tests*
|
||||
del /Q bin\ARM64\*.pdb
|
||||
del /Q bin\ARM64\*.exp
|
||||
del /Q bin\ARM64\*.lib
|
||||
del /Q bin\ARM64\*.iobj
|
||||
del /Q bin\ARM64\*.ipdb
|
||||
del /Q bin\ARM64\common-tests*
|
||||
rename bin\ARM64\updater-ARM64-ReleaseLTCG.exe updater.exe
|
||||
|
||||
|
||||
- name: Create arm64 release archive
|
||||
shell: cmd
|
||||
run: |
|
||||
"C:\Program Files\7-Zip\7z.exe" a -r duckstation-windows-arm64-release.zip ./bin/ARM64/*
|
||||
|
||||
- name: Upload arm64 release artifact
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: "windows-arm64"
|
||||
path: "duckstation-windows-arm64-release*.zip"
|
||||
path: "duckstation-windows-arm64-release.zip"
|
||||
|
||||
|
||||
linux-build:
|
||||
runs-on: ubuntu-22.04
|
||||
timeout-minutes: 120
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
# Work around https://github.com/actions/runner-images/issues/8659
|
||||
- name: Remove GCC 13 from runner image
|
||||
shell: bash
|
||||
run: |
|
||||
sudo rm -f /etc/apt/sources.list.d/ubuntu-toolchain-r-ubuntu-test-jammy.list
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --allow-downgrades 'libc6=2.35-0ubuntu*' 'libc6-dev=2.35-0ubuntu*' libstdc++6=12.3.0-1ubuntu1~22.04 libgcc-s1=12.3.0-1ubuntu1~22.04
|
||||
|
||||
- uses: actions/checkout@v4.1.6
|
||||
- uses: actions/checkout@v2.3.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Install packages
|
||||
shell: bash
|
||||
run: |
|
||||
# Workaround for https://github.com/actions/runner-images/issues/675
|
||||
scripts/retry.sh wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add -
|
||||
sudo scripts/retry.sh apt-add-repository -n 'deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-17 main'
|
||||
|
||||
sudo scripts/retry.sh apt-get update &&
|
||||
sudo scripts/retry.sh apt-get -y install \
|
||||
build-essential clang-17 cmake curl extra-cmake-modules git libasound2-dev libcurl4-openssl-dev libdbus-1-dev libdecor-0-dev libegl-dev libevdev-dev \
|
||||
libfontconfig-dev libfreetype-dev libfuse2 libgtk-3-dev libgudev-1.0-dev libharfbuzz-dev libinput-dev libopengl-dev libpipewire-0.3-dev libpulse-dev \
|
||||
libssl-dev libudev-dev libwayland-dev libx11-dev libx11-xcb-dev libxcb1-dev libxcb-composite0-dev libxcb-cursor-dev libxcb-damage0-dev libxcb-glx0-dev \
|
||||
libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-present-dev libxcb-randr0-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-shape0-dev \
|
||||
libxcb-shm0-dev libxcb-sync-dev libxcb-util-dev libxcb-xfixes0-dev libxcb-xinput-dev libxcb-xkb-dev libxext-dev libxkbcommon-x11-dev libxrandr-dev lld-17 \
|
||||
llvm-17 ninja-build patchelf pkg-config zlib1g-dev
|
||||
|
||||
- name: Cache Dependencies
|
||||
id: cache-deps
|
||||
uses: actions/cache@v4.0.2
|
||||
with:
|
||||
path: ~/deps
|
||||
key: deps ${{ hashFiles('scripts/build-dependencies-linux.sh') }}
|
||||
|
||||
- name: Build Dependencies
|
||||
if: steps.cache-deps.outputs.cache-hit != 'true'
|
||||
run: scripts/build-dependencies-linux.sh "$HOME/deps"
|
||||
|
||||
- name: Tag as preview build
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
echo '#pragma once' > src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_ASSET "DuckStation-x64.AppImage"' >> src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_TAGS {"latest", "preview"}' >> src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_TAG "preview"' >> src/scmversion/tag.h
|
||||
|
||||
- name: Tag as dev build
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: |
|
||||
echo '#pragma once' > src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_ASSET "DuckStation-x64.AppImage"' >> src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_TAGS {"latest", "preview"}' >> src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_TAG "latest"' >> src/scmversion/tag.h
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get -y install cmake ninja-build ccache libsdl2-dev libgtk-3-dev qtbase5-dev qtbase5-dev-tools qtbase5-private-dev qt5-default qttools5-dev libegl1-mesa-dev libevdev-dev libgbm-dev libdrm-dev libwayland-dev libwayland-egl-backend-dev extra-cmake-modules
|
||||
|
||||
- name: Compile build
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON -DCMAKE_PREFIX_PATH="$HOME/deps" -DCMAKE_C_COMPILER=clang-17 -DCMAKE_CXX_COMPILER=clang++-17 -DCMAKE_EXE_LINKER_FLAGS_INIT="-fuse-ld=lld" -DCMAKE_MODULE_LINKER_FLAGS_INIT="-fuse-ld=lld" -DCMAKE_SHARED_LINKER_FLAGS_INIT="-fuse-ld=lld" ..
|
||||
cmake --build . --parallel
|
||||
cd ..
|
||||
scripts/appimage/make-appimage.sh $(realpath .) $(realpath ./build) $HOME/deps DuckStation-x64
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_NOGUI_FRONTEND=ON -DBUILD_QT_FRONTEND=ON -DUSE_DRMKMS=ON -DUSE_EGL=ON -DUSE_SDL2=ON -DUSE_WAYLAND=ON -DUSE_X11=ON -G Ninja ..
|
||||
ninja
|
||||
../appimage/generate_appimages.sh $(pwd)
|
||||
|
||||
- name: Upload NoGUI AppImage
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: "linux-x64-appimage-nogui"
|
||||
path: "build/duckstation-nogui-x64.AppImage"
|
||||
|
||||
- name: Upload NoGUI AppImage zsync
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: "linux-x64-appimage-nogui-zsync"
|
||||
path: "build/duckstation-nogui-x64.AppImage.zsync"
|
||||
|
||||
- name: Upload Qt AppImage
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: "linux-x64-appimage-qt"
|
||||
path: "DuckStation-x64.AppImage"
|
||||
path: "build/duckstation-qt-x64.AppImage"
|
||||
|
||||
- name: Upload Qt AppImage zsync
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: "linux-x64-appimage-qt-zsync"
|
||||
path: "build/duckstation-qt-x64.AppImage.zsync"
|
||||
|
||||
|
||||
linux-flatpak-build:
|
||||
runs-on: ubuntu-22.04
|
||||
container:
|
||||
image: ghcr.io/flathub-infra/flatpak-github-actions:kde-6.7
|
||||
options: --privileged
|
||||
timeout-minutes: 120
|
||||
android-build:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.6
|
||||
- uses: actions/checkout@v2.3.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
set-safe-directory: ${{ env.GITHUB_WORKSPACE }}
|
||||
|
||||
# Work around container ownership issue
|
||||
- name: Set Safe Directory
|
||||
- name: Compile with Gradle
|
||||
shell: bash
|
||||
run: git config --global --add safe.directory "*"
|
||||
|
||||
- name: Generate AppStream XML
|
||||
run: |
|
||||
scripts/generate-metainfo.sh scripts/flatpak
|
||||
cat scripts/flatpak/org.duckstation.DuckStation.metainfo.xml
|
||||
cd android
|
||||
./gradlew assembleRelease
|
||||
|
||||
- name: Validate AppStream XML
|
||||
run: flatpak-builder-lint appstream scripts/flatpak/org.duckstation.DuckStation.metainfo.xml
|
||||
|
||||
- name: Validate manifest
|
||||
run: flatpak-builder-lint manifest scripts/flatpak/org.duckstation.DuckStation.json
|
||||
|
||||
- name: Build Flatpak
|
||||
uses: flathub-infra/flatpak-github-actions/flatpak-builder@23796715b3dfa4c86ddf50cf29c3cc8b3c82dca8
|
||||
- name: Sign APK
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev'
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
bundle: duckstation-x64.flatpak
|
||||
upload-artifact: false
|
||||
manifest-path: scripts/flatpak/org.duckstation.DuckStation.json
|
||||
arch: x86_64
|
||||
build-bundle: true
|
||||
verbose: true
|
||||
mirror-screenshots-url: https://dl.flathub.org/media
|
||||
branch: stable
|
||||
cache: true
|
||||
restore-cache: true
|
||||
cache-key: flatpak-x64-${{ hashFiles('scripts/flatpak/**/*.json') }}
|
||||
releaseDirectory: android/app/build/outputs/apk/release
|
||||
signingKeyBase64: ${{ secrets.APK_SIGNING_KEY }}
|
||||
alias: ${{ secrets.APK_KEY_ALIAS }}
|
||||
keyStorePassword: ${{ secrets.APK_KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.APK_KEY_PASSWORD }}
|
||||
|
||||
- name: Push to Flathub stable
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: flathub-infra/flatpak-github-actions/flat-manager@23796715b3dfa4c86ddf50cf29c3cc8b3c82dca8
|
||||
with:
|
||||
flat-manager-url: https://hub.flathub.org/
|
||||
repository: stable
|
||||
token: ${{ secrets.FLATHUB_STABLE_TOKEN }}
|
||||
build-log-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
- name: Validate build
|
||||
- name: Rename APK
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev'
|
||||
shell: bash
|
||||
run: |
|
||||
flatpak-builder-lint repo repo
|
||||
|
||||
- name: Upload Flatpak
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
cd android
|
||||
mv app/build/outputs/apk/release/app-release-unsigned-signed.apk ../duckstation-android.apk
|
||||
|
||||
- name: Upload APK
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev'
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: "linux-flatpak"
|
||||
path: "duckstation-x64.flatpak"
|
||||
|
||||
name: "android"
|
||||
path: "duckstation-android.apk"
|
||||
|
||||
macos-build:
|
||||
runs-on: macos-14
|
||||
timeout-minutes: 120
|
||||
runs-on: macos-10.15
|
||||
steps:
|
||||
- uses: actions/checkout@v4.1.6
|
||||
- uses: actions/checkout@v2.3.1
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Use Xcode 15.4
|
||||
run: sudo xcode-select -s /Applications/Xcode_15.4.app
|
||||
|
||||
- name: Install packages
|
||||
shell: bash
|
||||
run: |
|
||||
brew install curl ninja
|
||||
brew install qt5 sdl2
|
||||
|
||||
- name: Cache Dependencies
|
||||
id: cache-deps-mac
|
||||
uses: actions/cache@v4.0.2
|
||||
with:
|
||||
path: ~/deps
|
||||
key: deps-mac ${{ hashFiles('scripts/build-dependencies-mac.sh') }}
|
||||
|
||||
- name: Build Dependencies
|
||||
if: steps.cache-deps-mac.outputs.cache-hit != 'true'
|
||||
run: scripts/build-dependencies-mac.sh "$HOME/deps"
|
||||
|
||||
- name: Tag as preview build
|
||||
if: github.ref == 'refs/heads/master'
|
||||
- name: Clone mac externals
|
||||
shell: bash
|
||||
run: |
|
||||
echo '#pragma once' > src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_ASSET "duckstation-mac-release.zip"' >> src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_TAGS {"latest", "preview"}' >> src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_TAG "preview"' >> src/scmversion/tag.h
|
||||
|
||||
- name: Tag as dev build
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
run: |
|
||||
echo '#pragma once' > src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_ASSET "duckstation-mac-release.zip"' >> src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_TAGS {"latest", "preview"}' >> src/scmversion/tag.h
|
||||
echo '#define SCM_RELEASE_TAG "latest"' >> src/scmversion/tag.h
|
||||
git clone https://github.com/stenzek/duckstation-ext-mac.git dep/mac
|
||||
|
||||
- name: Compile and zip .app
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir build
|
||||
cd build
|
||||
export MACOSX_DEPLOYMENT_TARGET=11.0
|
||||
cmake -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" -DCMAKE_BUILD_TYPE=Release -DENABLE_OPENGL=OFF -DCMAKE_PREFIX_PATH="$HOME/deps" -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON -G Ninja ..
|
||||
cmake --build . --parallel
|
||||
mv bin/DuckStation.app .
|
||||
codesign -s - --deep -f -v DuckStation.app
|
||||
export MACOSX_DEPLOYMENT_TARGET=10.14
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DBUILD_NOGUI_FRONTEND=OFF -DBUILD_QT_FRONTEND=ON -DUSE_SDL2=ON -DQt5_DIR=/usr/local/opt/qt/lib/cmake/Qt5 ..
|
||||
cmake --build . --parallel 2
|
||||
cd bin
|
||||
zip -r duckstation-mac-release.zip DuckStation.app/
|
||||
|
||||
- name: Upload macOS .app
|
||||
uses: actions/upload-artifact@v4.3.3
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: "macos"
|
||||
path: "build/duckstation-mac-release.zip"
|
||||
name: "macos-x64"
|
||||
path: "build/bin/duckstation-mac-release.zip"
|
||||
|
||||
|
||||
create-release:
|
||||
needs: [windows-build, windows-arm64-build, linux-build, linux-flatpak-build, macos-build]
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [windows-build, windows-arm64-build, linux-build, android-build, macos-build]
|
||||
runs-on: "ubuntu-latest"
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/dev'
|
||||
steps:
|
||||
- name: Download Artifacts
|
||||
uses: actions/download-artifact@v4.1.7
|
||||
- name: Download Windows Artifacts
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
path: ./artifacts/
|
||||
name: "windows"
|
||||
|
||||
- name: Display Downloaded Artifacts
|
||||
run: find ./artifacts/
|
||||
- name: Download Windows ARM64 Artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: "windows-arm64"
|
||||
|
||||
- name: Download NoGUI AppImage Artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: "linux-x64-appimage-nogui"
|
||||
|
||||
- name: Download NoGUI AppImage zsync Artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: "linux-x64-appimage-nogui-zsync"
|
||||
|
||||
- name: Download Qt AppImage Artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: "linux-x64-appimage-qt"
|
||||
|
||||
- name: Download Qt AppImage zsync Artifact
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: "linux-x64-appimage-qt-zsync"
|
||||
|
||||
- name: Download Android APK
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: "android"
|
||||
|
||||
- name: Download Mac App
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: "macos-x64"
|
||||
|
||||
- name: Create preview release
|
||||
if: github.ref == 'refs/heads/master'
|
||||
@@ -413,13 +328,15 @@ jobs:
|
||||
prerelease: true
|
||||
title: "Latest Preview Build"
|
||||
files: |
|
||||
./artifacts/windows/duckstation-windows-x64-release.zip
|
||||
./artifacts/windows/duckstation-windows-x64-release-symbols.zip
|
||||
./artifacts/windows-arm64/duckstation-windows-arm64-release.zip
|
||||
./artifacts/windows-arm64/duckstation-windows-arm64-release-symbols.zip
|
||||
./artifacts/linux-x64-appimage-qt/DuckStation-x64.AppImage
|
||||
./artifacts/linux-flatpak/duckstation-x64.flatpak
|
||||
./artifacts/macos/duckstation-mac-release.zip
|
||||
windows/duckstation-windows-x64-release.zip
|
||||
windows/duckstation-windows-x64-release-symbols.zip
|
||||
windows-arm64/duckstation-windows-arm64-release.zip
|
||||
windows-arm64/duckstation-windows-arm64-release-symbols.zip
|
||||
linux-x64-appimage-nogui/duckstation-nogui-x64.AppImage
|
||||
linux-x64-appimage-nogui-zsync/duckstation-nogui-x64.AppImage.zsync
|
||||
linux-x64-appimage-qt/duckstation-qt-x64.AppImage
|
||||
linux-x64-appimage-qt-zsync/duckstation-qt-x64.AppImage.zsync
|
||||
android/duckstation-android.apk
|
||||
|
||||
- name: Create dev release
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
@@ -428,13 +345,16 @@ jobs:
|
||||
repo_token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
automatic_release_tag: "latest"
|
||||
prerelease: false
|
||||
title: "Latest Rolling Release"
|
||||
title: "Latest Development Build"
|
||||
files: |
|
||||
./artifacts/windows/duckstation-windows-x64-release.zip
|
||||
./artifacts/windows/duckstation-windows-x64-release-symbols.zip
|
||||
./artifacts/windows-arm64/duckstation-windows-arm64-release.zip
|
||||
./artifacts/windows-arm64/duckstation-windows-arm64-release-symbols.zip
|
||||
./artifacts/linux-x64-appimage-qt/DuckStation-x64.AppImage
|
||||
./artifacts/linux-flatpak/duckstation-x64.flatpak
|
||||
./artifacts/macos/duckstation-mac-release.zip
|
||||
windows/duckstation-windows-x64-release.zip
|
||||
windows/duckstation-windows-x64-release-symbols.zip
|
||||
windows-arm64/duckstation-windows-arm64-release.zip
|
||||
windows-arm64/duckstation-windows-arm64-release-symbols.zip
|
||||
linux-x64-appimage-nogui/duckstation-nogui-x64.AppImage
|
||||
linux-x64-appimage-nogui-zsync/duckstation-nogui-x64.AppImage.zsync
|
||||
linux-x64-appimage-qt/duckstation-qt-x64.AppImage
|
||||
linux-x64-appimage-qt-zsync/duckstation-qt-x64.AppImage.zsync
|
||||
android/duckstation-android.apk
|
||||
macos-x64/duckstation-mac-release.zip
|
||||
|
||||
|
||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -3,12 +3,10 @@
|
||||
|
||||
# binaries folder
|
||||
/bin/
|
||||
/Build/
|
||||
/build/
|
||||
/build-*/
|
||||
|
||||
# dependency build temp files
|
||||
deps-build/
|
||||
|
||||
# vs stuff
|
||||
.vs
|
||||
ipch
|
||||
@@ -19,7 +17,6 @@ ipch/*
|
||||
*.vcxproj.user
|
||||
*.VC.opendb
|
||||
*.VC.db
|
||||
/.vscode/
|
||||
|
||||
# cmake stuff
|
||||
CMakeCache.txt
|
||||
@@ -42,5 +39,4 @@ CMakeLists.txt.user
|
||||
__pycache__
|
||||
|
||||
# other repos
|
||||
/android
|
||||
|
||||
/dep/mac
|
||||
|
||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
[submodule "dep/msvc/qt"]
|
||||
path = dep/msvc/qt
|
||||
url = https://github.com/stenzek/duckstation-ext-qt-minimal.git
|
||||
shallow = true
|
||||
255
CMakeLists.txt
255
CMakeLists.txt
@@ -1,43 +1,138 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(duckstation C CXX)
|
||||
|
||||
# Policy settings.
|
||||
cmake_policy(SET CMP0069 NEW)
|
||||
set(CMAKE_POLICY_DEFAULT_CMP0069 NEW)
|
||||
|
||||
if(${CMAKE_SOURCE_DIR} STREQUAL ${CMAKE_BINARY_DIR})
|
||||
message(FATAL_ERROR "DuckStation does not support in-tree builds. Please make a build directory that is not the source"
|
||||
"directory and generate your CMake project there using either `cmake -B build_directory` or by "
|
||||
"running cmake from the build directory.")
|
||||
endif()
|
||||
|
||||
if(NOT CMAKE_BUILD_TYPE MATCHES "Debug|Devel|MinSizeRel|RelWithDebInfo|Release")
|
||||
message(STATUS "CMAKE_BUILD_TYPE not set, defaulting to Release.")
|
||||
set(CMAKE_BUILD_TYPE "Release")
|
||||
endif()
|
||||
|
||||
message(STATUS "CMake Version: ${CMAKE_VERSION}")
|
||||
message(STATUS "CMake System Name: ${CMAKE_SYSTEM_NAME}")
|
||||
message(STATUS "Build Type: ${CMAKE_BUILD_TYPE}")
|
||||
message("CMake Version: ${CMAKE_VERSION}")
|
||||
|
||||
# Pull in modules.
|
||||
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMakeModules/")
|
||||
include(DuckStationUtils)
|
||||
|
||||
# Detect system attributes.
|
||||
detect_operating_system()
|
||||
detect_compiler()
|
||||
detect_architecture()
|
||||
detect_page_size()
|
||||
detect_cache_line_size()
|
||||
# Platform detection.
|
||||
if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux")
|
||||
set(LINUX TRUE)
|
||||
set(SUPPORTS_X11 TRUE)
|
||||
set(SUPPORTS_WAYLAND TRUE)
|
||||
endif()
|
||||
|
||||
# Build options. Depends on system attributes.
|
||||
include(DuckStationBuildOptions)
|
||||
# Set minimum OS version for macOS. 10.14 should work.
|
||||
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.14.0" CACHE STRING "")
|
||||
|
||||
# Global options.
|
||||
if(NOT ANDROID)
|
||||
option(BUILD_SDL_FRONTEND "Build the SDL frontend" OFF)
|
||||
option(BUILD_NOGUI_FRONTEND "Build the NoGUI frontend" ON)
|
||||
option(BUILD_QT_FRONTEND "Build the Qt frontend" ON)
|
||||
option(ENABLE_DISCORD_PRESENCE "Build with Discord Rich Presence support" ON)
|
||||
option(USE_SDL2 "Link with SDL2 for controller support" ON)
|
||||
endif()
|
||||
|
||||
|
||||
# OpenGL context creation methods.
|
||||
if(SUPPORTS_X11)
|
||||
option(USE_X11 "Support X11 window system" ON)
|
||||
endif()
|
||||
if(SUPPORTS_WAYLAND)
|
||||
option(USE_WAYLAND "Support Wayland window system" OFF)
|
||||
endif()
|
||||
if(LINUX OR ANDROID)
|
||||
option(USE_EGL "Support EGL OpenGL context creation" ON)
|
||||
endif()
|
||||
if(LINUX AND NOT ANDROID)
|
||||
option(USE_DRMKMS "Support DRM/KMS OpenGL contexts" OFF)
|
||||
option(USE_FBDEV "Support FBDev OpenGL contexts" OFF)
|
||||
endif()
|
||||
|
||||
# Force EGL when using Wayland
|
||||
if(USE_WAYLAND)
|
||||
set(USE_EGL ON)
|
||||
endif()
|
||||
|
||||
if(ANDROID)
|
||||
if(BUILD_SDL_FRONTEND)
|
||||
message(WARNING "Building for Android, disabling SDL frontend")
|
||||
set(BUILD_SDL_FRONTEND OFF)
|
||||
endif()
|
||||
if(BUILD_NOGUI_FRONTEND)
|
||||
message(WARNING "Building for Android, disabling NoGUI frontend")
|
||||
set(BUILD_QT_FRONTEND OFF)
|
||||
endif()
|
||||
if(BUILD_QT_FRONTEND)
|
||||
message(WARNING "Building for Android, disabling Qt frontend")
|
||||
set(BUILD_QT_FRONTEND OFF)
|
||||
endif()
|
||||
if(ENABLE_DISCORD_PRESENCE)
|
||||
message("Building for Android, disabling Discord Presence support")
|
||||
set(ENABLE_DISCORD_PRESENCE OFF)
|
||||
endif()
|
||||
if(USE_SDL2)
|
||||
message("Building for Android, disabling SDL2 support")
|
||||
set(USE_SDL2 OFF)
|
||||
endif()
|
||||
if(USE_X11)
|
||||
set(USE_X11 OFF)
|
||||
endif()
|
||||
if(USE_WAYLAND)
|
||||
set(USE_WAYLAND OFF)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
# Common include/library directories on Windows.
|
||||
if(WIN32)
|
||||
set(SDL2_FOUND TRUE)
|
||||
set(SDL2_INCLUDE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/dep/msvc/sdl2/include")
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
set(SDL2_LIBRARIES "${CMAKE_CURRENT_SOURCE_DIR}/dep/msvc/sdl2/lib64/SDL2.lib")
|
||||
set(SDL2MAIN_LIBRARIES "${CMAKE_CURRENT_SOURCE_DIR}/dep/msvc/sdl2/lib64/SDL2main.lib")
|
||||
set(SDL2_DLL_PATH "${CMAKE_CURRENT_SOURCE_DIR}/dep/msvc/sdl2/bin64/SDL2.dll")
|
||||
set(Qt5_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dep/msvc/qt/5.15.0/msvc2017_64/lib/cmake/Qt5")
|
||||
else()
|
||||
set(SDL2_LIBRARIES "${CMAKE_CURRENT_SOURCE_DIR}/dep/msvc/sdl2/lib32/SDL2.lib")
|
||||
set(SDL2MAIN_LIBRARIES "${CMAKE_CURRENT_SOURCE_DIR}/dep/msvc/sdl2/lib32/SDL2main.lib")
|
||||
set(SDL2_DLL_PATH "${CMAKE_CURRENT_SOURCE_DIR}/dep/msvc/sdl2/bin32/SDL2.dll")
|
||||
set(Qt5_DIR "${CMAKE_CURRENT_SOURCE_DIR}/dep/msvc/qt/5.15.0/msvc2017_32/lib/cmake/Qt5")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
|
||||
# Required libraries.
|
||||
if(NOT ANDROID)
|
||||
if(NOT WIN32 AND (BUILD_SDL_FRONTEND OR USE_SDL2))
|
||||
find_package(SDL2 REQUIRED)
|
||||
endif()
|
||||
if(BUILD_QT_FRONTEND)
|
||||
find_package(Qt5 COMPONENTS Core Gui Widgets Network LinguistTools REQUIRED)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(USE_EGL)
|
||||
find_package(EGL REQUIRED)
|
||||
endif()
|
||||
if(USE_X11)
|
||||
find_package(X11 REQUIRED)
|
||||
endif()
|
||||
if(USE_WAYLAND)
|
||||
find_package(ECM REQUIRED NO_MODULE)
|
||||
list(APPEND CMAKE_MODULE_PATH "${ECM_MODULE_PATH}")
|
||||
find_package(Wayland REQUIRED Egl)
|
||||
message(STATUS "Wayland support enabled")
|
||||
endif()
|
||||
if(USE_DRMKMS AND USE_FBDEV)
|
||||
message(FATAL_ERROR "Only one of DRM/KMS and FBDev can be enabled")
|
||||
endif()
|
||||
if(USE_DRMKMS)
|
||||
find_package(GBM REQUIRED)
|
||||
find_package(Libdrm REQUIRED)
|
||||
message(STATUS "DRM/KMS support enabled")
|
||||
endif()
|
||||
if(USE_FBDEV)
|
||||
message(STATUS "FBDev Support enabled")
|
||||
endif()
|
||||
|
||||
# Set _DEBUG macro for Debug builds.
|
||||
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -D_DEBUG")
|
||||
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -D_DEBUG")
|
||||
|
||||
|
||||
# Release build optimizations for MSVC.
|
||||
if(MSVC)
|
||||
add_definitions("/D_CRT_SECURE_NO_WARNINGS")
|
||||
@@ -45,8 +140,8 @@ if(MSVC)
|
||||
# Set warning level 3 instead of 4.
|
||||
string(REPLACE "/W3" "/W4" ${config} "${${config}}")
|
||||
|
||||
# Enable intrinsic functions, disable minimal rebuild, UTF-8 source, set __cplusplus version.
|
||||
set(${config} "${${config}} /Oi /Gm- /utf-8 /Zc:__cplusplus")
|
||||
# Enable intrinsic functions, disable minimal rebuild, UTF-8 source.
|
||||
set(${config} "${${config}} /Oi /Gm- /utf-8")
|
||||
endforeach()
|
||||
|
||||
# RelWithDebInfo is set to Ob1 instead of Ob2.
|
||||
@@ -59,52 +154,90 @@ if(MSVC)
|
||||
# COMDAT folding/remove unused functions.
|
||||
set(CMAKE_EXE_LINKER_FLAGS_RELEASE "${CMAKE_EXE_LINKER_FLAGS_RELEASE} /OPT:REF /OPT:ICF")
|
||||
set(CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO "${CMAKE_EXE_LINKER_FLAGS_RELWITHDEBINFO} /OPT:REF /OPT:ICF")
|
||||
|
||||
# Enable LTO/LTCG on Release builds.
|
||||
if(${CMAKE_BUILD_TYPE} STREQUAL "Release")
|
||||
cmake_policy(SET CMP0069 NEW)
|
||||
include(CheckIPOSupported)
|
||||
check_ipo_supported(RESULT IPO_IS_SUPPORTED)
|
||||
if(IPO_IS_SUPPORTED)
|
||||
message(STATUS "Enabling LTCG/IPO.")
|
||||
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION ON)
|
||||
else()
|
||||
message(WARNING "LTCG/IPO is not supported, this will make the build slightly slower.")
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Warning disables.
|
||||
if(COMPILER_CLANG OR COMPILER_CLANG_CL OR COMPILER_GCC)
|
||||
include(CheckCXXFlag)
|
||||
check_cxx_flag(-Wall COMPILER_SUPPORTS_WALL)
|
||||
check_cxx_flag(-Wno-class-memaccess COMPILER_SUPPORTS_MEMACCESS)
|
||||
check_cxx_flag(-Wno-invalid-offsetof COMPILER_SUPPORTS_OFFSETOF)
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-switch")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-switch")
|
||||
|
||||
# Detect C++ version support.
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
|
||||
include(CheckCXXFlag)
|
||||
check_cxx_flag(-Wall COMPILER_SUPPORTS_WALL)
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-switch")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-switch")
|
||||
if(NOT ANDROID)
|
||||
check_cxx_flag(-Wno-class-memaccess COMPILER_SUPPORTS_MEMACCESS)
|
||||
check_cxx_flag(-Wno-invalid-offsetof COMPILER_SUPPORTS_OFFSETOF)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# We don't need exceptions, disable them to save a bit of code size.
|
||||
if(MSVC)
|
||||
string(REPLACE "/EHsc" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
|
||||
string(REPLACE "/GR" "" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
|
||||
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /D_HAS_EXCEPTIONS=0 /permissive-")
|
||||
# Detect processor type.
|
||||
if(${CMAKE_SYSTEM_PROCESSOR} STREQUAL "x86_64" OR ${CMAKE_SYSTEM_PROCESSOR} STREQUAL "amd64")
|
||||
set(CPU_ARCH "x64")
|
||||
elseif(${CMAKE_SYSTEM_PROCESSOR} STREQUAL "AMD64")
|
||||
# MSVC x86/x64
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
set(CPU_ARCH "x64")
|
||||
else()
|
||||
set(CPU_ARCH "x86")
|
||||
endif()
|
||||
elseif(${CMAKE_SYSTEM_PROCESSOR} STREQUAL "x86" OR ${CMAKE_SYSTEM_PROCESSOR} STREQUAL "i386" OR
|
||||
${CMAKE_SYSTEM_PROCESSOR} STREQUAL "i686")
|
||||
set(CPU_ARCH "x86")
|
||||
elseif(${CMAKE_SYSTEM_PROCESSOR} STREQUAL "aarch64")
|
||||
set(CPU_ARCH "aarch64")
|
||||
elseif(${CMAKE_SYSTEM_PROCESSOR} STREQUAL "arm" OR ${CMAKE_SYSTEM_PROCESSOR} STREQUAL "armv7-a" OR ${CMAKE_SYSTEM_PROCESSOR} STREQUAL "armv7l")
|
||||
set(CPU_ARCH "aarch32")
|
||||
if(ANDROID)
|
||||
# Force ARM mode, since apparently ANDROID_ARM_MODE isn't working..
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -marm")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -marm")
|
||||
else()
|
||||
# Enable NEON.
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -marm -march=armv7-a")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -marm -march=armv7-a")
|
||||
endif()
|
||||
else()
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-exceptions -fno-rtti")
|
||||
message(FATAL_ERROR "Unknown system processor: " ${CMAKE_SYSTEM_PROCESSOR})
|
||||
endif()
|
||||
|
||||
|
||||
# Write binaries to a seperate directory.
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bin")
|
||||
|
||||
# Enable large file support on Linux 32-bit platforms.
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 4)
|
||||
add_definitions("-D_FILE_OFFSET_BITS=64")
|
||||
if(WIN32)
|
||||
# For Windows, use the source directory, except for libretro.
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/bin/${CPU_ARCH}")
|
||||
else()
|
||||
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/bin")
|
||||
endif()
|
||||
|
||||
# Optional unit tests.
|
||||
if(BUILD_TESTS)
|
||||
enable_testing()
|
||||
endif()
|
||||
# Needed for Linux - put shared libraries in the binary directory.
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
|
||||
|
||||
# Prevent fmt from being built with exceptions, or being thrown at call sites.
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DFMT_EXCEPTIONS=0")
|
||||
|
||||
# Use C++20.
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
# Enable threads everywhere.
|
||||
set(THREADS_PREFER_PTHREAD_FLAG ON)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
|
||||
# Recursively include the source tree.
|
||||
include(DuckStationDependencies)
|
||||
enable_testing()
|
||||
add_subdirectory(dep)
|
||||
add_subdirectory(src)
|
||||
|
||||
# Output build summary.
|
||||
include(DuckStationBuildSummary)
|
||||
if(ANDROID)
|
||||
add_subdirectory(android/app/src/cpp)
|
||||
endif()
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# Borrowed from PCSX2.
|
||||
|
||||
if(APPLE)
|
||||
function(add_metal_sources target sources)
|
||||
if(CMAKE_GENERATOR MATCHES "Xcode")
|
||||
# If we're generating an xcode project, you can just add the shaders to the main pcsx2 target and xcode will deal with them properly
|
||||
# This will make sure xcode supplies code completion, etc (if you use a custom command, it won't)
|
||||
set_target_properties(${target} PROPERTIES
|
||||
XCODE_ATTRIBUTE_MTL_ENABLE_DEBUG_INFO INCLUDE_SOURCE
|
||||
)
|
||||
foreach(shader IN LISTS sources)
|
||||
target_sources(${target} PRIVATE ${shader})
|
||||
set_source_files_properties(${shader} PROPERTIES LANGUAGE METAL)
|
||||
endforeach()
|
||||
else()
|
||||
function(generateMetallib std triple outputName)
|
||||
set(MetalShaderOut)
|
||||
set(flags
|
||||
-ffast-math
|
||||
$<$<NOT:$<CONFIG:Release,MinSizeRel>>:-gline-tables-only>
|
||||
$<$<NOT:$<CONFIG:Release,MinSizeRel>>:-MO>
|
||||
)
|
||||
foreach(shader IN LISTS sources)
|
||||
file(RELATIVE_PATH relativeShader "${CMAKE_SOURCE_DIR}" "${shader}")
|
||||
set(shaderOut ${CMAKE_CURRENT_BINARY_DIR}/${outputName}/${relativeShader}.air)
|
||||
list(APPEND MetalShaderOut ${shaderOut})
|
||||
get_filename_component(shaderDir ${shaderOut} DIRECTORY)
|
||||
add_custom_command(OUTPUT ${shaderOut}
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory ${shaderDir}
|
||||
COMMAND xcrun metal ${flags} -std=${std} -target ${triple} -o ${shaderOut} -c ${shader}
|
||||
DEPENDS ${shader}
|
||||
)
|
||||
set(metallib ${CMAKE_CURRENT_BINARY_DIR}/${outputName}.metallib)
|
||||
endforeach()
|
||||
add_custom_command(OUTPUT ${metallib}
|
||||
COMMAND xcrun metallib -o ${metallib} ${MetalShaderOut}
|
||||
DEPENDS ${MetalShaderOut}
|
||||
)
|
||||
target_sources(${target} PRIVATE ${metallib})
|
||||
set_source_files_properties(${metallib} PROPERTIES MACOSX_PACKAGE_LOCATION Resources)
|
||||
endfunction()
|
||||
generateMetallib(macos-metal2.0 air64-apple-macos10.13 default)
|
||||
generateMetallib(macos-metal2.2 air64-apple-macos10.15 Metal22)
|
||||
generateMetallib(macos-metal2.3 air64-apple-macos11.0 Metal23)
|
||||
endif()
|
||||
endfunction()
|
||||
endif()
|
||||
@@ -1,50 +0,0 @@
|
||||
function(copy_base_translations target)
|
||||
get_target_property(MOC_EXECUTABLE_LOCATION Qt6::moc IMPORTED_LOCATION)
|
||||
get_filename_component(QT_BINARY_DIRECTORY "${MOC_EXECUTABLE_LOCATION}" DIRECTORY)
|
||||
find_program(LCONVERT_EXE lconvert HINTS "${QT_BINARY_DIRECTORY}")
|
||||
set(BASE_TRANSLATIONS_DIR "${QT_BINARY_DIRECTORY}/../translations")
|
||||
|
||||
if(NOT APPLE)
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E make_directory "$<TARGET_FILE_DIR:${target}>/translations")
|
||||
endif()
|
||||
|
||||
file(GLOB qmFiles "${BASE_TRANSLATIONS_DIR}/qt_*.qm")
|
||||
foreach(path IN LISTS qmFiles)
|
||||
get_filename_component(file ${path} NAME)
|
||||
|
||||
# qt_help_<lang> just has to ruin everything.
|
||||
if(file MATCHES "qt_help_" OR NOT file MATCHES "qt_([^.]+).qm")
|
||||
continue()
|
||||
endif()
|
||||
|
||||
# If qtbase_<lang>.qm exists, merge all qms for that language into a single qm.
|
||||
set(lang "${CMAKE_MATCH_1}")
|
||||
set(baseQmPath "${BASE_TRANSLATIONS_DIR}/qtbase_${lang}.qm")
|
||||
if(EXISTS "${baseQmPath}")
|
||||
set(outPath "${CMAKE_CURRENT_BINARY_DIR}/qt_${lang}.qm")
|
||||
set(srcQmFiles)
|
||||
file(GLOB langQmFiles "${BASE_TRANSLATIONS_DIR}/qt*${lang}.qm")
|
||||
foreach(qmFile IN LISTS langQmFiles)
|
||||
get_filename_component(file ${qmFile} NAME)
|
||||
if(file STREQUAL "qt_${lang}.qm")
|
||||
continue()
|
||||
endif()
|
||||
LIST(APPEND srcQmFiles "${qmFile}")
|
||||
endforeach()
|
||||
add_custom_command(OUTPUT ${outPath}
|
||||
COMMAND "${LCONVERT_EXE}" -verbose -of qm -o "${outPath}" ${srcQmFiles}
|
||||
DEPENDS ${srcQmFiles}
|
||||
)
|
||||
set(path "${outPath}")
|
||||
endif()
|
||||
|
||||
target_sources(${target} PRIVATE ${path})
|
||||
if(APPLE)
|
||||
set_source_files_properties(${path} PROPERTIES MACOSX_PACKAGE_LOCATION Resources/translations)
|
||||
else()
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${path}" "$<TARGET_FILE_DIR:${target}>/translations")
|
||||
endif()
|
||||
endforeach()
|
||||
endfunction()
|
||||
46
CMakeModules/DolphinPostprocessBundle.cmake
Normal file
46
CMakeModules/DolphinPostprocessBundle.cmake
Normal file
@@ -0,0 +1,46 @@
|
||||
# This module can be used in two different ways.
|
||||
#
|
||||
# When invoked as `cmake -P DolphinPostprocessBundle.cmake`, it fixes up an
|
||||
# application folder to be standalone. It bundles all required libraries from
|
||||
# the system and fixes up library IDs. Any additional shared libraries, like
|
||||
# plugins, that are found under Contents/MacOS/ will be made standalone as well.
|
||||
#
|
||||
# When called with `include(DolphinPostprocessBundle)`, it defines a helper
|
||||
# function `dolphin_postprocess_bundle` that sets up the command form of the
|
||||
# module as a post-build step.
|
||||
|
||||
if(CMAKE_GENERATOR)
|
||||
# Being called as include(DolphinPostprocessBundle), so define a helper function.
|
||||
set(_DOLPHIN_POSTPROCESS_BUNDLE_MODULE_LOCATION "${CMAKE_CURRENT_LIST_FILE}")
|
||||
function(dolphin_postprocess_bundle target)
|
||||
add_custom_command(TARGET ${target} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -DDOLPHIN_BUNDLE_PATH="$<TARGET_FILE_DIR:${target}>/../.."
|
||||
-P "${_DOLPHIN_POSTPROCESS_BUNDLE_MODULE_LOCATION}"
|
||||
)
|
||||
endfunction()
|
||||
return()
|
||||
endif()
|
||||
|
||||
get_filename_component(DOLPHIN_BUNDLE_PATH "${DOLPHIN_BUNDLE_PATH}" ABSOLUTE)
|
||||
message(STATUS "Fixing up application bundle: ${DOLPHIN_BUNDLE_PATH}")
|
||||
|
||||
# Make sure to fix up any additional shared libraries (like plugins) that are
|
||||
# needed.
|
||||
file(GLOB_RECURSE extra_libs "${DOLPHIN_BUNDLE_PATH}/Contents/MacOS/*.dylib")
|
||||
|
||||
# BundleUtilities doesn't support DYLD_FALLBACK_LIBRARY_PATH behavior, which
|
||||
# makes it sometimes break on libraries that do weird things with @rpath. Specify
|
||||
# equivalent search directories until https://gitlab.kitware.com/cmake/cmake/issues/16625
|
||||
# is fixed and in our minimum CMake version.
|
||||
set(extra_dirs "/usr/local/lib" "/lib" "/usr/lib")
|
||||
|
||||
# BundleUtilities is overly verbose, so disable most of its messages
|
||||
function(message)
|
||||
if(NOT ARGV MATCHES "^STATUS;")
|
||||
_message(${ARGV})
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
include(BundleUtilities)
|
||||
set(BU_CHMOD_BUNDLE_ITEMS ON)
|
||||
fixup_bundle("${DOLPHIN_BUNDLE_PATH}" "${extra_libs}" "${extra_dirs}")
|
||||
@@ -1,15 +0,0 @@
|
||||
# Renderer options.
|
||||
option(ENABLE_OPENGL "Build with OpenGL renderer" ON)
|
||||
option(ENABLE_VULKAN "Build with Vulkan renderer" ON)
|
||||
option(BUILD_NOGUI_FRONTEND "Build the NoGUI frontend" OFF)
|
||||
option(BUILD_QT_FRONTEND "Build the Qt frontend" ON)
|
||||
option(BUILD_REGTEST "Build regression test runner" OFF)
|
||||
option(BUILD_TESTS "Build unit tests" OFF)
|
||||
|
||||
if(LINUX OR BSD)
|
||||
option(ENABLE_X11 "Support X11 window system" ON)
|
||||
option(ENABLE_WAYLAND "Support Wayland window system" ON)
|
||||
endif()
|
||||
if(APPLE)
|
||||
option(SKIP_POSTPROCESS_BUNDLE "Disable bundle post-processing, including Qt additions" OFF)
|
||||
endif()
|
||||
@@ -1,45 +0,0 @@
|
||||
if(ENABLE_OPENGL)
|
||||
message(STATUS "Building with OpenGL support.")
|
||||
endif()
|
||||
if(ENABLE_VULKAN)
|
||||
message(STATUS "Building with Vulkan support.")
|
||||
endif()
|
||||
if(ENABLE_X11)
|
||||
message(STATUS "Building with X11 support.")
|
||||
endif()
|
||||
if(ENABLE_WAYLAND)
|
||||
message(STATUS "Building with Wayland support.")
|
||||
endif()
|
||||
|
||||
if(BUILD_QT_FRONTEND)
|
||||
message(STATUS "Building Qt frontend.")
|
||||
endif()
|
||||
if(BUILD_NOGUI_FRONTEND)
|
||||
message(STATUS "Building NoGUI frontend.")
|
||||
endif()
|
||||
if(BUILD_REGTEST)
|
||||
message(STATUS "Building RegTest frontend.")
|
||||
endif()
|
||||
if(BUILD_TESTS)
|
||||
message(STATUS "Building unit tests.")
|
||||
endif()
|
||||
|
||||
if(NOT IS_SUPPORTED_COMPILER)
|
||||
message(WARNING "
|
||||
*************** UNSUPPORTED CONFIGURATION ***************
|
||||
You are not compiling DuckStation with a supported compiler.
|
||||
It may not even build successfully.
|
||||
DuckStation only supports the Clang and MSVC compilers.
|
||||
No support will be provided, continue at your own risk.
|
||||
*********************************************************")
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
message(WARNING "
|
||||
*************** UNSUPPORTED CONFIGURATION ***************
|
||||
You are compiling DuckStation with CMake on Windows.
|
||||
It may not even build successfully.
|
||||
DuckStation only supports MSBuild on Windows.
|
||||
No support will be provided, continue at your own risk.
|
||||
*********************************************************")
|
||||
endif()
|
||||
@@ -1,61 +0,0 @@
|
||||
# From PCSX2: On macOS, Mono.framework contains an ancient version of libpng. We don't want that.
|
||||
# Avoid it by telling cmake to avoid finding frameworks while we search for libpng.
|
||||
if(APPLE)
|
||||
set(FIND_FRAMEWORK_BACKUP ${CMAKE_FIND_FRAMEWORK})
|
||||
set(CMAKE_FIND_FRAMEWORK NEVER)
|
||||
endif()
|
||||
|
||||
# Enable threads everywhere.
|
||||
set(THREADS_PREFER_PTHREAD_FLAG ON)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
find_package(SDL2 2.30.4 REQUIRED)
|
||||
find_package(Zstd 1.5.6 REQUIRED)
|
||||
find_package(WebP REQUIRED) # v1.4.0, spews an error on Linux because no pkg-config.
|
||||
find_package(ZLIB REQUIRED) # 1.3, but Mac currently doesn't use it.
|
||||
find_package(PNG 1.6.40 REQUIRED)
|
||||
find_package(JPEG REQUIRED) # No version because flatpak uses libjpeg-turbo.
|
||||
find_package(Freetype 2.11.1 REQUIRED)
|
||||
|
||||
if(NOT WIN32)
|
||||
find_package(CURL REQUIRED)
|
||||
endif()
|
||||
|
||||
if(ENABLE_X11)
|
||||
find_package(X11 REQUIRED)
|
||||
if (NOT X11_Xrandr_FOUND)
|
||||
message(FATAL_ERROR "XRandR extension is required")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(ENABLE_WAYLAND)
|
||||
find_package(ECM REQUIRED NO_MODULE)
|
||||
list(APPEND CMAKE_MODULE_PATH "${ECM_MODULE_PATH}")
|
||||
find_package(Wayland REQUIRED Egl)
|
||||
endif()
|
||||
|
||||
if(ENABLE_VULKAN)
|
||||
find_package(Shaderc REQUIRED)
|
||||
find_package(spirv_cross_c_shared REQUIRED)
|
||||
|
||||
if(LINUX)
|
||||
# We need to add the rpath for shaderc to the executable.
|
||||
get_filename_component(SHADERC_LIBRARY_DIRECTORY ${SHADERC_LIBRARY} DIRECTORY)
|
||||
list(APPEND CMAKE_BUILD_RPATH ${SHADERC_LIBRARY_DIRECTORY})
|
||||
get_target_property(SPIRV_CROSS_LIBRARY spirv-cross-c-shared IMPORTED_LOCATION)
|
||||
get_filename_component(SPIRV_CROSS_LIBRARY_DIRECTORY ${SPIRV_CROSS_LIBRARY} DIRECTORY)
|
||||
list(APPEND CMAKE_BUILD_RPATH ${SPIRV_CROSS_LIBRARY_DIRECTORY})
|
||||
endif()
|
||||
endif()
|
||||
|
||||
if(LINUX)
|
||||
find_package(UDEV REQUIRED)
|
||||
endif()
|
||||
|
||||
if(NOT WIN32 AND NOT APPLE)
|
||||
find_package(Libbacktrace REQUIRED)
|
||||
endif()
|
||||
|
||||
if(APPLE)
|
||||
set(CMAKE_FIND_FRAMEWORK ${FIND_FRAMEWORK_BACKUP})
|
||||
endif()
|
||||
@@ -1,196 +0,0 @@
|
||||
function(disable_compiler_warnings_for_target target)
|
||||
if(MSVC)
|
||||
target_compile_options(${target} PRIVATE "/W0")
|
||||
else()
|
||||
target_compile_options(${target} PRIVATE "-w")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(detect_operating_system)
|
||||
message(STATUS "CMake Version: ${CMAKE_VERSION}")
|
||||
message(STATUS "CMake System Name: ${CMAKE_SYSTEM_NAME}")
|
||||
|
||||
# LINUX wasn't added until CMake 3.25.
|
||||
if (CMAKE_VERSION VERSION_LESS 3.25.0 AND CMAKE_SYSTEM_NAME MATCHES "Linux")
|
||||
# Have to make it visible in this scope as well for below.
|
||||
set(LINUX TRUE PARENT_SCOPE)
|
||||
set(LINUX TRUE)
|
||||
endif()
|
||||
|
||||
if(WIN32)
|
||||
message(STATUS "Building for Windows.")
|
||||
elseif(APPLE AND NOT IOS)
|
||||
message(STATUS "Building for MacOS.")
|
||||
elseif(LINUX)
|
||||
message(STATUS "Building for Linux.")
|
||||
elseif(BSD)
|
||||
message(STATUS "Building for *BSD.")
|
||||
else()
|
||||
message(FATAL_ERROR "Unsupported platform.")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(detect_compiler)
|
||||
if(MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
|
||||
set(COMPILER_CLANG_CL TRUE PARENT_SCOPE)
|
||||
set(IS_SUPPORTED_COMPILER TRUE PARENT_SCOPE)
|
||||
message(STATUS "Building with Clang-CL.")
|
||||
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "AppleClang")
|
||||
set(COMPILER_CLANG TRUE PARENT_SCOPE)
|
||||
set(IS_SUPPORTED_COMPILER TRUE PARENT_SCOPE)
|
||||
message(STATUS "Building with Clang/LLVM.")
|
||||
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
|
||||
set(COMPILER_GCC TRUE PARENT_SCOPE)
|
||||
set(IS_SUPPORTED_COMPILER FALSE PARENT_SCOPE)
|
||||
message(STATUS "Building with GNU GCC.")
|
||||
elseif(MSVC)
|
||||
set(IS_SUPPORTED_COMPILER TRUE PARENT_SCOPE)
|
||||
message(STATUS "Building with MSVC.")
|
||||
else()
|
||||
message(FATAL_ERROR "Unknown compiler: ${CMAKE_CXX_COMPILER_ID}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(detect_architecture)
|
||||
if(APPLE AND NOT "${CMAKE_OSX_ARCHITECTURES}" STREQUAL "")
|
||||
# Universal binaries.
|
||||
if("x86_64" IN_LIST CMAKE_OSX_ARCHITECTURES)
|
||||
message(STATUS "Building x86_64 MacOS binaries.")
|
||||
set(CPU_ARCH_X64 TRUE PARENT_SCOPE)
|
||||
endif()
|
||||
if("arm64" IN_LIST CMAKE_OSX_ARCHITECTURES)
|
||||
message(STATUS "Building ARM64 MacOS binaries.")
|
||||
set(CPU_ARCH_ARM64 TRUE PARENT_SCOPE)
|
||||
endif()
|
||||
elseif(("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "x86_64" OR "${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "amd64" OR
|
||||
"${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "AMD64" OR "${CMAKE_OSX_ARCHITECTURES}" STREQUAL "x86_64") AND
|
||||
CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
message(STATUS "Building x86_64 binaries.")
|
||||
set(CPU_ARCH_X64 TRUE PARENT_SCOPE)
|
||||
elseif(("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "aarch64" OR "${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "arm64") AND
|
||||
CMAKE_SIZEOF_VOID_P EQUAL 8) # Might have an A64 kernel, e.g. Raspbian.
|
||||
message(STATUS "Building ARM64 binaries.")
|
||||
set(CPU_ARCH_ARM64 TRUE PARENT_SCOPE)
|
||||
elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "arm" OR "${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "armv7-a" OR
|
||||
"${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "armv7l" OR
|
||||
(("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "aarch64" OR "${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "arm64")
|
||||
AND CMAKE_SIZEOF_VOID_P EQUAL 4))
|
||||
message(STATUS "Building ARM32 binaries.")
|
||||
set(CPU_ARCH_ARM32 TRUE PARENT_SCOPE)
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -marm -march=armv7-a" PARENT_SCOPE)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -marm -march=armv7-a" PARENT_SCOPE)
|
||||
elseif("${CMAKE_SYSTEM_PROCESSOR}" STREQUAL "riscv64")
|
||||
message(STATUS "Building RISC-V 64 binaries.")
|
||||
set(CPU_ARCH_RISCV64 TRUE PARENT_SCOPE)
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -finline-atomics" PARENT_SCOPE)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -finline-atomics" PARENT_SCOPE)
|
||||
|
||||
# Still need this, apparently.
|
||||
link_libraries("-latomic")
|
||||
|
||||
if(NOT "${CMAKE_BUILD_TYPE}" STREQUAL "Debug")
|
||||
# Frame pointers generate an annoying amount of code on leaf functions.
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fomit-frame-pointer" PARENT_SCOPE)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fomit-frame-pointer" PARENT_SCOPE)
|
||||
endif()
|
||||
else()
|
||||
message(FATAL_ERROR "Unknown system processor: ${CMAKE_SYSTEM_PROCESSOR}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(detect_page_size)
|
||||
# This is only needed for ARM64, or if the user hasn't overridden it explicitly.
|
||||
if(NOT CPU_ARCH_ARM64 OR HOST_PAGE_SIZE)
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(NOT LINUX)
|
||||
# For universal Apple builds, we use preprocessor macros to determine page size.
|
||||
# Similar for Windows, except it's always 4KB.
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(CMAKE_CROSSCOMPILING)
|
||||
message(WARNING "Cross-compiling and can't determine page size, assuming default.")
|
||||
return()
|
||||
endif()
|
||||
|
||||
message(STATUS "Determining host page size")
|
||||
set(detect_page_size_file ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/src.c)
|
||||
file(WRITE ${detect_page_size_file} "
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
int main() {
|
||||
int res = sysconf(_SC_PAGESIZE);
|
||||
printf(\"%d\", res);
|
||||
return (res > 0) ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}")
|
||||
try_run(
|
||||
detect_page_size_run_result
|
||||
detect_page_size_compile_result
|
||||
${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}
|
||||
${detect_page_size_file}
|
||||
RUN_OUTPUT_VARIABLE detect_page_size_output)
|
||||
if(NOT detect_page_size_compile_result OR NOT detect_page_size_run_result EQUAL 0)
|
||||
message(FATAL_ERROR "Could not determine host page size.")
|
||||
else()
|
||||
message(STATUS "Host page size: ${detect_page_size_output}")
|
||||
set(HOST_PAGE_SIZE ${detect_page_size_output} CACHE STRING "Reported host page size")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(detect_cache_line_size)
|
||||
# This is only needed for ARM64, or if the user hasn't overridden it explicitly.
|
||||
if(NOT CPU_ARCH_ARM64 OR HOST_CACHE_LINE_SIZE)
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(NOT LINUX)
|
||||
# For universal Apple builds, we use preprocessor macros to determine page size.
|
||||
# Similar for Windows, except it's always 64 bytes.
|
||||
return()
|
||||
endif()
|
||||
|
||||
if(CMAKE_CROSSCOMPILING)
|
||||
message(WARNING "Cross-compiling and can't determine page size, assuming default.")
|
||||
return()
|
||||
endif()
|
||||
|
||||
message(STATUS "Determining host cache line size")
|
||||
set(detect_cache_line_size_file ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeTmp/src.c)
|
||||
file(WRITE ${detect_cache_line_size_file} "
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
int main() {
|
||||
int l1i = sysconf(_SC_LEVEL1_DCACHE_LINESIZE);
|
||||
int l1d = sysconf(_SC_LEVEL1_ICACHE_LINESIZE);
|
||||
int res = (l1i > l1d) ? l1i : l1d;
|
||||
for (int index = 0; index < 16; index++) {
|
||||
char buf[128];
|
||||
snprintf(buf, sizeof(buf), \"/sys/devices/system/cpu/cpu0/cache/index%d/coherency_line_size\", index);
|
||||
FILE* fp = fopen(buf, \"rb\");
|
||||
if (!fp)
|
||||
break;
|
||||
fread(buf, sizeof(buf), 1, fp);
|
||||
fclose(fp);
|
||||
int val = atoi(buf);
|
||||
res = (val > res) ? val : res;
|
||||
}
|
||||
printf(\"%d\", res);
|
||||
return (res > 0) ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}")
|
||||
try_run(
|
||||
detect_cache_line_size_run_result
|
||||
detect_cache_line_size_compile_result
|
||||
${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}
|
||||
${detect_cache_line_size_file}
|
||||
RUN_OUTPUT_VARIABLE detect_cache_line_size_output)
|
||||
if(NOT detect_cache_line_size_compile_result OR NOT detect_cache_line_size_run_result EQUAL 0)
|
||||
message(FATAL_ERROR "Could not determine host cache line size.")
|
||||
else()
|
||||
message(STATUS "Host cache line size: ${detect_cache_line_size_output}")
|
||||
set(HOST_CACHE_LINE_SIZE ${detect_cache_line_size_output} CACHE STRING "Reported host cache line size")
|
||||
endif()
|
||||
endfunction()
|
||||
172
CMakeModules/FindEGL.cmake
Normal file
172
CMakeModules/FindEGL.cmake
Normal file
@@ -0,0 +1,172 @@
|
||||
#.rst:
|
||||
# FindEGL
|
||||
# -------
|
||||
#
|
||||
# Try to find EGL.
|
||||
#
|
||||
# This will define the following variables:
|
||||
#
|
||||
# ``EGL_FOUND``
|
||||
# True if (the requested version of) EGL is available
|
||||
# ``EGL_VERSION``
|
||||
# The version of EGL; note that this is the API version defined in the
|
||||
# headers, rather than the version of the implementation (eg: Mesa)
|
||||
# ``EGL_LIBRARIES``
|
||||
# This can be passed to target_link_libraries() instead of the ``EGL::EGL``
|
||||
# target
|
||||
# ``EGL_INCLUDE_DIRS``
|
||||
# This should be passed to target_include_directories() if the target is not
|
||||
# used for linking
|
||||
# ``EGL_DEFINITIONS``
|
||||
# This should be passed to target_compile_options() if the target is not
|
||||
# used for linking
|
||||
#
|
||||
# If ``EGL_FOUND`` is TRUE, it will also define the following imported target:
|
||||
#
|
||||
# ``EGL::EGL``
|
||||
# The EGL library
|
||||
#
|
||||
# In general we recommend using the imported target, as it is easier to use.
|
||||
# Bear in mind, however, that if the target is in the link interface of an
|
||||
# exported library, it must be made available by the package config file.
|
||||
#
|
||||
# Since pre-1.0.0.
|
||||
|
||||
#=============================================================================
|
||||
# Copyright 2014 Alex Merry <alex.merry@kde.org>
|
||||
# Copyright 2014 Martin Gräßlin <mgraesslin@kde.org>
|
||||
#
|
||||
# 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 copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the copyright
|
||||
# notice, this list of conditions and the following disclaimer in the
|
||||
# documentation and/or other materials provided with the distribution.
|
||||
# 3. The name of the author may not be used to endorse or promote products
|
||||
# derived from this software without specific prior written permission.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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(${CMAKE_CURRENT_LIST_DIR}/ECMFindModuleHelpersStub.cmake)
|
||||
include(CheckCXXSourceCompiles)
|
||||
include(CMakePushCheckState)
|
||||
|
||||
ecm_find_package_version_check(EGL)
|
||||
|
||||
# Use pkg-config to get the directories and then use these values
|
||||
# in the FIND_PATH() and FIND_LIBRARY() calls
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(PKG_EGL QUIET egl)
|
||||
|
||||
set(EGL_DEFINITIONS ${PKG_EGL_CFLAGS_OTHER})
|
||||
|
||||
find_path(EGL_INCLUDE_DIR
|
||||
NAMES
|
||||
EGL/egl.h
|
||||
HINTS
|
||||
${PKG_EGL_INCLUDE_DIRS}
|
||||
)
|
||||
find_library(EGL_LIBRARY
|
||||
NAMES
|
||||
EGL
|
||||
HINTS
|
||||
${PKG_EGL_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
# NB: We do *not* use the version information from pkg-config, as that
|
||||
# is the implementation version (eg: the Mesa version)
|
||||
if(EGL_INCLUDE_DIR)
|
||||
# egl.h has defines of the form EGL_VERSION_x_y for each supported
|
||||
# version; so the header for EGL 1.1 will define EGL_VERSION_1_0 and
|
||||
# EGL_VERSION_1_1. Finding the highest supported version involves
|
||||
# finding all these defines and selecting the highest numbered.
|
||||
file(READ "${EGL_INCLUDE_DIR}/EGL/egl.h" _EGL_header_contents)
|
||||
string(REGEX MATCHALL
|
||||
"[ \t]EGL_VERSION_[0-9_]+"
|
||||
_EGL_version_lines
|
||||
"${_EGL_header_contents}"
|
||||
)
|
||||
unset(_EGL_header_contents)
|
||||
foreach(_EGL_version_line ${_EGL_version_lines})
|
||||
string(REGEX REPLACE
|
||||
"[ \t]EGL_VERSION_([0-9_]+)"
|
||||
"\\1"
|
||||
_version_candidate
|
||||
"${_EGL_version_line}"
|
||||
)
|
||||
string(REPLACE "_" "." _version_candidate "${_version_candidate}")
|
||||
if(NOT DEFINED EGL_VERSION OR EGL_VERSION VERSION_LESS _version_candidate)
|
||||
set(EGL_VERSION "${_version_candidate}")
|
||||
endif()
|
||||
endforeach()
|
||||
unset(_EGL_version_lines)
|
||||
endif()
|
||||
|
||||
cmake_push_check_state(RESET)
|
||||
list(APPEND CMAKE_REQUIRED_LIBRARIES "${EGL_LIBRARY}")
|
||||
list(APPEND CMAKE_REQUIRED_INCLUDES "${EGL_INCLUDE_DIR}")
|
||||
|
||||
check_cxx_source_compiles("
|
||||
#include <EGL/egl.h>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
EGLint x = 0; EGLDisplay dpy = 0; EGLContext ctx = 0;
|
||||
eglDestroyContext(dpy, ctx);
|
||||
}" HAVE_EGL)
|
||||
|
||||
cmake_pop_check_state()
|
||||
|
||||
set(required_vars EGL_INCLUDE_DIR HAVE_EGL)
|
||||
if(NOT EMSCRIPTEN)
|
||||
list(APPEND required_vars EGL_LIBRARY)
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(EGL
|
||||
FOUND_VAR
|
||||
EGL_FOUND
|
||||
REQUIRED_VARS
|
||||
${required_vars}
|
||||
VERSION_VAR
|
||||
EGL_VERSION
|
||||
)
|
||||
|
||||
if(EGL_FOUND AND NOT TARGET EGL::EGL)
|
||||
if (EMSCRIPTEN)
|
||||
add_library(EGL::EGL INTERFACE IMPORTED)
|
||||
# Nothing further to be done, system include paths have headers and linkage is implicit.
|
||||
else()
|
||||
add_library(EGL::EGL UNKNOWN IMPORTED)
|
||||
set_target_properties(EGL::EGL PROPERTIES
|
||||
IMPORTED_LOCATION "${EGL_LIBRARY}"
|
||||
INTERFACE_COMPILE_OPTIONS "${EGL_DEFINITIONS}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${EGL_INCLUDE_DIR}"
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
mark_as_advanced(EGL_LIBRARY EGL_INCLUDE_DIR HAVE_EGL)
|
||||
|
||||
# compatibility variables
|
||||
set(EGL_LIBRARIES ${EGL_LIBRARY})
|
||||
set(EGL_INCLUDE_DIRS ${EGL_INCLUDE_DIR})
|
||||
set(EGL_VERSION_STRING ${EGL_VERSION})
|
||||
|
||||
include(FeatureSummary)
|
||||
set_package_properties(EGL PROPERTIES
|
||||
URL "https://www.khronos.org/egl/"
|
||||
DESCRIPTION "A platform-agnostic mechanism for creating rendering surfaces for use with other graphics libraries, such as OpenGL|ES and OpenVG."
|
||||
)
|
||||
70
CMakeModules/FindGBM.cmake
Normal file
70
CMakeModules/FindGBM.cmake
Normal file
@@ -0,0 +1,70 @@
|
||||
# https://fossies.org/linux/misc/xbmc-18.9-Leia.tar.gz/xbmc-18.9-Leia/cmake/modules/FindGBM.cmake?m=t
|
||||
|
||||
# FindGBM
|
||||
# ----------
|
||||
# Finds the GBM library
|
||||
#
|
||||
# This will define the following variables::
|
||||
#
|
||||
# GBM_FOUND - system has GBM
|
||||
# GBM_INCLUDE_DIRS - the GBM include directory
|
||||
# GBM_LIBRARIES - the GBM libraries
|
||||
# GBM_DEFINITIONS - the GBM definitions
|
||||
#
|
||||
# and the following imported targets::
|
||||
#
|
||||
# GBM::GBM - The GBM library
|
||||
|
||||
if(PKG_CONFIG_FOUND)
|
||||
pkg_check_modules(PC_GBM gbm QUIET)
|
||||
endif()
|
||||
|
||||
find_path(GBM_INCLUDE_DIR NAMES gbm.h
|
||||
PATHS ${PC_GBM_INCLUDEDIR})
|
||||
find_library(GBM_LIBRARY NAMES gbm
|
||||
PATHS ${PC_GBM_LIBDIR})
|
||||
|
||||
set(GBM_VERSION ${PC_GBM_VERSION})
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(GBM
|
||||
REQUIRED_VARS GBM_LIBRARY GBM_INCLUDE_DIR
|
||||
VERSION_VAR GBM_VERSION)
|
||||
|
||||
include(CheckCSourceCompiles)
|
||||
set(CMAKE_REQUIRED_LIBRARIES ${GBM_LIBRARY})
|
||||
check_c_source_compiles("#include <gbm.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
gbm_bo_map(NULL, 0, 0, 0, 0, GBM_BO_TRANSFER_WRITE, NULL, NULL);
|
||||
}
|
||||
" GBM_HAS_BO_MAP)
|
||||
|
||||
check_c_source_compiles("#include <gbm.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
gbm_surface_create_with_modifiers(NULL, 0, 0, 0, NULL, 0);
|
||||
}
|
||||
" GBM_HAS_MODIFIERS)
|
||||
|
||||
if(GBM_FOUND)
|
||||
set(GBM_LIBRARIES ${GBM_LIBRARY})
|
||||
set(GBM_INCLUDE_DIRS ${GBM_INCLUDE_DIR})
|
||||
set(GBM_DEFINITIONS -DHAVE_GBM=1)
|
||||
if(GBM_HAS_BO_MAP)
|
||||
list(APPEND GBM_DEFINITIONS -DHAS_GBM_BO_MAP=1)
|
||||
endif()
|
||||
if(GBM_HAS_MODIFIERS)
|
||||
list(APPEND GBM_DEFINITIONS -DHAS_GBM_MODIFIERS=1)
|
||||
endif()
|
||||
if(NOT TARGET GBM::GBM)
|
||||
add_library(GBM::GBM UNKNOWN IMPORTED)
|
||||
set_target_properties(GBM::GBM PROPERTIES
|
||||
IMPORTED_LOCATION "${GBM_LIBRARY}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${GBM_INCLUDE_DIR}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
mark_as_advanced(GBM_INCLUDE_DIR GBM_LIBRARY)
|
||||
33
CMakeModules/FindLIBEVDEV.cmake
Normal file
33
CMakeModules/FindLIBEVDEV.cmake
Normal file
@@ -0,0 +1,33 @@
|
||||
# - Try to find libevdev
|
||||
# Once done this will define
|
||||
# LIBEVDEV_FOUND - System has libevdev
|
||||
# LIBEVDEV_INCLUDE_DIRS - The libevdev include directories
|
||||
# LIBEVDEV_LIBRARIES - The libraries needed to use libevdev
|
||||
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(PC_LIBEVDEV QUIET libevdev)
|
||||
|
||||
FIND_PATH(
|
||||
LIBEVDEV_INCLUDE_DIR libevdev/libevdev.h
|
||||
HINTS ${PC_LIBEVDEV_INCLUDEDIR} ${PC_LIBEVDEV_INCLUDE_DIRS}
|
||||
/usr/include
|
||||
/usr/local/include
|
||||
${LIBEVDEV_PATH_INCLUDES}
|
||||
)
|
||||
|
||||
FIND_LIBRARY(
|
||||
LIBEVDEV_LIBRARY
|
||||
NAMES evdev libevdev
|
||||
HINTS ${PC_LIBEVDEV_LIBDIR} ${PC_LIBEVDEV_LIBRARY_DIRS}
|
||||
PATHS ${ADDITIONAL_LIBRARY_PATHS}
|
||||
${LIBEVDEV_PATH_LIB}
|
||||
)
|
||||
|
||||
set(LIBEVDEV_LIBRARIES ${LIBEVDEV_LIBRARY} )
|
||||
set(LIBEVDEV_INCLUDE_DIRS ${LIBEVDEV_INCLUDE_DIR} )
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(LIBEVDEV DEFAULT_MSG
|
||||
LIBEVDEV_LIBRARY LIBEVDEV_INCLUDE_DIR)
|
||||
|
||||
mark_as_advanced(LIBEVDEV_INCLUDE_DIR LIBEVDEV_LIBRARY )
|
||||
@@ -1,31 +0,0 @@
|
||||
# - Try to find libbacktrace
|
||||
# Once done this will define
|
||||
# LIBBACKTRACE_FOUND - System has libbacktrace
|
||||
# LIBBACKTRACE_INCLUDE_DIRS - The libbacktrace include directories
|
||||
# LIBBACKTRACE_LIBRARIES - The libraries needed to use libbacktrace
|
||||
|
||||
FIND_PATH(
|
||||
LIBBACKTRACE_INCLUDE_DIR backtrace.h
|
||||
HINTS /usr/include /usr/local/include
|
||||
${LIBBACKTRACE_PATH_INCLUDES}
|
||||
)
|
||||
|
||||
FIND_LIBRARY(
|
||||
LIBBACKTRACE_LIBRARY
|
||||
NAMES backtrace
|
||||
PATHS ${ADDITIONAL_LIBRARY_PATHS} ${LIBBACKTRACE_PATH_LIB}
|
||||
)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(Libbacktrace DEFAULT_MSG
|
||||
LIBBACKTRACE_LIBRARY LIBBACKTRACE_INCLUDE_DIR)
|
||||
|
||||
if(LIBBACKTRACE_FOUND)
|
||||
add_library(libbacktrace::libbacktrace UNKNOWN IMPORTED)
|
||||
set_target_properties(libbacktrace::libbacktrace PROPERTIES
|
||||
IMPORTED_LOCATION ${LIBBACKTRACE_LIBRARY}
|
||||
INTERFACE_INCLUDE_DIRECTORIES ${LIBBACKTRACE_INCLUDE_DIR}
|
||||
)
|
||||
endif()
|
||||
|
||||
mark_as_advanced(LIBBACKTRACE_INCLUDE_DIR LIBBACKTRACE_LIBRARY)
|
||||
107
CMakeModules/FindLibdrm.cmake
Normal file
107
CMakeModules/FindLibdrm.cmake
Normal file
@@ -0,0 +1,107 @@
|
||||
# https://raw.githubusercontent.com/KDE/kwin/master/cmake/modules/FindLibdrm.cmake
|
||||
|
||||
#.rst:
|
||||
# FindLibdrm
|
||||
# -------
|
||||
#
|
||||
# Try to find libdrm on a Unix system.
|
||||
#
|
||||
# This will define the following variables:
|
||||
#
|
||||
# ``Libdrm_FOUND``
|
||||
# True if (the requested version of) libdrm is available
|
||||
# ``Libdrm_VERSION``
|
||||
# The version of libdrm
|
||||
# ``Libdrm_LIBRARIES``
|
||||
# This can be passed to target_link_libraries() instead of the ``Libdrm::Libdrm``
|
||||
# target
|
||||
# ``Libdrm_INCLUDE_DIRS``
|
||||
# This should be passed to target_include_directories() if the target is not
|
||||
# used for linking
|
||||
# ``Libdrm_DEFINITIONS``
|
||||
# This should be passed to target_compile_options() if the target is not
|
||||
# used for linking
|
||||
#
|
||||
# If ``Libdrm_FOUND`` is TRUE, it will also define the following imported target:
|
||||
#
|
||||
# ``Libdrm::Libdrm``
|
||||
# The libdrm library
|
||||
#
|
||||
# In general we recommend using the imported target, as it is easier to use.
|
||||
# Bear in mind, however, that if the target is in the link interface of an
|
||||
# exported library, it must be made available by the package config file.
|
||||
|
||||
#=============================================================================
|
||||
# SPDX-FileCopyrightText: 2014 Alex Merry <alex.merry@kde.org>
|
||||
# SPDX-FileCopyrightText: 2014 Martin Gräßlin <mgraesslin@kde.org>
|
||||
#
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
#=============================================================================
|
||||
|
||||
if(CMAKE_VERSION VERSION_LESS 2.8.12)
|
||||
message(FATAL_ERROR "CMake 2.8.12 is required by FindLibdrm.cmake")
|
||||
endif()
|
||||
if(CMAKE_MINIMUM_REQUIRED_VERSION VERSION_LESS 2.8.12)
|
||||
message(AUTHOR_WARNING "Your project should require at least CMake 2.8.12 to use FindLibdrm.cmake")
|
||||
endif()
|
||||
|
||||
if(NOT WIN32)
|
||||
# Use pkg-config to get the directories and then use these values
|
||||
# in the FIND_PATH() and FIND_LIBRARY() calls
|
||||
find_package(PkgConfig)
|
||||
pkg_check_modules(PKG_Libdrm QUIET libdrm)
|
||||
|
||||
set(Libdrm_DEFINITIONS ${PKG_Libdrm_CFLAGS_OTHER})
|
||||
set(Libdrm_VERSION ${PKG_Libdrm_VERSION})
|
||||
|
||||
find_path(Libdrm_INCLUDE_DIR
|
||||
NAMES
|
||||
xf86drm.h
|
||||
HINTS
|
||||
${PKG_Libdrm_INCLUDE_DIRS}
|
||||
)
|
||||
find_library(Libdrm_LIBRARY
|
||||
NAMES
|
||||
drm
|
||||
HINTS
|
||||
${PKG_Libdrm_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(Libdrm
|
||||
FOUND_VAR
|
||||
Libdrm_FOUND
|
||||
REQUIRED_VARS
|
||||
Libdrm_LIBRARY
|
||||
Libdrm_INCLUDE_DIR
|
||||
VERSION_VAR
|
||||
Libdrm_VERSION
|
||||
)
|
||||
|
||||
if(Libdrm_FOUND AND NOT TARGET Libdrm::Libdrm)
|
||||
add_library(Libdrm::Libdrm UNKNOWN IMPORTED)
|
||||
set_target_properties(Libdrm::Libdrm PROPERTIES
|
||||
IMPORTED_LOCATION "${Libdrm_LIBRARY}"
|
||||
INTERFACE_COMPILE_OPTIONS "${Libdrm_DEFINITIONS}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${Libdrm_INCLUDE_DIR}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${Libdrm_INCLUDE_DIR}/libdrm"
|
||||
)
|
||||
endif()
|
||||
|
||||
mark_as_advanced(Libdrm_LIBRARY Libdrm_INCLUDE_DIR)
|
||||
|
||||
# compatibility variables
|
||||
set(Libdrm_LIBRARIES ${Libdrm_LIBRARY})
|
||||
set(Libdrm_INCLUDE_DIRS ${Libdrm_INCLUDE_DIR} "${Libdrm_INCLUDE_DIR}/libdrm")
|
||||
set(Libdrm_VERSION_STRING ${Libdrm_VERSION})
|
||||
|
||||
else()
|
||||
message(STATUS "FindLibdrm.cmake cannot find libdrm on Windows systems.")
|
||||
set(Libdrm_FOUND FALSE)
|
||||
endif()
|
||||
|
||||
include(FeatureSummary)
|
||||
set_package_properties(Libdrm PROPERTIES
|
||||
URL "https://wiki.freedesktop.org/dri/"
|
||||
DESCRIPTION "Userspace interface to kernel DRM services."
|
||||
)
|
||||
389
CMakeModules/FindSDL2.cmake
Normal file
389
CMakeModules/FindSDL2.cmake
Normal file
@@ -0,0 +1,389 @@
|
||||
# Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
||||
# file Copyright.txt or https://cmake.org/licensing for details.
|
||||
# Sourced from https://raw.githubusercontent.com/aminosbh/sdl2-cmake-modules/master/FindSDL2.cmake
|
||||
|
||||
# Copyright 2019 Amine Ben Hassouna <amine.benhassouna@gmail.com>
|
||||
# Copyright 2000-2019 Kitware, Inc. and Contributors
|
||||
# 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 Kitware, Inc. nor the names of 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
|
||||
# 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.
|
||||
|
||||
#[=======================================================================[.rst:
|
||||
FindSDL2
|
||||
--------
|
||||
|
||||
Locate SDL2 library
|
||||
|
||||
This module defines the following 'IMPORTED' targets:
|
||||
|
||||
::
|
||||
|
||||
SDL2::Core
|
||||
The SDL2 library, if found.
|
||||
Libraries should link to SDL2::Core
|
||||
|
||||
SDL2::Main
|
||||
The SDL2main library, if found.
|
||||
Applications should link to SDL2::Main instead of SDL2::Core
|
||||
|
||||
|
||||
|
||||
This module will set the following variables in your project:
|
||||
|
||||
::
|
||||
|
||||
SDL2_LIBRARIES, the name of the library to link against
|
||||
SDL2_INCLUDE_DIRS, where to find SDL.h
|
||||
SDL2_FOUND, if false, do not try to link to SDL2
|
||||
SDL2MAIN_FOUND, if false, do not try to link to SDL2main
|
||||
SDL2_VERSION_STRING, human-readable string containing the version of SDL2
|
||||
|
||||
|
||||
|
||||
This module responds to the following cache variables:
|
||||
|
||||
::
|
||||
|
||||
SDL2_PATH
|
||||
Set a custom SDL2 Library path (default: empty)
|
||||
|
||||
SDL2_NO_DEFAULT_PATH
|
||||
Disable search SDL2 Library in default path.
|
||||
If SDL2_PATH (default: ON)
|
||||
Else (default: OFF)
|
||||
|
||||
SDL2_INCLUDE_DIR
|
||||
SDL2 headers path.
|
||||
|
||||
SDL2_LIBRARY
|
||||
SDL2 Library (.dll, .so, .a, etc) path.
|
||||
|
||||
SDL2MAIN_LIBRAY
|
||||
SDL2main Library (.a) path.
|
||||
|
||||
SDL2_BUILDING_LIBRARY
|
||||
This flag is useful only when linking to SDL2_LIBRARIES insead of
|
||||
SDL2::Main. It is required only when building a library that links to
|
||||
SDL2_LIBRARIES, because only applications need main() (No need to also
|
||||
link to SDL2main).
|
||||
If this flag is defined, then no SDL2main will be added to SDL2_LIBRARIES
|
||||
and no SDL2::Main target will be created.
|
||||
|
||||
|
||||
Don't forget to include SDLmain.h and SDLmain.m in your project for the
|
||||
OS X framework based version. (Other versions link to -lSDL2main which
|
||||
this module will try to find on your behalf.) Also for OS X, this
|
||||
module will automatically add the -framework Cocoa on your behalf.
|
||||
|
||||
|
||||
Additional Note: If you see an empty SDL2_LIBRARY in your project
|
||||
configuration, it means CMake did not find your SDL2 library
|
||||
(SDL2.dll, libsdl2.so, SDL2.framework, etc). Set SDL2_LIBRARY to point
|
||||
to your SDL2 library, and configure again. Similarly, if you see an
|
||||
empty SDL2MAIN_LIBRARY, you should set this value as appropriate. These
|
||||
values are used to generate the final SDL2_LIBRARIES variable and the
|
||||
SDL2::Core and SDL2::Main targets, but when these values are unset,
|
||||
SDL2_LIBRARIES, SDL2::Core and SDL2::Main does not get created.
|
||||
|
||||
|
||||
$SDL2DIR is an environment variable that would correspond to the
|
||||
./configure --prefix=$SDL2DIR used in building SDL2. l.e.galup 9-20-02
|
||||
|
||||
|
||||
|
||||
Created by Amine Ben Hassouna:
|
||||
Adapt FindSDL.cmake to SDL2 (FindSDL2.cmake).
|
||||
Add cache variables for more flexibility:
|
||||
SDL2_PATH, SDL2_NO_DEFAULT_PATH (for details, see doc above).
|
||||
Mark 'Threads' as a required dependency for non-OSX systems.
|
||||
Modernize the FindSDL2.cmake module by creating specific targets:
|
||||
SDL2::Core and SDL2::Main (for details, see doc above).
|
||||
|
||||
|
||||
Original FindSDL.cmake module:
|
||||
Modified by Eric Wing. Added code to assist with automated building
|
||||
by using environmental variables and providing a more
|
||||
controlled/consistent search behavior. Added new modifications to
|
||||
recognize OS X frameworks and additional Unix paths (FreeBSD, etc).
|
||||
Also corrected the header search path to follow "proper" SDL
|
||||
guidelines. Added a search for SDLmain which is needed by some
|
||||
platforms. Added a search for threads which is needed by some
|
||||
platforms. Added needed compile switches for MinGW.
|
||||
|
||||
On OSX, this will prefer the Framework version (if found) over others.
|
||||
People will have to manually change the cache value of SDL2_LIBRARY to
|
||||
override this selection or set the SDL2_PATH variable or the CMake
|
||||
environment CMAKE_INCLUDE_PATH to modify the search paths.
|
||||
|
||||
Note that the header path has changed from SDL/SDL.h to just SDL.h
|
||||
This needed to change because "proper" SDL convention is #include
|
||||
"SDL.h", not <SDL/SDL.h>. This is done for portability reasons
|
||||
because not all systems place things in SDL/ (see FreeBSD).
|
||||
#]=======================================================================]
|
||||
|
||||
# Define options for searching SDL2 Library in a custom path
|
||||
|
||||
set(SDL2_PATH "" CACHE STRING "Custom SDL2 Library path")
|
||||
|
||||
set(_SDL2_NO_DEFAULT_PATH OFF)
|
||||
if(SDL2_PATH)
|
||||
set(_SDL2_NO_DEFAULT_PATH ON)
|
||||
endif()
|
||||
|
||||
set(SDL2_NO_DEFAULT_PATH ${_SDL2_NO_DEFAULT_PATH}
|
||||
CACHE BOOL "Disable search SDL2 Library in default path")
|
||||
unset(_SDL2_NO_DEFAULT_PATH)
|
||||
|
||||
set(SDL2_NO_DEFAULT_PATH_CMD)
|
||||
if(SDL2_NO_DEFAULT_PATH)
|
||||
set(SDL2_NO_DEFAULT_PATH_CMD NO_DEFAULT_PATH)
|
||||
endif()
|
||||
|
||||
# Search for the SDL2 include directory
|
||||
find_path(SDL2_INCLUDE_DIR SDL.h
|
||||
HINTS
|
||||
ENV SDL2DIR
|
||||
${SDL2_NO_DEFAULT_PATH_CMD}
|
||||
PATH_SUFFIXES SDL2
|
||||
# path suffixes to search inside ENV{SDL2DIR}
|
||||
include/SDL2 include
|
||||
PATHS ${SDL2_PATH}
|
||||
DOC "Where the SDL2 headers can be found"
|
||||
)
|
||||
|
||||
set(SDL2_INCLUDE_DIRS "${SDL2_INCLUDE_DIR}")
|
||||
|
||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||
set(VC_LIB_PATH_SUFFIX lib/x64)
|
||||
else()
|
||||
set(VC_LIB_PATH_SUFFIX lib/x86)
|
||||
endif()
|
||||
|
||||
# SDL-2.0 is the name used by FreeBSD ports...
|
||||
# don't confuse it for the version number.
|
||||
find_library(SDL2_LIBRARY
|
||||
NAMES SDL2 SDL-2.0
|
||||
HINTS
|
||||
ENV SDL2DIR
|
||||
${SDL2_NO_DEFAULT_PATH_CMD}
|
||||
PATH_SUFFIXES lib ${VC_LIB_PATH_SUFFIX}
|
||||
PATHS ${SDL2_PATH}
|
||||
DOC "Where the SDL2 Library can be found"
|
||||
)
|
||||
|
||||
set(SDL2_LIBRARIES "${SDL2_LIBRARY}")
|
||||
|
||||
if(NOT SDL2_BUILDING_LIBRARY)
|
||||
if(NOT SDL2_INCLUDE_DIR MATCHES ".framework")
|
||||
# Non-OS X framework versions expect you to also dynamically link to
|
||||
# SDL2main. This is mainly for Windows and OS X. Other (Unix) platforms
|
||||
# seem to provide SDL2main for compatibility even though they don't
|
||||
# necessarily need it.
|
||||
|
||||
if(SDL2_PATH)
|
||||
set(SDL2MAIN_LIBRARY_PATHS "${SDL2_PATH}")
|
||||
endif()
|
||||
|
||||
if(NOT SDL2_NO_DEFAULT_PATH)
|
||||
set(SDL2MAIN_LIBRARY_PATHS
|
||||
/sw
|
||||
/opt/local
|
||||
/opt/csw
|
||||
/opt
|
||||
"${SDL2MAIN_LIBRARY_PATHS}"
|
||||
)
|
||||
endif()
|
||||
|
||||
find_library(SDL2MAIN_LIBRARY
|
||||
NAMES SDL2main
|
||||
HINTS
|
||||
ENV SDL2DIR
|
||||
${SDL2_NO_DEFAULT_PATH_CMD}
|
||||
PATH_SUFFIXES lib ${VC_LIB_PATH_SUFFIX}
|
||||
PATHS ${SDL2MAIN_LIBRARY_PATHS}
|
||||
DOC "Where the SDL2main library can be found"
|
||||
)
|
||||
unset(SDL2MAIN_LIBRARY_PATHS)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# SDL2 may require threads on your system.
|
||||
# The Apple build may not need an explicit flag because one of the
|
||||
# frameworks may already provide it.
|
||||
# But for non-OSX systems, I will use the CMake Threads package.
|
||||
if(NOT APPLE)
|
||||
find_package(Threads QUIET)
|
||||
if(NOT Threads_FOUND AND NOT WIN32)
|
||||
set(SDL2_THREADS_NOT_FOUND "Could NOT find Threads (Threads is required by SDL2).")
|
||||
if(SDL2_FIND_REQUIRED)
|
||||
message(FATAL_ERROR ${SDL2_THREADS_NOT_FOUND})
|
||||
else()
|
||||
if(NOT SDL2_FIND_QUIETLY)
|
||||
message(STATUS ${SDL2_THREADS_NOT_FOUND})
|
||||
endif()
|
||||
return()
|
||||
endif()
|
||||
unset(SDL2_THREADS_NOT_FOUND)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# MinGW needs an additional link flag, -mwindows
|
||||
# It's total link flags should look like -lmingw32 -lSDL2main -lSDL2 -mwindows
|
||||
if(MINGW)
|
||||
set(MINGW32_LIBRARY mingw32 "-mwindows" CACHE STRING "link flags for MinGW")
|
||||
endif()
|
||||
|
||||
if(SDL2_LIBRARY)
|
||||
# For SDL2main
|
||||
if(SDL2MAIN_LIBRARY AND NOT SDL2_BUILDING_LIBRARY)
|
||||
list(FIND SDL2_LIBRARIES "${SDL2MAIN_LIBRARY}" _SDL2_MAIN_INDEX)
|
||||
if(_SDL2_MAIN_INDEX EQUAL -1)
|
||||
set(SDL2_LIBRARIES "${SDL2MAIN_LIBRARY}" ${SDL2_LIBRARIES})
|
||||
endif()
|
||||
unset(_SDL2_MAIN_INDEX)
|
||||
endif()
|
||||
|
||||
# For OS X, SDL2 uses Cocoa as a backend so it must link to Cocoa.
|
||||
# CMake doesn't display the -framework Cocoa string in the UI even
|
||||
# though it actually is there if I modify a pre-used variable.
|
||||
# I think it has something to do with the CACHE STRING.
|
||||
# So I use a temporary variable until the end so I can set the
|
||||
# "real" variable in one-shot.
|
||||
if(APPLE)
|
||||
set(SDL2_LIBRARIES ${SDL2_LIBRARIES} "-framework Cocoa")
|
||||
endif()
|
||||
|
||||
# For threads, as mentioned Apple doesn't need this.
|
||||
# In fact, there seems to be a problem if I used the Threads package
|
||||
# and try using this line, so I'm just skipping it entirely for OS X.
|
||||
if(NOT APPLE)
|
||||
set(SDL2_LIBRARIES ${SDL2_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT})
|
||||
endif()
|
||||
|
||||
# For MinGW library
|
||||
if(MINGW)
|
||||
set(SDL2_LIBRARIES ${MINGW32_LIBRARY} ${SDL2_LIBRARIES})
|
||||
endif()
|
||||
|
||||
endif()
|
||||
|
||||
# Read SDL2 version
|
||||
if(SDL2_INCLUDE_DIR AND EXISTS "${SDL2_INCLUDE_DIR}/SDL_version.h")
|
||||
file(STRINGS "${SDL2_INCLUDE_DIR}/SDL_version.h" SDL2_VERSION_MAJOR_LINE REGEX "^#define[ \t]+SDL_MAJOR_VERSION[ \t]+[0-9]+$")
|
||||
file(STRINGS "${SDL2_INCLUDE_DIR}/SDL_version.h" SDL2_VERSION_MINOR_LINE REGEX "^#define[ \t]+SDL_MINOR_VERSION[ \t]+[0-9]+$")
|
||||
file(STRINGS "${SDL2_INCLUDE_DIR}/SDL_version.h" SDL2_VERSION_PATCH_LINE REGEX "^#define[ \t]+SDL_PATCHLEVEL[ \t]+[0-9]+$")
|
||||
string(REGEX REPLACE "^#define[ \t]+SDL_MAJOR_VERSION[ \t]+([0-9]+)$" "\\1" SDL2_VERSION_MAJOR "${SDL2_VERSION_MAJOR_LINE}")
|
||||
string(REGEX REPLACE "^#define[ \t]+SDL_MINOR_VERSION[ \t]+([0-9]+)$" "\\1" SDL2_VERSION_MINOR "${SDL2_VERSION_MINOR_LINE}")
|
||||
string(REGEX REPLACE "^#define[ \t]+SDL_PATCHLEVEL[ \t]+([0-9]+)$" "\\1" SDL2_VERSION_PATCH "${SDL2_VERSION_PATCH_LINE}")
|
||||
set(SDL2_VERSION_STRING ${SDL2_VERSION_MAJOR}.${SDL2_VERSION_MINOR}.${SDL2_VERSION_PATCH})
|
||||
unset(SDL2_VERSION_MAJOR_LINE)
|
||||
unset(SDL2_VERSION_MINOR_LINE)
|
||||
unset(SDL2_VERSION_PATCH_LINE)
|
||||
unset(SDL2_VERSION_MAJOR)
|
||||
unset(SDL2_VERSION_MINOR)
|
||||
unset(SDL2_VERSION_PATCH)
|
||||
endif()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
|
||||
FIND_PACKAGE_HANDLE_STANDARD_ARGS(SDL2
|
||||
REQUIRED_VARS SDL2_LIBRARY SDL2_INCLUDE_DIR
|
||||
VERSION_VAR SDL2_VERSION_STRING)
|
||||
|
||||
if(SDL2MAIN_LIBRARY)
|
||||
FIND_PACKAGE_HANDLE_STANDARD_ARGS(SDL2main
|
||||
REQUIRED_VARS SDL2MAIN_LIBRARY SDL2_INCLUDE_DIR
|
||||
VERSION_VAR SDL2_VERSION_STRING)
|
||||
endif()
|
||||
|
||||
|
||||
mark_as_advanced(SDL2_PATH
|
||||
SDL2_NO_DEFAULT_PATH
|
||||
SDL2_LIBRARY
|
||||
SDL2MAIN_LIBRARY
|
||||
SDL2_INCLUDE_DIR
|
||||
SDL2_BUILDING_LIBRARY)
|
||||
|
||||
|
||||
# SDL2:: targets (SDL2::Core and SDL2::Main)
|
||||
if(SDL2_FOUND)
|
||||
|
||||
# SDL2::Core target
|
||||
if(SDL2_LIBRARY AND NOT TARGET SDL2::Core)
|
||||
add_library(SDL2::Core UNKNOWN IMPORTED)
|
||||
set_target_properties(SDL2::Core PROPERTIES
|
||||
IMPORTED_LOCATION "${SDL2_LIBRARY}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${SDL2_INCLUDE_DIR}")
|
||||
|
||||
if(APPLE)
|
||||
# For OS X, SDL2 uses Cocoa as a backend so it must link to Cocoa.
|
||||
# For more details, please see above.
|
||||
set_property(TARGET SDL2::Core APPEND PROPERTY
|
||||
INTERFACE_LINK_OPTIONS "-framework Cocoa")
|
||||
else()
|
||||
# For threads, as mentioned Apple doesn't need this.
|
||||
# For more details, please see above.
|
||||
set_property(TARGET SDL2::Core APPEND PROPERTY
|
||||
INTERFACE_LINK_LIBRARIES Threads::Threads)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# SDL2::Main target
|
||||
# Applications should link to SDL2::Main instead of SDL2::Core
|
||||
# For more details, please see above.
|
||||
if(NOT SDL2_BUILDING_LIBRARY AND NOT TARGET SDL2::Main)
|
||||
|
||||
if(SDL2_INCLUDE_DIR MATCHES ".framework" OR NOT SDL2MAIN_LIBRARY)
|
||||
add_library(SDL2::Main INTERFACE IMPORTED)
|
||||
set_property(TARGET SDL2::Main PROPERTY
|
||||
INTERFACE_LINK_LIBRARIES SDL2::Core)
|
||||
elseif(SDL2MAIN_LIBRARY)
|
||||
# MinGW requires that the mingw32 library is specified before the
|
||||
# libSDL2main.a static library when linking.
|
||||
# The SDL2::MainInternal target is used internally to make sure that
|
||||
# CMake respects this condition.
|
||||
add_library(SDL2::MainInternal UNKNOWN IMPORTED)
|
||||
set_property(TARGET SDL2::MainInternal PROPERTY
|
||||
IMPORTED_LOCATION "${SDL2MAIN_LIBRARY}")
|
||||
set_property(TARGET SDL2::MainInternal PROPERTY
|
||||
INTERFACE_LINK_LIBRARIES SDL2::Core)
|
||||
|
||||
add_library(SDL2::Main INTERFACE IMPORTED)
|
||||
|
||||
if(MINGW)
|
||||
# MinGW needs an additional link flag '-mwindows' and link to mingw32
|
||||
set_property(TARGET SDL2::Main PROPERTY
|
||||
INTERFACE_LINK_LIBRARIES "mingw32" "-mwindows")
|
||||
endif()
|
||||
|
||||
set_property(TARGET SDL2::Main APPEND PROPERTY
|
||||
INTERFACE_LINK_LIBRARIES SDL2::MainInternal)
|
||||
endif()
|
||||
|
||||
endif()
|
||||
endif()
|
||||
@@ -1,31 +0,0 @@
|
||||
# - Try to find SHADERC
|
||||
# Once done this will define
|
||||
# SHADERC_FOUND - System has SHADERC
|
||||
# SHADERC_INCLUDE_DIRS - The SHADERC include directories
|
||||
# SHADERC_LIBRARIES - The libraries needed to use SHADERC
|
||||
|
||||
find_path(
|
||||
SHADERC_INCLUDE_DIR shaderc/shaderc.h
|
||||
${SHADERC_PATH_INCLUDES}
|
||||
)
|
||||
|
||||
find_library(
|
||||
SHADERC_LIBRARY
|
||||
NAMES shaderc_shared.1 shaderc_shared
|
||||
PATHS ${ADDITIONAL_LIBRARY_PATHS} ${SHADERC_PATH_LIB}
|
||||
)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(Shaderc DEFAULT_MSG
|
||||
SHADERC_LIBRARY SHADERC_INCLUDE_DIR)
|
||||
|
||||
if(SHADERC_FOUND)
|
||||
add_library(Shaderc::shaderc_shared UNKNOWN IMPORTED)
|
||||
set_target_properties(Shaderc::shaderc_shared PROPERTIES
|
||||
IMPORTED_LOCATION ${SHADERC_LIBRARY}
|
||||
INTERFACE_INCLUDE_DIRECTORIES ${SHADERC_INCLUDE_DIR}
|
||||
INTERFACE_COMPILE_DEFINITIONS "SHADERC_SHAREDLIB"
|
||||
)
|
||||
endif()
|
||||
|
||||
mark_as_advanced(SHADERC_INCLUDE_DIR SHADERC_LIBRARY)
|
||||
@@ -1,32 +0,0 @@
|
||||
# - Try to find UDEV
|
||||
# Once done, this will define
|
||||
#
|
||||
# UDEV_FOUND - system has UDEV
|
||||
# UDEV_INCLUDE_DIRS - the UDEV include directories
|
||||
# UDEV_LIBRARIES - the UDEV library
|
||||
find_package(PkgConfig)
|
||||
|
||||
pkg_check_modules(UDEV_PKGCONF libudev)
|
||||
|
||||
find_path(UDEV_INCLUDE_DIRS
|
||||
NAMES libudev.h
|
||||
PATHS ${UDEV_PKGCONF_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
find_library(UDEV_LIBRARIES
|
||||
NAMES udev
|
||||
PATHS ${UDEV_PKGCONF_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(UDEV DEFAULT_MSG UDEV_INCLUDE_DIRS UDEV_LIBRARIES)
|
||||
|
||||
mark_as_advanced(UDEV_INCLUDE_DIRS UDEV_LIBRARIES)
|
||||
|
||||
if(UDEV_FOUND AND NOT (TARGET UDEV::UDEV))
|
||||
add_library (UDEV::UDEV UNKNOWN IMPORTED)
|
||||
set_target_properties(UDEV::UDEV
|
||||
PROPERTIES
|
||||
IMPORTED_LOCATION ${UDEV_LIBRARIES}
|
||||
INTERFACE_INCLUDE_DIRECTORIES ${UDEV_INCLUDE_DIRS})
|
||||
endif()
|
||||
@@ -1,166 +0,0 @@
|
||||
# Copyright (C) 2020 Sony Interactive Entertainment Inc.
|
||||
# Copyright (C) 2012 Raphael Kubo da Costa <rakuco@webkit.org>
|
||||
# Copyright (C) 2013 Igalia S.L.
|
||||
#
|
||||
# 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.
|
||||
# 2. 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 HOLDER AND ITS 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 ITS
|
||||
# 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.
|
||||
|
||||
#[=======================================================================[.rst:
|
||||
FindWebP
|
||||
--------------
|
||||
|
||||
Find WebP headers and libraries.
|
||||
|
||||
Imported Targets
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
``WebP::libwebp``
|
||||
The WebP library, if found.
|
||||
|
||||
``WebP::demux``
|
||||
The WebP demux library, if found.
|
||||
|
||||
Result Variables
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
This will define the following variables in your project:
|
||||
|
||||
``WebP_FOUND``
|
||||
true if (the requested version of) WebP is available.
|
||||
``WebP_VERSION``
|
||||
the version of WebP.
|
||||
``WebP_LIBRARIES``
|
||||
the libraries to link against to use WebP.
|
||||
``WebP_INCLUDE_DIRS``
|
||||
where to find the WebP headers.
|
||||
``WebP_COMPILE_OPTIONS``
|
||||
this should be passed to target_compile_options(), if the
|
||||
target is not used for linking
|
||||
|
||||
#]=======================================================================]
|
||||
|
||||
find_package(PkgConfig QUIET)
|
||||
pkg_check_modules(PC_WEBP QUIET libwebp)
|
||||
set(WebP_COMPILE_OPTIONS ${PC_WEBP_CFLAGS_OTHER})
|
||||
set(WebP_VERSION ${PC_WEBP_CFLAGS_VERSION})
|
||||
|
||||
find_path(WebP_INCLUDE_DIR
|
||||
NAMES webp/decode.h
|
||||
HINTS ${PC_WEBP_INCLUDEDIR} ${PC_WEBP_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
find_library(WebP_LIBRARY
|
||||
NAMES ${WebP_NAMES} webp libwebp
|
||||
HINTS ${PC_WEBP_LIBDIR} ${PC_WEBP_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
# There's nothing in the WebP headers that could be used to detect the exact
|
||||
# WebP version being used so don't attempt to do so. A version can only be found
|
||||
# through pkg-config
|
||||
if ("${WebP_FIND_VERSION}" VERSION_GREATER "${WebP_VERSION}")
|
||||
if (WebP_VERSION)
|
||||
message(FATAL_ERROR "Required version (" ${WebP_FIND_VERSION} ") is higher than found version (" ${WebP_VERSION} ")")
|
||||
else ()
|
||||
message(WARNING "Cannot determine WebP version without pkg-config")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
# Find components
|
||||
if (WebP_INCLUDE_DIR AND WebP_LIBRARY)
|
||||
set(_WebP_REQUIRED_LIBS_FOUND ON)
|
||||
set(WebP_LIBS_FOUND "WebP (required): ${WebP_LIBRARY}")
|
||||
else ()
|
||||
set(_WebP_REQUIRED_LIBS_FOUND OFF)
|
||||
set(WebP_LIBS_NOT_FOUND "WebP (required)")
|
||||
endif ()
|
||||
|
||||
if ("demux" IN_LIST WebP_FIND_COMPONENTS)
|
||||
find_library(WebP_DEMUX_LIBRARY
|
||||
NAMES ${WebP_DEMUX_NAMES} webpdemux libwebpdemux
|
||||
HINTS ${PC_WEBP_LIBDIR} ${PC_WEBP_LIBRARY_DIRS}
|
||||
)
|
||||
|
||||
if (WebP_DEMUX_LIBRARY)
|
||||
if (WebP_FIND_REQUIRED_demux)
|
||||
list(APPEND WebP_LIBS_FOUND "demux (required): ${WebP_DEMUX_LIBRARY}")
|
||||
else ()
|
||||
list(APPEND WebP_LIBS_FOUND "demux (optional): ${WebP_DEMUX_LIBRARY}")
|
||||
endif ()
|
||||
else ()
|
||||
if (WebP_FIND_REQUIRED_demux)
|
||||
set(_WebP_REQUIRED_LIBS_FOUND OFF)
|
||||
list(APPEND WebP_LIBS_NOT_FOUND "demux (required)")
|
||||
else ()
|
||||
list(APPEND WebP_LIBS_NOT_FOUND "demux (optional)")
|
||||
endif ()
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (NOT WebP_FIND_QUIETLY)
|
||||
if (WebP_LIBS_FOUND)
|
||||
message(STATUS "Found the following WebP libraries:")
|
||||
foreach (found ${WebP_LIBS_FOUND})
|
||||
message(STATUS " ${found}")
|
||||
endforeach ()
|
||||
endif ()
|
||||
if (WebP_LIBS_NOT_FOUND)
|
||||
message(STATUS "The following WebP libraries were not found:")
|
||||
foreach (found ${WebP_LIBS_NOT_FOUND})
|
||||
message(STATUS " ${found}")
|
||||
endforeach ()
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
find_package_handle_standard_args(WebP
|
||||
FOUND_VAR WebP_FOUND
|
||||
REQUIRED_VARS WebP_INCLUDE_DIR WebP_LIBRARY _WebP_REQUIRED_LIBS_FOUND
|
||||
VERSION_VAR WebP_VERSION
|
||||
)
|
||||
|
||||
if (WebP_LIBRARY AND NOT TARGET WebP::libwebp)
|
||||
add_library(WebP::libwebp UNKNOWN IMPORTED GLOBAL)
|
||||
set_target_properties(WebP::libwebp PROPERTIES
|
||||
IMPORTED_LOCATION "${WebP_LIBRARY}"
|
||||
INTERFACE_COMPILE_OPTIONS "${WebP_COMPILE_OPTIONS}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${WebP_INCLUDE_DIR}"
|
||||
)
|
||||
endif ()
|
||||
|
||||
if (WebP_DEMUX_LIBRARY AND NOT TARGET WebP::demux)
|
||||
add_library(WebP::demux UNKNOWN IMPORTED GLOBAL)
|
||||
set_target_properties(WebP::demux PROPERTIES
|
||||
IMPORTED_LOCATION "${WebP_DEMUX_LIBRARY}"
|
||||
INTERFACE_COMPILE_OPTIONS "${WebP_COMPILE_OPTIONS}"
|
||||
INTERFACE_INCLUDE_DIRECTORIES "${WebP_INCLUDE_DIR}"
|
||||
)
|
||||
endif ()
|
||||
|
||||
mark_as_advanced(
|
||||
WebP_INCLUDE_DIR
|
||||
WebP_LIBRARY
|
||||
WebP_DEMUX_LIBRARY
|
||||
)
|
||||
|
||||
if (WebP_FOUND)
|
||||
set(WebP_LIBRARIES ${WebP_LIBRARY} ${WebP_DEMUX_LIBRARY})
|
||||
set(WebP_INCLUDE_DIRS ${WebP_INCLUDE_DIR})
|
||||
endif ()
|
||||
@@ -1,45 +0,0 @@
|
||||
# Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
#
|
||||
# - Try to find Facebook zstd library
|
||||
# This will define
|
||||
# Zstd_FOUND
|
||||
# Zstd_INCLUDE_DIR
|
||||
# Zstd_LIBRARY
|
||||
#
|
||||
|
||||
find_path(Zstd_INCLUDE_DIR NAMES zstd.h)
|
||||
|
||||
find_library(Zstd_LIBRARY_DEBUG NAMES zstdd zstd_staticd)
|
||||
find_library(Zstd_LIBRARY_RELEASE NAMES zstd zstd_static)
|
||||
|
||||
include(SelectLibraryConfigurations)
|
||||
SELECT_LIBRARY_CONFIGURATIONS(Zstd)
|
||||
|
||||
include(FindPackageHandleStandardArgs)
|
||||
FIND_PACKAGE_HANDLE_STANDARD_ARGS(
|
||||
Zstd DEFAULT_MSG
|
||||
Zstd_LIBRARY Zstd_INCLUDE_DIR
|
||||
)
|
||||
|
||||
mark_as_advanced(Zstd_INCLUDE_DIR Zstd_LIBRARY)
|
||||
|
||||
if(Zstd_FOUND AND NOT (TARGET Zstd::Zstd))
|
||||
add_library (Zstd::Zstd UNKNOWN IMPORTED)
|
||||
set_target_properties(Zstd::Zstd
|
||||
PROPERTIES
|
||||
IMPORTED_LOCATION ${Zstd_LIBRARY}
|
||||
INTERFACE_INCLUDE_DIRECTORIES ${Zstd_INCLUDE_DIR})
|
||||
endif()
|
||||
14
CMakeModules/aarch64-cross-toolchain.cmake
Normal file
14
CMakeModules/aarch64-cross-toolchain.cmake
Normal file
@@ -0,0 +1,14 @@
|
||||
# Source: https://github.com/stenzek/duckstation/issues/626#issuecomment-660718306
|
||||
|
||||
# Target system
|
||||
SET(CMAKE_SYSTEM_NAME Linux)
|
||||
SET(CMAKE_SYSTEM_PROCESSOR aarch64)
|
||||
SET(CMAKE_SYSTEM_VERSION 1)
|
||||
set(CMAKE_CROSSCOMPILING TRUE)
|
||||
|
||||
# Cross compiler
|
||||
SET(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
|
||||
SET(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
|
||||
set(CMAKE_LIBRARY_ARCHITECTURE aarch64-linux-gnu)
|
||||
|
||||
set(THREADS_PTHREAD_ARG "0" CACHE STRING "Result from TRY_RUN" FORCE)
|
||||
14
CMakeModules/armv7-cross-toolchain.cmake
Normal file
14
CMakeModules/armv7-cross-toolchain.cmake
Normal file
@@ -0,0 +1,14 @@
|
||||
# Source: https://github.com/stenzek/duckstation/issues/626#issuecomment-660718306
|
||||
|
||||
# Target system
|
||||
SET(CMAKE_SYSTEM_NAME Linux)
|
||||
SET(CMAKE_SYSTEM_PROCESSOR armv7l)
|
||||
SET(CMAKE_SYSTEM_VERSION 1)
|
||||
set(CMAKE_CROSSCOMPILING TRUE)
|
||||
|
||||
# Cross compiler
|
||||
SET(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
|
||||
SET(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
|
||||
set(CMAKE_LIBRARY_ARCHITECTURE arm-linux-gnueabihf)
|
||||
|
||||
set(THREADS_PTHREAD_ARG "0" CACHE STRING "Result from TRY_RUN" FORCE)
|
||||
28
CMakeSettings.json
Normal file
28
CMakeSettings.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"configurations": [
|
||||
{
|
||||
"name": "x64-Debug",
|
||||
"generator": "Ninja",
|
||||
"configurationType": "Debug",
|
||||
"inheritEnvironments": [ "msvc_x64_x64" ],
|
||||
"buildRoot": "${projectDir}\\build\\${name}",
|
||||
"installRoot": "${projectDir}\\out\\install\\${name}",
|
||||
"cmakeCommandArgs": "",
|
||||
"buildCommandArgs": "-v",
|
||||
"ctestCommandArgs": "",
|
||||
"variables": []
|
||||
},
|
||||
{
|
||||
"name": "x64-Release",
|
||||
"generator": "Ninja",
|
||||
"configurationType": "RelWithDebInfo",
|
||||
"buildRoot": "${projectDir}\\build\\${name}",
|
||||
"installRoot": "${projectDir}\\out\\install\\${name}",
|
||||
"cmakeCommandArgs": "",
|
||||
"buildCommandArgs": "-v",
|
||||
"ctestCommandArgs": "",
|
||||
"inheritEnvironments": [ "msvc_x64_x64" ],
|
||||
"variables": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,16 +1,12 @@
|
||||
# DuckStation Contributors List
|
||||
|
||||
The following people have contributed to the project in some way, and are credited here.
|
||||
|
||||
## Code Contributions
|
||||
|
||||
- Connor McLaughlin - @stenzek
|
||||
- @ggrtk
|
||||
- @CookiePLMonster
|
||||
- @PookaMustard
|
||||
|
||||
## Translators
|
||||
|
||||
- Anderson Cardoso - Portuguese (Br)
|
||||
- @bajolzas - Portuguese (Pt)
|
||||
- posix - @Richard-L, blexx - German
|
||||
@@ -23,68 +19,44 @@ The following people have contributed to the project in some way, and are credit
|
||||
- @DenSinH - Dutch
|
||||
- @BenjaminSiskoo - French
|
||||
- mikakunin - Japanese
|
||||
- Zuzia, Seba, @CookiePLMonster - Polish
|
||||
- Foxtrot Uniform - Turkish
|
||||
|
||||
## UI Contributions
|
||||
|
||||
- @kamfretoz
|
||||
- @maxihplay (MBee)
|
||||
- Zuzia, Seba - Polish
|
||||
|
||||
## Cheat Database
|
||||
|
||||
- Pugsy
|
||||
- Unicorngoulash
|
||||
|
||||
## Game Compatibility Database
|
||||
|
||||
- @Zet-sensei
|
||||
- @DarkFrost89
|
||||
- @macattack222
|
||||
- @HeroponRikiBestest
|
||||
- @picili
|
||||
- @andercard0
|
||||
- @Abbanon
|
||||
- @Shideravan
|
||||
- @mirrornoir
|
||||
- @pryon
|
||||
- @MojoJojoDojo
|
||||
- @heckez-sys
|
||||
- @Damaniel
|
||||
- @RaydenX93
|
||||
- @gp2man
|
||||
- @Richard-L
|
||||
- @pan2marumie3
|
||||
- @CookiePLMonster
|
||||
- @LoStraniero91
|
||||
- @JFD62780
|
||||
- @lmarciano9
|
||||
- @Facepalm38
|
||||
- @Alien-Grey
|
||||
- @dmlipat
|
||||
- @Krusher97
|
||||
- @AngryScotsmanGaming
|
||||
- @PookaMustard
|
||||
- @waspennator
|
||||
- @Serpentario
|
||||
- @QuasarDGames
|
||||
- @egamboau
|
||||
- @goldstinger
|
||||
- @DankRank
|
||||
- @Kesnos-ho
|
||||
- @Facepalm38
|
||||
- @ZL1LAC
|
||||
- @cs50-account
|
||||
- @nxrighthere
|
||||
- @Overload86
|
||||
- @landcaster
|
||||
- @Sekai9
|
||||
- @Zet-sensei
|
||||
- @DarkFrost89
|
||||
- @macattack222
|
||||
- @HeroponRikiBestest
|
||||
- @picili
|
||||
- @andercard0
|
||||
- @Abbanon
|
||||
- @Shideravan
|
||||
- @mirrornoir
|
||||
- @pryon
|
||||
- @MojoJojoDojo
|
||||
- @heckez-sys
|
||||
- @Damaniel
|
||||
- @RaydenX93
|
||||
- @gp2man
|
||||
- @Richard-L
|
||||
- @pan2marumie3
|
||||
- @CookiePLMonster
|
||||
- @LoStraniero91
|
||||
- @JFD62780
|
||||
- @lmarciano9
|
||||
- @Facepalm38
|
||||
- @Alien-Grey
|
||||
- @dmlipat
|
||||
- @Krusher97
|
||||
- @AngryScotsmanGaming
|
||||
|
||||
## Special Thanks
|
||||
|
||||
The following people did not directly contribute to the emulator, but it would not be in the state if not for them.
|
||||
- nocash (https://problemkaputt.de/) for fantastic documentation.
|
||||
- @PeterLemon for great simple test programs.
|
||||
- amidog for CPU, GTE and GPU test programs.
|
||||
- Jakub Czekański - @JaCzekanski - for collaboration on hardware tests.
|
||||
|
||||
- nocash (https://problemkaputt.de/) for fantastic documentation.
|
||||
- @PeterLemon for great simple test programs.
|
||||
- amidog for CPU, GTE and GPU test programs.
|
||||
- Jakub Czekański - @JaCzekanski - for collaboration on hardware tests.
|
||||
|
||||
53
NEWS.md
Normal file
53
NEWS.md
Normal file
@@ -0,0 +1,53 @@
|
||||
- 2021/01/24: Runahead added - work around input lag in some games by running frames ahead of time and backtracking on input. DuckStation's implementation works with upscaling and the hardware renderers, but you still require a powerful computer for higher frame counts.
|
||||
- 2021/01/24: Rewind added - you can now "smooth rewind" (but not for long), or "skip rewind" (for much long) while playing.
|
||||
- 2021/01/10: Option to sync to host refresh rate added (enabled by default). This will give the smoothest animation possible with zero duped frames, at the cost of running the game <1% faster. Users with variable refresh rate (GSync/FreeSync) displays will want to disable the option.
|
||||
- 2021/01/10: Audio resampling added when fast forwarding to fixed speeds. Instead of crackling audio, you'll now get pitch altered audio.
|
||||
- 2021/01/03: Per game settings and game properties added to Android version.
|
||||
- 2020/12/30: Box and Adaptive downsampling modes added. Adaptive downsampling will smooth 2D backgrounds but attempt to preserve 3D geometry via pixel similarity (only supported in D3D11/Vulkan). Box is a simple average filter which will downsample to native resolution.
|
||||
- 2020/12/30: Hotkey binding added to Android version. You can now bind hotkeys such as fast forward, save state, etc to controller buttons. The ability to bind multi-button combinations will be added in the future.
|
||||
- 2020/12/29: Controller mapping/binding added for Android version. By default mappings will be clear and you will have to set them, you can do this from `Settings -> Controllers -> Controller Mapping`. Profiles can be saved and loaded as well.
|
||||
- 2020/12/29: Dark theme added for Android. By default it will follow your system theme (Android 10+), but can be overridden in settings.
|
||||
- 2020/12/29: DirectInput/DInput controller interface added for Windows. You can use this if you are having difficulties with SDL. Vibration is not supported yet.
|
||||
- 2020/12/25: Partial texture replacement support added. For now, this is only applicable to a small number of games which upload backgrounds to video RAM every frame. Dumping and replacement options are available in `Advanced Settings`.
|
||||
- 2020/12/22: PGXP Depth Buffer enhancement added. This enhancement can eliminate "polygon fighting" in games, by giving the PS1 the depth buffer it never had. Compatibility is rather low at the moment, but for the games it does work in, it works very well. The depth buffer will be made available to postprocessing shaders in the future, enabling effects such as SSAO.
|
||||
- 2020/12/21: DuckStation now has two releases - Development and Preview. New features will appear in Preview first, and make their way to the Development release a few days later. To switch to preview, update to the latest development build (older builds will update to development), change the channel from `latest` to `preview` in general settings, and click `Check for Updates`.
|
||||
- 2020/12/16: Integrated CPU debugger added in Qt frontend.
|
||||
- 2020/12/13: Button layout for the touchscreen controller in the Android version can now be customized.
|
||||
- 2020/12/10: Translation support added for Android version. Currently Brazillian Portuguese, Italian, and Dutch are available.
|
||||
- 2020/11/27: Cover support added for game list in Android version. Procedure is the same as the desktop version, except you should place cover images in `<storage>/duckstation/covers` (see [Adding Game Covers](https://github.com/stenzek/duckstation/wiki/Adding-Game-Covers)).
|
||||
- 2020/11/27: Disc database is shipped with desktop and Android versions courtesy of redump.org. This will provide titles for games on Android, where it was not possible previously.
|
||||
- 2020/11/27: Compatibility databases added to libretro core - broken enhancements will be automatically disabled. You can turn this off by disabling "Apply Compatibility Settings" in the core options.
|
||||
- 2020/11/27: SDL game controller database is included with desktop versions courtesy of https://github.com/gabomdq/SDL_GameControllerDB.
|
||||
- 2020/11/21: OpenGL ES 2.0 host display support added. You cannot use the hardware renderer with GLES2, it still requires GLES3, but GLES2 GPUs can now use the software renderer.
|
||||
- 2020/11/21: Threaded renderer for software renderer added. Can result in a significant speed boost depending on the game.
|
||||
- 2020/11/21: AArch32/armv7 recompiler added. Android and Linux builds will follow after further testing, but for now you can build it yourself.
|
||||
- 2020/11/18: Window size (resize window to Nx content resolution) added to Qt and SDL frontends.
|
||||
- 2020/11/10: Widescreen hack now renders in the display aspect ratio instead of always 16:9.
|
||||
- 2020/11/01: Exclusive fullscreen option added for Windows D3D11 users. Enjoy buttery smooth PAL games.
|
||||
- 2020/10/31: Multisample antialiasing added as an enhancement.
|
||||
- 2020/10/30: Option to use analog stick as d-pad for analog controller added.
|
||||
- 2020/10/20: New cheat manager with memory scanning added. More features will be added over time.
|
||||
- 2020/10/05: CD-ROM read speedup enhancement added.
|
||||
- 2020/09/30: CPU overclocking is now supported. Use with caution as it will break games and increase system requirements. It can be set globally or per-game.
|
||||
- 2020/09/25: Cheat support added for libretro core.
|
||||
- 2020/09/23: Game covers added to Qt frontend (see [Adding Game Covers](https://github.com/stenzek/duckstation/wiki/Adding-Game-Covers)).
|
||||
- 2020/09/19: Memory card importer/editor added to Qt frontend.
|
||||
- 2020/09/13: Support for chaining post processing shaders added.
|
||||
- 2020/09/12: Additional texture filtering options added.
|
||||
- 2020/09/09: Basic cheat support added. Not all instructions/commands are supported yet.
|
||||
- 2020/09/01: Many additional user settings available, including memory cards and enhancements. Now you can set these per-game.
|
||||
- 2020/08/25: Automated builds for macOS now available.
|
||||
- 2020/08/22: XInput controller backend added.
|
||||
- 2020/08/20: Per-game setting overrides added. Mostly for compatibility, but some options are customizable.
|
||||
- 2020/08/19: CPU PGXP mode added. It is very slow and incompatible with the recompiler, only use for games which need it.
|
||||
- 2020/08/15: Playlist support/single memcard for multi-disc games in Qt frontend added.
|
||||
- 2020/08/07: Automatic updater for standalone Windows builds.
|
||||
- 2020/08/01: Initial PGXP (geometry/perspective correction) support.
|
||||
- 2020/07/28: Qt frontend supports displaying interface in multiple languages.
|
||||
- 2020/07/23: m3u multi-disc support for libretro core.
|
||||
- 2020/07/22: Support multiple bindings for each controller button/axis.
|
||||
- 2020/07/18: Widescreen hack enhancement added.
|
||||
- 2020/07/04: Vulkan renderer now available in libretro core.
|
||||
- 2020/07/02: Now available as a libretro core.
|
||||
- 2020/07/01: Lightgun support with custom crosshairs.
|
||||
- 2020/06/19: Vulkan hardware renderer added.
|
||||
303
README.md
303
README.md
@@ -1,19 +1,44 @@
|
||||
# DuckStation - PlayStation 1, aka. PSX Emulator
|
||||
[Features](#features) | [Downloading and Running](#downloading-and-running) | [Building](#building) | [Disclaimers](#disclaimers)
|
||||
[Latest News](#latest-news) | [Features](#features) | [Screenshots](#screenshots) | [Downloading and Running](#downloading-and-running) | [Building](#building) | [Disclaimers](#disclaimers)
|
||||
|
||||
**Latest Builds for Windows 10/11 (x64/ARM64), Linux (AppImage/Flatpak), and macOS (11.0+ Universal):** https://github.com/stenzek/duckstation/releases/tag/latest
|
||||
**Discord Server:** https://discord.gg/Buktv3t
|
||||
|
||||
**Game Compatibility List:** https://docs.google.com/spreadsheets/d/e/2PACX-1vRE0jjiK_aldpICoy5kVQlpk2f81Vo6P4p9vfg4d7YoTOoDlH4PQHoXjTD2F7SdN8SSBLoEAItaIqQo/pubhtml
|
||||
**Latest Windows, Linux (AppImage), Mac, Android** https://github.com/stenzek/duckstation/releases/tag/latest
|
||||
|
||||
**Discord Server:** https://www.duckstation.org/discord.html
|
||||
**Available on Google Play:** https://play.google.com/store/apps/details?id=com.github.stenzek.duckstation&hl=en_AU&gl=US
|
||||
|
||||
**Game Compatibility List:** https://docs.google.com/spreadsheets/d/1H66MxViRjjE5f8hOl5RQmF5woS1murio2dsLn14kEqo/edit?usp=sharing
|
||||
|
||||
**Wiki:** https://www.duckstation.org/wiki/
|
||||
|
||||
DuckStation is an simulator/emulator of the Sony PlayStation(TM) console, focusing on playability, speed, and long-term maintainability. The goal is to be as accurate as possible while maintaining performance suitable for low-end devices. "Hack" options are discouraged, the default configuration should support all playable games with only some of the enhancements having compatibility issues.
|
||||
|
||||
A "BIOS" ROM image is required to to start the emulator and to play games. You can use an image from any hardware version or region, although mismatching game regions and BIOS regions may have compatibility issues. A ROM image is not provided with the emulator for legal reasons, you should dump this from your own console using Caetla or other means.
|
||||
|
||||
## Latest News
|
||||
Older entries are available at https://github.com/stenzek/duckstation/blob/master/NEWS.md
|
||||
|
||||
- 2021/01/31: "Fullscreen UI" added, aka "Big Duck/TV Mode". This interface is fully navigatible with a controller. Currently it's limited to the NoGUI frontend, but it will be available directly in the Qt frontend in the near future, with more features being added (e.g. game grid) as well.
|
||||
- 2021/01/24: Runahead added - work around input lag in some games by running frames ahead of time and backtracking on input. DuckStation's implementation works with upscaling and the hardware renderers, but you still require a powerful computer for higher frame counts.
|
||||
- 2021/01/24: Rewind added - you can now "smooth rewind" (but not for long), or "skip rewind" (for much long) while playing.
|
||||
- 2021/01/10: Option to sync to host refresh rate added (enabled by default). This will give the smoothest animation possible with zero duped frames, at the cost of running the game <1% faster. Users with variable refresh rate (GSync/FreeSync) displays will want to disable the option.
|
||||
- 2021/01/10: Audio resampling added when fast forwarding to fixed speeds. Instead of crackling audio, you'll now get pitch altered audio.
|
||||
- 2021/01/03: Per game settings and game properties added to Android version.
|
||||
- 2020/12/30: Box and Adaptive downsampling modes added. Adaptive downsampling will smooth 2D backgrounds but attempt to preserve 3D geometry via pixel similarity (only supported in D3D11/Vulkan). Box is a simple average filter which will downsample to native resolution.
|
||||
- 2020/12/30: Hotkey binding added to Android version. You can now bind hotkeys such as fast forward, save state, etc to controller buttons. The ability to bind multi-button combinations will be added in the future.
|
||||
- 2020/12/29: Controller mapping/binding added for Android version. By default mappings will be clear and you will have to set them, you can do this from `Settings -> Controllers -> Controller Mapping`. Profiles can be saved and loaded as well.
|
||||
- 2020/12/29: Dark theme added for Android. By default it will follow your system theme (Android 10+), but can be overridden in settings.
|
||||
- 2020/12/29: DirectInput/DInput controller interface added for Windows. You can use this if you are having difficulties with SDL. Vibration is not supported yet.
|
||||
- 2020/12/25: Partial texture replacement support added. For now, this is only applicable to a small number of games which upload backgrounds to video RAM every frame. Dumping and replacement options are available in `Advanced Settings`.
|
||||
- 2020/12/22: PGXP Depth Buffer enhancement added. This enhancement can eliminate "polygon fighting" in games, by giving the PS1 the depth buffer it never had. Compatibility is rather low at the moment, but for the games it does work in, it works very well. The depth buffer will be made available to postprocessing shaders in the future, enabling effects such as SSAO.
|
||||
- 2020/12/21: DuckStation now has two releases - Development and Preview. New features will appear in Preview first, and make their way to the Development release a few days later. To switch to preview, update to the latest development build (older builds will update to development), change the channel from `latest` to `preview` in general settings, and click `Check for Updates`.
|
||||
- 2020/12/16: Integrated CPU debugger added in Qt frontend.
|
||||
- 2020/12/13: Button layout for the touchscreen controller in the Android version can now be customized.
|
||||
- 2020/12/10: Translation support added for Android version. Currently Brazillian Portuguese, Italian, and Dutch are available.
|
||||
|
||||
## Features
|
||||
|
||||
DuckStation features a fully-featured frontend built using Qt, as well as a fullscreen/TV UI based on Dear ImGui.
|
||||
DuckStation features a fully-featured frontend built using Qt, as well as a fullscreen/TV UI based on Dear ImGui. An Android version has been started, but is not yet feature complete.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/stenzek/duckstation/md-images/main-qt.png" alt="Main Window Screenshot" />
|
||||
@@ -22,106 +47,110 @@ DuckStation features a fully-featured frontend built using Qt, as well as a full
|
||||
|
||||
Other features include:
|
||||
|
||||
- CPU Recompiler/JIT (x86-64, armv7/AArch32, AArch64, RISC-V/RV64).
|
||||
- Hardware (D3D11, D3D12, OpenGL, Vulkan, Metal) and software rendering.
|
||||
- Upscaling, texture filtering, and true colour (24-bit) in hardware renderers.
|
||||
- PGXP for geometry precision, texture correction, and depth buffer emulation.
|
||||
- Adaptive downsampling filter.
|
||||
- Post processing shader chains (GLSL and experimental Reshade FX).
|
||||
- "Fast boot" for skipping BIOS splash/intro.
|
||||
- Save state support.
|
||||
- Windows, Linux, macOS support.
|
||||
- Supports bin/cue images, raw bin/img files, MAME CHD, single-track ECM, MDS/MDF, and unencrypted PBP formats.
|
||||
- Direct booting of homebrew executables.
|
||||
- Direct loading of Portable Sound Format (psf) files.
|
||||
- Digital and analog controllers for input (rumble is forwarded to host).
|
||||
- Namco GunCon lightgun support (simulated with mouse).
|
||||
- NeGcon support.
|
||||
- Qt and "Big Picture" UI.
|
||||
- Automatic updates with preview and latest channels.
|
||||
- Automatic content scanning - game titles/hashes are provided by redump.org.
|
||||
- Optional automatic switching of memory cards for each game.
|
||||
- Supports loading cheats from existing lists.
|
||||
- Memory card editor and save importer.
|
||||
- Emulated CPU overclocking.
|
||||
- Integrated and remote debugging.
|
||||
- Multitap controllers (up to 8 devices).
|
||||
- RetroAchievements.
|
||||
- Automatic loading/applying of PPF patches.
|
||||
- CPU Recompiler/JIT (x86-64, armv7/AArch32 and AArch64)
|
||||
- Hardware (D3D11, OpenGL, Vulkan) and software rendering
|
||||
- Upscaling, texture filtering, and true colour (24-bit) in hardware renderers
|
||||
- PGXP for geometry precision, texture correction, and depth buffer emulation
|
||||
- Adaptive downsampling filter
|
||||
- Post processing shader chains
|
||||
- "Fast boot" for skipping BIOS splash/intro
|
||||
- Save state support
|
||||
- Windows, Linux, **highly experimental** macOS support
|
||||
- Supports bin/cue images, raw bin/img files, and MAME CHD formats.
|
||||
- Direct booting of homebrew executables
|
||||
- Direct loading of Portable Sound Format (psf) files
|
||||
- Digital and analog controllers for input (rumble is forwarded to host)
|
||||
- Namco GunCon lightgun support (simulated with mouse)
|
||||
- NeGcon support
|
||||
- Qt and NoGUI frontends for desktop
|
||||
- Automatic updates for Windows builds
|
||||
- Automatic content scanning - game titles/regions are provided by redump.org
|
||||
- Optional automatic switching of memory cards for each game
|
||||
- Supports loading cheats from libretro or PCSXR format lists
|
||||
- Memory card editor and save importer
|
||||
- Emulated CPU overclocking
|
||||
- Integrated and remote debugging
|
||||
|
||||
## System Requirements
|
||||
- A CPU faster than a potato. But it needs to be x86_64, AArch32/armv7, AArch64/ARMv8, or RISC-V/RV64.
|
||||
- For the hardware renderers, a GPU capable of OpenGL 3.1/OpenGL ES 3.1/Direct3D 11 Feature Level 10.0 (or Vulkan 1.0) and above. So, basically anything made in the last 10 years or so.
|
||||
- SDL, XInput or DInput compatible game controller (e.g. XB360/XBOne/XBSeries). DualShock 3 users on Windows will need to install the official DualShock 3 drivers included as part of PlayStation Now.
|
||||
- A CPU faster than a potato. But it needs to be x86_64, AArch32/armv7, or AArch64/ARMv8, otherwise you won't get a recompiler and it'll be slow.
|
||||
- For the hardware renderers, a GPU capable of OpenGL 3.1/OpenGL ES 3.0/Direct3D 11 Feature Level 10.0 (or Vulkan 1.0) and above. So, basically anything made in the last 10 years or so.
|
||||
- SDL, XInput or DInput compatible game controller (e.g. XB360/XBOne). DualShock 3 users on Windows will need to install the official DualShock 3 drivers included as part of PlayStation Now.
|
||||
|
||||
## Downloading and running
|
||||
Binaries of DuckStation for Windows x64/ARM64, Linux x86_64 (in AppImage/Flatpak formats), and macOS Universal Binaries are available via GitHub Releases and are automatically built with every commit/push. Binaries or packages distributed through other sources may be out of date and are not supported by the developer, please speak to them for support, not us.
|
||||
Binaries of DuckStation for Windows x64/ARM64, x86_64 Linux x86_64 (in AppImage format), and Android ARMv8/AArch64 are available via GitHub Releases and are automatically built with every commit/push. Binaries or packages distributed through other sources may be out of date and are not supported by the developer.
|
||||
|
||||
### Windows
|
||||
|
||||
DuckStation **requires** Windows 10/11, specifically version 1809 or newer. If you are still using Windows 7/8/8.1, DuckStation **will not run** on your operating system. Running these operating systems in 2023 should be considered a security risk, and I would recommend updating to something which receives vendor support.
|
||||
If you must use an older operating system, [v0.1-5624](https://github.com/stenzek/duckstation/releases/tag/v0.1-5624) is the last version which will run. But do not expect to recieve any assistance, these builds are no longer supported.
|
||||
**Windows 10 is the only version of Windows supported by the developer.** Windows 7/8 may work, but is not supported. I am aware some users are still using Windows 7, but it is no longer supported by Microsoft and too much effort to get running on modern hardware. Game bugs are unlikely to be affected by the operating system, however performance issues should be verified on Windows 10 before reporting.
|
||||
|
||||
To download:
|
||||
- Go to https://github.com/stenzek/duckstation/releases/tag/latest, and download the Windows x64 build. This is a zip archive containing the prebuilt binary.
|
||||
- Alternatively, direct download link: https://github.com/stenzek/duckstation/releases/download/latest/duckstation-windows-x64-release.zip
|
||||
- Extract the archive **to a subdirectory**. The archive has no root subdirectory, so extracting to the current directory will drop a bunch of files in your download directory if you do not extract to a subdirectory.
|
||||
|
||||
Once downloaded and extracted, you can launch the emulator with `duckstation-qt-x64-ReleaseLTCG.exe`. Follow the Setup Wizard to get started.
|
||||
Once downloaded and extracted, you can launch the emulator with `duckstation-qt-x64-ReleaseLTCG.exe`.
|
||||
To set up:
|
||||
1. Either configure the path to a BIOS image in the settings, or copy one or more PlayStation BIOS images to the bios/ subdirectory. On Windows, by default this will be located in `C:\Users\YOUR_USERNAME\Documents\DuckStation\bios`. If you don't want to use the Documents directory to save the BIOS/memory cards/etc, you can use portable mode. See [User directory](#user-directories).
|
||||
2. If using the Qt frontend, add the directories containing your disc images by clicking `Settings->Add Game Directory`.
|
||||
2. Select a game from the list, or open a disc image file and enjoy.
|
||||
|
||||
**If you get an error about `vcruntime140_1.dll` being missing, you will need to update your Visual C++ runtime.** You can do that from this page: https://support.microsoft.com/en-au/help/2977003/the-latest-supported-visual-c-downloads. Specifically, you want the x64 runtime, which can be downloaded from https://aka.ms/vs/17/release/vc_redist.x64.exe.
|
||||
**If you get an error about `vcruntime140_1.dll` being missing, you will need to update your Visual C++ runtime.** You can do that from this page: https://support.microsoft.com/en-au/help/2977003/the-latest-supported-visual-c-downloads. Specifically, you want the x64 runtime, which can be downloaded from https://aka.ms/vs/16/release/vc_redist.x64.exe.
|
||||
|
||||
The Qt frontend includes an automatic update checker. Builds downloaded after 2020/08/07 will automatically check for updates each time the emulator starts, this can be disabled in Settings. Alternatively, you can force an update check by clicking `Help->Check for Updates`.
|
||||
|
||||
### Linux
|
||||
|
||||
The only supported version of DuckStation for Linux are the AppImage and Flatpak in the releases page. If you installed DuckStation from another source or distribution (e.g. EmuDeck), you should contact the packager for support, we have no control over it.
|
||||
Prebuilt binaries for 64-bit Linux distros are available for download in the AppImage format. However, these binaries may be incompatible with older Linux distros (e.g. Ubuntu distros earlier than 18.04.4 LTS) due to older distros not providing newer versions of the C/C++ standard libraries required by the AppImage binaries.
|
||||
|
||||
The release on [Flathub](https://flathub.org/apps/org.duckstation.DuckStation) is official, and synchronized with the latest rolling/stable release on GitHub.
|
||||
|
||||
You **should not** install DuckStation from unofficial repositories such as the AUR, they are **known to be broken**.
|
||||
|
||||
#### AppImage
|
||||
|
||||
The AppImages require a distribution equivalent to Ubuntu 22.04 or newer to run.
|
||||
|
||||
- Go to https://github.com/stenzek/duckstation/releases/tag/latest, and download `duckstation-x64.AppImage`.
|
||||
- Run `chmod a+x` on the downloaded AppImage -- following this step, the AppImage can be run like a typical executable.
|
||||
|
||||
#### Flatpak
|
||||
|
||||
- Go to https://github.com/stenzek/duckstation/releases/tag/latest, and download `duckstation-x64.flatpak`.
|
||||
- Run `flatpak install ./duckstation-x64.flatpak`.
|
||||
|
||||
or, if you have FlatHub set up:
|
||||
- Run `flatpak install org.duckstation.DuckStation`.
|
||||
|
||||
Use `flatpak run org.duckstation.DuckStation` to start, or select `DuckStation` in the launcher of your desktop environment. Follow the Setup Wizard to get started.
|
||||
|
||||
### macOS
|
||||
|
||||
Universal MacOS builds are provided for both x64 and ARM64 (Apple Silicon).
|
||||
|
||||
MacOS Big Sir (11.0) is required, as this is also the minimum requirement for Qt.
|
||||
**Linux users are encouraged to build from source when possible and optionally create their own AppImages for features such as desktop integration if desired.**
|
||||
|
||||
To download:
|
||||
- Go to https://github.com/stenzek/duckstation/releases/tag/latest, and download `duckstation-mac-release.zip`.
|
||||
- Extract the zip by double-clicking it.
|
||||
- Open DuckStation.app, optionally moving it to your desired location first.
|
||||
- Depending on GateKeeper configuration, you may need to right click -> Open the first time you run it, as code signing certificates are out of the question for a project which brings in zero revenue.
|
||||
|
||||
- Go to https://github.com/stenzek/duckstation/releases/tag/latest, and download either `duckstation-qt-x64.AppImage` or `duckstation-nogui-x64.AppImage` for your desired frontend.
|
||||
- Run `chmod a+x` on the downloaded AppImage -- following this step, the AppImage can be run like a typical executable.
|
||||
- Optionally use a program such as [appimaged](https://github.com/AppImage/appimaged) or [AppImageLauncher](https://github.com/TheAssassin/AppImageLauncher) for desktop integration. [AppImageUpdate](https://github.com/AppImage/AppImageUpdate) can be used alongside appimaged to easily update your DuckStation AppImage.
|
||||
|
||||
### macOS
|
||||
|
||||
To download:
|
||||
- Go to https://github.com/stenzek/duckstation/releases/tag/latest, and download the Mac build. This is a zip archive containing the prebuilt binary.
|
||||
- Alternatively, direct download link: https://github.com/stenzek/duckstation/releases/download/latest/duckstation-mac-release.zip
|
||||
- Extract the zip archive. If you're using Safari, apparently this happens automatically. This will give you DuckStation.app.
|
||||
- Right click DuckStation.app, and click Open. As the package is not signed (Mac certificates are expensive), you must do this the first time you open it. Subsequent runs can be done by double-clicking.
|
||||
|
||||
macOS support is considered experimental and not actively supported by the developer; the builds are provided here as a courtesy. Please feel free to submit issues, but it may be some time before
|
||||
they are investigated.
|
||||
|
||||
**macOS builds do not support automatic updates yet.** If there is sufficient demand, this may be something I will consider.
|
||||
|
||||
|
||||
### Android
|
||||
|
||||
You will need a device with armv7 (32-bit ARM), AArch64 (64-bit ARM), or x86_64 (64-bit x86). 64-bit is preferred, the requirements are higher for 32-bit, you'll probably want at least a 1.5GHz CPU.
|
||||
A prebuilt APK is now available for Android. However, please keep in mind that the Android version does not contain all features present in the desktop version yet. You will need a device with armv7 (32-bit ARM) or AArch64 (64-bit ARM). 64-bit is preferred, the requirements are higher for 32-bit, you'll probably want at least a 1.5GHz CPU.
|
||||
|
||||
Download from Google Play: https://play.google.com/store/apps/details?id=com.github.stenzek.duckstation
|
||||
APK and Beta Downloads: https://www.duckstation.org/android/
|
||||
|
||||
**No support is provided for the Android app**, it is free and your expectations should be in line with that. Please **do not** email me about issues about it, or ask for help, you will be ignored.
|
||||
Download link: https://github.com/stenzek/duckstation/releases/download/latest/duckstation-android.apk
|
||||
|
||||
To use:
|
||||
1. Install and run the app for the first time.
|
||||
2. Follow the setup wizard.
|
||||
- Install and run the app for the first time.
|
||||
- This will create `/sdcard/duckstation`. Drop your BIOS files in `/sdcard/duckstation/bios`.
|
||||
- Add game directories by hitting the `+` icon and selecting a directory.
|
||||
- Map your controller buttons and axes by going into `Controller Mapping` under `Controllers` in `Settings`.
|
||||
- Tap a game to start.
|
||||
|
||||
If you have an external controller, you will need to map the buttons and sticks in settings.
|
||||
|
||||
### Title Information
|
||||
|
||||
PlayStation game discs do not contain title information. For game titles, we use the redump.org database cross-referenced with the game's executable code. A version of the database is included with the DuckStation download, but you can replace this with a different database by saving it as `cache/redump.dat` in your user directory, or updated by going into the `Game List Settings` in the Qt Frontend, and clicking `Update Redump Database`.
|
||||
|
||||
### Region detection and BIOS images
|
||||
By default, DuckStation will emulate the region check present in the CD-ROM controller of the console. This means that when the region of the console does not match the disc, it will refuse to boot, giving a "Please insert PlayStation CD-ROM" message. DuckStation supports automatic detection disc regions, and if you set the console region to auto-detect as well, this should never be a problem.
|
||||
|
||||
If you wish to use auto-detection, you do not need to change the BIOS path each time you switch regions. Simply place the BIOS images for the other regions in the **same directory** as the configured image. This will probably be in the `bios/` subdirectory. Then set the console region to "Auto-Detect", and everything should work fine. The console/log will tell you if you are missing the image for the disc's region.
|
||||
|
||||
Some users have been confused by the "BIOS Path" option, the reason it is a path and not a directory is so that an unknown BIOS revision can be used/tested.
|
||||
|
||||
Alternatively, the region checking can be disabled in the console options tab. This is the only way to play unlicensed games or homebrew which does not supply a correct region string on the disc, aside from using fastboot which skips the check entirely.
|
||||
|
||||
Mismatching the disc and console regions with the check disabled is supported, but may break games if they are patching the BIOS and expecting specific content.
|
||||
|
||||
### LibCrypt protection and SBI files
|
||||
|
||||
@@ -131,57 +160,59 @@ For these games, make sure that the CD image and its corresponding SBI (.sbi) fi
|
||||
|
||||
For example, if your disc image was named `Spyro3.cue`, you would place the SBI file in the same directory, and name it `Spyro3.sbi`.
|
||||
|
||||
CHD images with built-in subchannel information are also supported.
|
||||
|
||||
## Building
|
||||
|
||||
### Windows
|
||||
Requirements:
|
||||
- Visual Studio 2022
|
||||
|
||||
|
||||
1. Clone the respository: `git clone https://github.com/stenzek/duckstation.git`.
|
||||
2. Download the dependencies pack from https://github.com/stenzek/duckstation-ext-qt-minimal/releases/download/latest/deps-x64.7z, and extract it to `dep\msvc`.
|
||||
3. Open the Visual Studio solution `duckstation.sln` in the root, or "Open Folder" for cmake build.
|
||||
4. Build solution.
|
||||
5. Binaries are located in `bin/x64`.
|
||||
6. Run `duckstation-qt-x64-Release.exe` or whichever config you used.
|
||||
- Visual Studio 2019
|
||||
|
||||
1. Clone the respository with submodules (`git clone --recursive https://github.com/stenzek/duckstation.git -b dev`).
|
||||
2. Open the Visual Studio solution `duckstation.sln` in the root, or "Open Folder" for cmake build.
|
||||
3. Build solution.
|
||||
4. Binaries are located in `bin/x64`.
|
||||
5. Run `duckstation-qt-x64-Release.exe` or whichever config you used.
|
||||
|
||||
### Linux
|
||||
Requirements (Debian/Ubuntu package names):
|
||||
- CMake (`cmake`)
|
||||
- SDL2 (`libsdl2-dev`)
|
||||
- pkgconfig (`pkg-config`)
|
||||
- Qt 5 (`qtbase5-dev`, `qtbase5-private-dev`, `qtbase5-dev-tools`, `qttools5-dev`)
|
||||
- libevdev (`libevdev-dev`)
|
||||
- git (`git`) (Note: needed to clone the repository and at build time)
|
||||
- Optional for faster building: Ninja (`ninja-build`)
|
||||
- Optional for framebuffer output: DRM/GBM (`libgbm-dev`, `libdrm-dev`)
|
||||
|
||||
#### Required Dependencies
|
||||
|
||||
Ubuntu/Debian package names:
|
||||
```
|
||||
build-essential clang cmake curl extra-cmake-modules git libasound2-dev libcurl4-openssl-dev libdbus-1-dev libdecor-0-dev libegl-dev libevdev-dev libfontconfig-dev libfreetype-dev libgtk-3-dev libgudev-1.0-dev libharfbuzz-dev libinput-dev libopengl-dev libpipewire-0.3-dev libpulse-dev libssl-dev libudev-dev libwayland-dev libx11-dev libx11-xcb-dev libxcb1-dev libxcb-composite0-dev libxcb-cursor-dev libxcb-damage0-dev libxcb-glx0-dev libxcb-icccm4-dev libxcb-image0-dev libxcb-keysyms1-dev libxcb-present-dev libxcb-randr0-dev libxcb-render0-dev libxcb-render-util0-dev libxcb-shape0-dev libxcb-shm0-dev libxcb-sync-dev libxcb-util-dev libxcb-xfixes0-dev libxcb-xinput-dev libxcb-xkb-dev libxext-dev libxkbcommon-x11-dev libxrandr-dev lld llvm ninja-build pkg-config zlib1g-dev
|
||||
```
|
||||
|
||||
Fedora package names:
|
||||
```
|
||||
alsa-lib-devel brotli-devel clang cmake dbus-devel egl-wayland-devel extra-cmake-modules fontconfig-devel gcc-c++ gtk3-devel libcurl-devel libdecor-devel libevdev-devel libICE-devel libinput-devel libSM-devel libX11-devel libXau-devel libxcb-devel libXcomposite-devel libXcursor-devel libXext-devel libXfixes-devel libXft-devel libXi-devel libxkbcommon-devel libxkbcommon-x11-devel libXpresent-devel libXrandr-devel libXrender-devel lld llvm make mesa-libEGL-devel mesa-libGL-devel ninja-build openssl-devel patch pcre2-devel perl-Digest-SHA pipewire-devel pulseaudio-libs-devel systemd-devel wayland-devel xcb-util-cursor-devel xcb-util-devel xcb-util-errors-devel xcb-util-image-devel xcb-util-keysyms-devel xcb-util-renderutil-devel xcb-util-wm-devel xcb-util-xrm-devel zlib-devel
|
||||
```
|
||||
|
||||
#### Building
|
||||
|
||||
1. Clone the repository: `git clone https://github.com/stenzek/duckstation.git`, `cd duckstation`.
|
||||
2. Build dependencies. You can save these outside of the tree if you like. This will take a while. `scripts/build-dependencies-linux.sh deps`.
|
||||
3. Run CMake to configure the build system. Assuming a build subdirectory of `build-release`, run `cmake -B build-release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_EXE_LINKER_FLAGS_INIT="-fuse-ld=lld" -DCMAKE_MODULE_LINKER_FLAGS_INIT="-fuse-ld=lld" -DCMAKE_SHARED_LINKER_FLAGS_INIT="-fuse-ld=lld" -DCMAKE_PREFIX_PATH="$PWD/deps" -G Ninja`. If you want a release (optimized) build, include `-DCMAKE_BUILD_TYPE=Release -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON`.
|
||||
4. Compile the source code. For the example above, run `ninja -C build-release`
|
||||
5. Run the binary, located in the build directory under `./build-release/bin/duckstation-qt`.
|
||||
1. Clone the repository. Submodules aren't necessary, there is only one and it is only used for Windows (`git clone https://github.com/stenzek/duckstation.git -b dev`).
|
||||
2. Create a build directory, either in-tree or elsewhere.
|
||||
3. Run cmake to configure the build system. Assuming a build subdirectory of `build-release`, `cd build-release && cmake -DCMAKE_BUILD_TYPE=Release -GNinja ..`.
|
||||
4. Compile the source code. For the example above, run `ninja`.
|
||||
5. Run the binary, located in the build directory under `bin/duckstation-qt`.
|
||||
|
||||
### macOS
|
||||
**NOTE:** macOS is highly experimental and not tested by the developer. Use at your own risk, things may be horribly broken.
|
||||
|
||||
Requirements:
|
||||
- CMake
|
||||
- Xcode
|
||||
- CMake (installed by default? otherwise, `brew install cmake`)
|
||||
- SDL2 (`brew install sdl2`)
|
||||
- Qt 5 (`brew install qt5`)
|
||||
|
||||
|
||||
1. Clone the repository: `git clone https://github.com/stenzek/duckstation.git`.
|
||||
2. Build the dependencies. This will take a while. `scripts/build-dependencies-mac.sh deps`.
|
||||
2. Run CMake to configure the build system: `cmake -Bbuild-release -DCMAKE_BUILD_TYPE=Release -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON -DCMAKE_PREFIX_PATH="$PWD/deps"`.
|
||||
4. Compile the source code: `cmake --build build-release --parallel`.
|
||||
1. Clone the repository. Submodules aren't necessary, there is only one and it is only used for Windows (`git clone https://github.com/stenzek/duckstation.git -b dev`).
|
||||
2. Clone the mac externals repository (for MoltenVK): `git clone https://github.com/stenzek/duckstation-ext-mac.git dep/mac`.
|
||||
2. Create a build directory, either in-tree or elsewhere, e.g. `mkdir build-release`, `cd build-release`.
|
||||
3. Run cmake to configure the build system: `cmake -DCMAKE_BUILD_TYPE=Release -DQt5_DIR=/usr/local/opt/qt/lib/cmake/Qt5 ..`. You may need to tweak `Qt5_DIR` depending on your system.
|
||||
4. Compile the source code: `make`. Use `make -jN` where `N` is the number of CPU cores in your system for a faster build.
|
||||
5. Run the binary, located in the build directory under `bin/DuckStation.app`.
|
||||
|
||||
### Android
|
||||
Requirements:
|
||||
- Android Studio with the NDK and CMake installed
|
||||
|
||||
1. Clone the repository. Submodules aren't necessary, there is only one and it is only used for Windows.
|
||||
2. Open the project in the `android` directory.
|
||||
3. Select Build -> Build Bundle(s) / APKs(s) -> Build APK(s).
|
||||
4. Install APK on device, or use Run menu for attached device.
|
||||
|
||||
## User Directories
|
||||
The "User Directory" is where you should place your BIOS images, where settings are saved to, and memory cards/save states are saved by default.
|
||||
An optional [SDL game controller database file](#sdl-game-controller-database) can be also placed here.
|
||||
@@ -201,7 +232,7 @@ in the same directory as the DuckStation executable.
|
||||
## Bindings for Qt frontend
|
||||
Your keyboard or game controller can be used to simulate a variety of PlayStation controllers. Controller input is supported through DInput, XInput, and SDL backends and can be changed through `Settings -> General Settings`.
|
||||
|
||||
To bind your input device, go to `Settings -> Controllers`. Each of the buttons/axes for the simulated controller will be listed, alongside the corresponding key/button on your device that it is currently bound to. To rebind, click the box next to the button/axis name, and press the key or button on your input device that you wish to bind to. When binding rumble, simply press any button on the controller you wish to send rumble to.
|
||||
To bind your input device, go to `Settings -> Controller Settings`. Each of the buttons/axes for the simulated controller will be listed, alongside the corresponding key/button on your device that it is currently bound to. To rebind, click the box next to the button/axis name, and press the key or button on your input device that you wish to bind to. When binding rumble, simply press any button on the controller you wish to send rumble to.
|
||||
|
||||
## SDL Game Controller Database
|
||||
DuckStation releases ship with a database of game controller mappings for the SDL controller backend, courtesy of https://github.com/gabomdq/SDL_GameControllerDB. The included `gamecontrollerdb.txt` file can be found in the `database` subdirectory of the DuckStation program directory.
|
||||
@@ -210,24 +241,44 @@ If you are experiencing issues binding your controller with the SDL controller b
|
||||
|
||||
## Default bindings
|
||||
Controller 1:
|
||||
- **Left Stick:** W/A/S/D
|
||||
- **Right Stick:** T/F/G/H
|
||||
- **D-Pad:** Up/Left/Down/Right
|
||||
- **Triangle/Square/Circle/Cross:** I/J/L/K
|
||||
- **D-Pad:** W/A/S/D
|
||||
- **Triangle/Square/Circle/Cross:** Numpad8/Numpad4/Numpad6/Numpad2
|
||||
- **L1/R1:** Q/E
|
||||
- **L2/R2:** 1/3
|
||||
- **L3/R3:** 2/4
|
||||
- **Start:** Enter
|
||||
- **Select:** Backspace
|
||||
|
||||
Hotkeys:
|
||||
- **Escape:** Open Pause Menu
|
||||
- **F11:** Toggle Fullscreen
|
||||
- **Tab:** Temporarily Disable Speed Limiter
|
||||
- **Space:** Pause/Resume Emulation
|
||||
- **Escape:** Power off console
|
||||
- **ALT+ENTER:** Toggle fullscreen
|
||||
- **Tab:** Temporarily disable speed limiter
|
||||
- **Pause/Break:** Pause/resume emulation
|
||||
- **Page Up/Down:** Increase/decrease resolution scale in hardware renderers
|
||||
- **End:** Toggle software renderer
|
||||
|
||||
## Tests
|
||||
- Passes amidog's CPU and GTE tests in both interpreter and recompiler modes, partial passing of CPX tests
|
||||
|
||||
## Screenshots
|
||||
<p align="center">
|
||||
<a href="https://raw.githubusercontent.com/stenzek/duckstation/md-images/monkey.jpg"><img src="https://raw.githubusercontent.com/stenzek/duckstation/md-images/monkey.jpg" alt="Monkey Hero" width="400" /></a>
|
||||
<a href="https://raw.githubusercontent.com/stenzek/duckstation/md-images/rrt4.jpg"><img src="https://raw.githubusercontent.com/stenzek/duckstation/md-images/rrt4.jpg" alt="Ridge Racer Type 4" width="400" /></a>
|
||||
<a href="https://raw.githubusercontent.com/stenzek/duckstation/md-images/tr2.jpg"><img src="https://raw.githubusercontent.com/stenzek/duckstation/md-images/tr2.jpg" alt="Tomb Raider 2" width="400" /></a>
|
||||
<a href="https://raw.githubusercontent.com/stenzek/duckstation/md-images/quake2.jpg"><img src="https://raw.githubusercontent.com/stenzek/duckstation/md-images/quake2.jpg" alt="Quake 2" width="400" /></a>
|
||||
<a href="https://raw.githubusercontent.com/stenzek/duckstation/md-images/croc.jpg"><img src="https://raw.githubusercontent.com/stenzek/duckstation/md-images/croc.jpg" alt="Croc" width="400" /></a>
|
||||
<a href="https://raw.githubusercontent.com/stenzek/duckstation/md-images/croc2.jpg"><img src="https://raw.githubusercontent.com/stenzek/duckstation/md-images/croc2.jpg" alt="Croc 2" width="400" /></a>
|
||||
<a href="https://raw.githubusercontent.com/stenzek/duckstation/md-images/ff7.jpg"><img src="https://raw.githubusercontent.com/stenzek/duckstation/md-images/ff7.jpg" alt="Final Fantasy 7" width="400" /></a>
|
||||
<a href="https://raw.githubusercontent.com/stenzek/duckstation/md-images/mm8.jpg"><img src="https://raw.githubusercontent.com/stenzek/duckstation/md-images/mm8.jpg" alt="Mega Man 8" width="400" /></a>
|
||||
<a href="https://raw.githubusercontent.com/stenzek/duckstation/md-images/ff8.jpg"><img src="https://raw.githubusercontent.com/stenzek/duckstation/md-images/ff8.jpg" alt="Final Fantasy 8 in Fullscreen UI" width="400" /></a>
|
||||
<a href="https://raw.githubusercontent.com/stenzek/duckstation/md-images/spyro.jpg"><img src="https://raw.githubusercontent.com/stenzek/duckstation/md-images/spyro.jpg" alt="Spyro in Fullscreen UI" width="400" /></a>
|
||||
<a href="https://raw.githubusercontent.com/stenzek/duckstation/md-images/tof.jpg"><img src="https://raw.githubusercontent.com/stenzek/duckstation/md-images/tof.jpg" alt="Threads of Fate in Fullscreen UI" width="400" /></a>
|
||||
<a href="https://raw.githubusercontent.com/stenzek/duckstation/md-images/gamegrid.png"><img src="https://raw.githubusercontent.com/stenzek/duckstation/md-images/gamegrid.png" alt="Game Grid" width="400" /></a>
|
||||
</p>
|
||||
|
||||
## Disclaimers
|
||||
|
||||
Icon by icons8: https://icons8.com/icon/74847/platforms.undefined.short-title
|
||||
|
||||
"PlayStation" and "PSX" are registered trademarks of Sony Interactive Entertainment Europe Limited. This project is not affiliated in any way with Sony Interactive Entertainment.
|
||||
|
||||
|
||||
|
||||
221
README.pt-br.md
221
README.pt-br.md
@@ -1,221 +0,0 @@
|
||||
Tradução:
|
||||
|
||||
# DuckStation - Emulador de PlayStation 1, também conhecido como PSX
|
||||
[Últimas Notícias](#latest-news) | [Recursos](#features) | [Download e Execução](#downloading-and-running) | [Compilação](#building) | [Avisos Legais](#disclaimers)
|
||||
|
||||
**Últimas Versões para Windows 10/11, Linux (AppImage/Flatpak) e macOS:** https://github.com/stenzek/duckstation/releases/tag/latest
|
||||
|
||||
**Lista de Compatibilidade de Jogos:** https://docs.google.com/spreadsheets/d/1H66MxViRjjE5f8hOl5RQmF5woS1murio2dsLn14kEqo/edit
|
||||
|
||||
**Wiki:** https://www.duckstation.org/wiki/
|
||||
|
||||
DuckStation é um simulador/emulador do console Sony PlayStation(TM), focando na jogabilidade, velocidade e manutenção a longo prazo. O objetivo é ser o mais preciso possível, mantendo um desempenho adequado para dispositivos de baixo desempenho. Opções de "hack" não são recomendadas, a configuração padrão deve suportar todos os jogos jogáveis, com apenas algumas das melhorias tendo problemas de compatibilidade.
|
||||
|
||||
Uma imagem ROM do "BIOS" é necessária para iniciar o emulador e jogar os jogos. Você pode usar uma imagem de qualquer versão de hardware ou região, embora regiões de jogos e regiões de BIOS que não idênticas podem resultar em problemas de compatibilidade. A imagem ROM ou Jogo não é fornecida com o emulador por motivos legais; você deve obter do seu próprio console usando Caetla ou outros meios.
|
||||
|
||||
## Recursos
|
||||
|
||||
O DuckStation possui uma interface totalmente funcional construída usando Qt, bem como uma interface de tela cheia/TV baseada no Dear ImGui.
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/stenzek/duckstation/md-images/main-qt.png" alt="Captura de Tela da Janela Principal" />
|
||||
<img src="https://raw.githubusercontent.com/stenzek/duckstation/md-images/bigduck.png" alt="Captura de Tela da Interface de Tela Cheia" />
|
||||
</p>
|
||||
|
||||
Outros recursos incluem:
|
||||
|
||||
- Recompilador de CPU/JIT (x86-64, armv7/AArch32 e AArch64).
|
||||
- Renderização de hardware (D3D11, D3D12, OpenGL, Vulkan, Metal) e renderização de software.
|
||||
- Ampliação, filtragem de textura e cor verdadeira (24 bits) nos renderizadores de hardware.
|
||||
- PGXP para precisão de geometria, correção de textura e emulação de buffer de profundidade.
|
||||
- Filtro de downsampling adaptativo.
|
||||
- Cadeias de shaders de pós-processamento (GLSL e Reshade FX experimental).
|
||||
- "Inicialização rápida" para pular a tela de abertura/intro do BIOS.
|
||||
- Suporte a salvar estados.
|
||||
- Suporte para Windows, Linux e macOS.
|
||||
- Suporta imagens bin/cue, arquivos bin/img crus, MAME CHD, ECM de única faixa, MDS/MDF e formatos PBP não criptografados.
|
||||
- Inicialização direta de executáveis homebrew.
|
||||
- Carregamento direto de arquivos Portable Sound Format (psf).
|
||||
- Controles digitais e analógicos.
|
||||
- Suporte ao lightgun Namco GunCon (simulado com o mouse).
|
||||
- Suporte ao NeGcon.
|
||||
- Interface Qt e "Big Picture".
|
||||
- Atualizações automáticas a partir dos canais oficiais.
|
||||
- Verificação automática de conteúdo - os títulos/jogos são fornecidos por redump.org.
|
||||
- Troca automática opcional de cartões de memória para cada jogo.
|
||||
- Suporta carregar trapaças de listas existentes.
|
||||
- Editor de cartões de memória e importador de salvamento.
|
||||
- Overclock emulado de CPU.
|
||||
- Depuração integrada e remota.
|
||||
- Controles multitap (até 8 dispositivos).
|
||||
- RetroAchievements.
|
||||
- Carregamento/aplicação automática de patches PPF.
|
||||
|
||||
## Requisitos do Sistema
|
||||
- Um CPU rápido. Mas precisa ser x86_64, AArch32/armv7 ou AArch64/ARMv8, caso contrário, o recompilação será lenta.
|
||||
- Para os renderizadores de hardware, é necessário uma GPU compatível com OpenGL 3.1/OpenGL ES 3.1/Direct3D 11 Feature Level 10.0 (ou Vulkan 1.0) e superior. basicamente, qualquer computador produzido nos últimos 10 anos mais ou menos deve dar conta.
|
||||
- Controlador de jogo compatível com SDL, XInput ou DInput (por exemplo, XB360/XBOne/XBSeries). Usuários de DualShock 3 no Windows precisarão instalar os drivers oficiais do DualShock 3 incluídos como parte do PlayStation Now.
|
||||
|
||||
## Download e Execução
|
||||
Executáveis do DuckStation para Windows x64/ARM64, Linux x86_64 (nos formatos AppImage/Flatpak) e para macOS estão disponíveis via GitHub na aba Releases e são automaticamente compilados a cada commit/envio. Executáveis ou pacotes distribuídos por outras fontes podem estar desatualizados e não são suportados pelo desenvolvedor, por favor, entre em contato com eles para obter suporte, não conosco.
|
||||
|
||||
### Windows
|
||||
|
||||
DuckStation **requer** Windows 10/11, especificamente a versão 1809 ou mais recente. Se você ainda estiver usando Windows 7/8/8.1, o DuckStation **não funcionará** no seu sistema operacional. Usar esses sistemas operacionais em 2023 deve ser considerado um risco de segurança, recomendaria atualizar para algo que receba suporte do fornecedor.
|
||||
Se você precisa usar um sistema operacional mais antigo, [v0.1-5624](https://github.com/stenzek/duckstation/releases/tag/v0.1-5624) é a última versão que funcionará. Mas não espere receber nenhuma assistência, essas compilações não são mais suportadas.
|
||||
|
||||
Para baixar:
|
||||
- Acesse https://github.com/stenzek/duckstation/releases/tag/latest e baixe a compilação do Windows x64. Este é um arquivo ZIP contendo o executável pré-compilado.
|
||||
- Alternativamente, link de download direto: https://github.com/stenzek/duckstation/releases/download/latest/duckstation-windows-x64-release.zip
|
||||
- Extraia o arquivo ZIP **para uma pasta**. O arquivo ZIP não tem um subdiretório raiz, então, se você não extrair para um subdiretório, ele irá despejar vários arquivos no seu diretório de download.
|
||||
|
||||
Depois de baixado e extraído, pode iniciar o emulador com `duckstation-qt-x64-ReleaseLTCG.exe`. Siga o Assistente de configuração para começar.
|
||||
|
||||
**Se você receber um erro sobre a falta de `vcruntime140_1.dll`, precisará atualizar sua runtime do Visual C++.**faça da seguinte forma, nesta página: https://support.microsoft.com/en-au/help/2977003/the-latest-supported-visual-c-downloads. Especificamente, você deseja a runtime x64, que pode ser baixada em https://aka.ms/vs/17/release/vc_redist.x64.exe.
|
||||
|
||||
### Linux
|
||||
|
||||
As únicas versões suportadas do DuckStation para Linux são o AppImage e o Flatpak na página de lançamentos. Se você instalou o DuckStation de outra fonte ou distribuição (por exemplo, EmuDeck), você deve entrar em contato com o responsável para suporte, nós não temos controle sobre isso.
|
||||
|
||||
#### AppImage
|
||||
|
||||
Os AppImages requerem uma distribuição equivalente ao Ubuntu 22.04 ou mais recente para serem executados.
|
||||
|
||||
- Acesse https://github.com/stenzek/duckstation/releases/tag/latest e baixe `duckstation-x64.AppImage`.
|
||||
- Execute `chmod a+x` no AppImage baixado -- após este passo, o AppImage pode ser executado como um executável típico.
|
||||
|
||||
#### Flatpak
|
||||
|
||||
- Acesse https://github.com/stenzek/duckstation/releases/tag/latest e baixe `duckstation-x64.flatpak`.
|
||||
- Execute `flatpak install ./duckstation-x64.flatpak`.
|
||||
|
||||
ou, se você tiver o FlatHub configurado:
|
||||
- Execute `flatpak install org.duckstation.DuckStation`.
|
||||
|
||||
Use `flatpak run org.duckstation.DuckStation` para iniciar, ou selecione `DuckStation` no lançador do seu ambiente de desktop. Siga o Assistente de Configuração para começar.
|
||||
|
||||
### macOS
|
||||
|
||||
São fornecidas compilações universais do MacOS para x64 e ARM64 (Apple Silicon).
|
||||
|
||||
MacOS Big Sir (11.0) é necessário, pois também é o requisito mínimo para o Qt.
|
||||
|
||||
Para baixar:
|
||||
- Acesse https://github.com/stenzek/duckstation/releases/tag/latest e baixe `duckstation-mac-release.zip`.
|
||||
- Extraia o arquivo ZIP dando um duplo clique nele.
|
||||
- Abra o DuckStation.app, opcionalmente movendo-o para a localização desejada antes.
|
||||
- Dependendo da configuração do GateKeeper, você pode precisar clicar com o botão direito -> Abrir na primeira vez que executá-lo, já que certificados de assinatura de código estão fora de questão para um projeto que não gera receita alguma.
|
||||
|
||||
### Android
|
||||
|
||||
Você precisará de um dispositivo com armv7 (32 bits ARM), AArch64 (64 bits ARM) ou x86_64 (64 bits x86). 64 bits são preferíveis, os requisitos são mais altos para 32 bits, você provavelmente vai querer pelo menos um CPU de 1,5 GHz.
|
||||
|
||||
A distribuição pelo Google Play é o mecanismo de distribuição recomendado e resultará em tamanhos de download menores: https://play.google.com/store/apps/details?id=com.github.stenzek.duckstation
|
||||
|
||||
**Não é fornecido suporte para o aplicativo Android**, ele é gratuito e suas expectativas devem estar alinhadas com isso. Por favor, **não** me envie e-mails sobre problemas relacionados a ele, eles serão ignorados.
|
||||
|
||||
Se você precisar usar um APK, os links para download estão listados em https://www.duckstation.org/android/
|
||||
|
||||
Para usar:
|
||||
1. Instale e execute o aplicativo pela primeira vez.
|
||||
2. Adicione diretórios de jogos tocando no botão de adição e selecionando um diretório. Você pode adicionar diretórios adicionais depois selecionando "Editar Diretórios de Jogos" no menu.
|
||||
3. Toque em um jogo para começar. Quando você inicia um jogo pela primeira vez, ele pedirá para importar uma imagem de BIOS.
|
||||
|
||||
Se você tiver um controle externo, precisará mapear os botões e analogicos nas configurações.
|
||||
|
||||
### Proteção LibCrypt e arquivos SBI
|
||||
|
||||
Alguns jogos da região PAL usam a proteção LibCrypt, que requer informações adicionais de subcanal de CD para funcionar corretamente. O não funcionamento do libcrypt geralmente se manifesta como travamentos, mas às vezes pode afetar a jogabilidade, dependendo de como o jogo o implementou.
|
||||
|
||||
Para esses jogos, certifique-se de que a imagem do CD e seu arquivo correspondente SBI (.sbi) tenham o mesmo nome e estejam na mesma pasta. O DuckStation carregará automaticamente o arquivo SBI quando ele for encontrado ao lado da imagem do CD.
|
||||
|
||||
Por exemplo, se sua imagem de disco se chamasse `Spyro3.cue`, você colocaria o arquivo SBI na mesma pasta e o nomearia como `Spyro3.sbi`.
|
||||
|
||||
## Compilação
|
||||
|
||||
### Windows
|
||||
Requisitos:
|
||||
- Visual Studio 2022
|
||||
|
||||
1. Clone o repositório: `git clone https://github.com/stenzek/duckstation.git`.
|
||||
2. Baixe o pacote de dependências em https://github.com/stenzek/duckstation-ext-qt-minimal/releases/download/latest/deps-x64.7z e extraia-o para `dep\msvc`.
|
||||
3. Abra a solução do Visual Studio `duckstation.sln` na raiz ou "Abrir Pasta" para a compilação com CMake.
|
||||
4. Compile a solução.
|
||||
5. Os binários estão localizados em `bin/x64`.
|
||||
6. Execute `duckstation-qt-x64-Release.exe` ou a configuração que você usou.
|
||||
|
||||
### Linux
|
||||
Requisitos (nomes de pacotes Debian/Ubuntu):
|
||||
- CMake (`cmake`)
|
||||
- SDL2 (pelo menos a versão 2.28.2) (`libsdl2-dev` `libxrandr-dev`)
|
||||
- pkgconfig (`pkg-config`)
|
||||
- Qt 6 (pelo menos a versão 6.5.1) (`qt6-base-dev` `qt6-base-private-dev` `qt6-base-dev-tools` `qt6-tools-dev` `libqt6svg6`)
|
||||
- git (`git`) (Nota: necessário para clonar o repositório e na hora da compilação)
|
||||
- Quando o Wayland estiver habilitado (padrão): (`libwayland-dev` `libwayland-egl-backend-dev` `extra-cmake-modules` `qt6-wayland`)
|
||||
- libcurl (`libcurl4-openssl-dev`)
|
||||
- Opcional para compilação mais rápida: Ninja (`ninja-build`)
|
||||
|
||||
1. Clone o repositório: `git clone https://github.com/stenzek/duckstation.git -b dev`.
|
||||
2. Crie um diretório de compilação, seja dentro ou fora do diretório de origem.
|
||||
3. Execute o CMake para configurar o sistema de compilação. Supondo que o diretório de compilação seja `build-release`, execute `cmake -Bbuild-release -DCMAKE_BUILD_TYPE=Release`. Se você tiver o Ninja instalado, adicione `-GNinja` ao final da linha de comando do CMake para compilações mais rápidas.
|
||||
4. Compile o código-fonte. Para o exemplo acima, execute `cmake --build build-release --parallel`.
|
||||
5. Execute o binário, que está localizado no diretório de compilação em `bin/duckstation-qt`.
|
||||
|
||||
### macOS
|
||||
|
||||
Requisitos:
|
||||
- CMake
|
||||
- SDL2 (pelo menos a versão 2.28.2)
|
||||
- Qt 6 (pelo menos a versão 6.5.1)
|
||||
|
||||
Opcional (recomendado para compilações mais rápidas):
|
||||
- Ninja
|
||||
|
||||
1. Clone o repositório: `git clone https://github.com/stenzek/duckstation.git`.
|
||||
2. Execute o CMake para configurar o sistema de compilação: `cmake -Bbuild-release -DCMAKE_BUILD_TYPE=Release`. Você pode precisar especificar `-DQt6_DIR` dependendo do seu sistema. Se você tiver o Ninja instalado, adicione `-GNinja` ao final da linha de comando do CMake para compilações mais rápidas.
|
||||
4. Compile o código-fonte: `cmake --build build-release --parallel`.
|
||||
5. Execute o binário, que está localizado no diretório de compilação em `bin/DuckStation.app`.
|
||||
|
||||
## Diretórios de Usuários
|
||||
O "Diretório de Usuário" é onde você deve colocar suas imagens da BIOS, onde as configurações são salvas e onde os cartões de memória e estados de salvamento são salvos por padrão. Um [arquivo opcional de banco de dados de controle de jogo SDL](#sdl-game-controller-database) também pode ser colocado aqui.
|
||||
|
||||
Ele está localizado nos seguintes lugares, dependendo da plataforma que você está usando:
|
||||
|
||||
- Windows: Meus Documentos\DuckStation
|
||||
- Linux: `$XDG_DATA_HOME/duckstation`, ou `~/.local/share/duckstation`.
|
||||
- macOS: `~/Library/Application Support/DuckStation`.
|
||||
|
||||
Portanto, se você estiver usando o Linux, sugiro colocar suas imagens do BIOS em `~/.local/share/duckstation/bios`. Este diretório será criado na primeira vez que você executar o DuckStation.
|
||||
|
||||
Se você deseja usar uma compilação "portátil", onde o diretório do usuário é o mesmo onde o executável está localizado, crie um arquivo vazio chamado `portable.txt` no mesmo diretório onde o executável do DuckStation está.
|
||||
|
||||
## Associações para a interface Qt
|
||||
Seu teclado ou controle podem ser usados para simular uma variedade de controles de PlayStation. A entrada do controle é suportada através dos back-ends DInput, XInput e SDL e pode ser alterada em `Configurações -> Configurações Gerais`.
|
||||
|
||||
Para atribuir seu dispositivo de entrada, vá para `Configurações -> Configurações do Controle`. Cada um dos botões/eixos para o controle emulador será listado, juntamente com a tecla/botão correspondente do seu dispositivo a que ele atualmente em uso. Para atribuir novamente, clique na caixa ao lado do nome do botão/eixo e pressione a tecla ou botão do seu dispositivo de entrada que deseja atribuir. Ao atribuir a vibração, basta pressionar qualquer botão no controle para o qual você deseja que seja configurado.
|
||||
|
||||
## Banco de Dados de Controle de Jogo SDL
|
||||
Os lançamentos do DuckStation incluem um banco de dados de mapeamentos de controle de jogo para o back-end do controle SDL, cortesia de https://github.com/gabomdq/SDL_GameControllerDB. O arquivo `gamecontrollerdb.txt` incluído pode ser encontrado no subdiretório `database` do diretório do programa DuckStation.
|
||||
|
||||
Se você estiver tendo problemas para associar seu controle com o back-end do controlador SDL, pode ser necessário adicionar um mapeamento personalizado ao arquivo de banco de dados. Faça uma cópia de `gamecontrollerdb.txt` e coloque-o no seu [diretório de usuário](#user-directories) (ou diretamente no diretório do programa, se estiver executando em modo portátil) e siga as instruções no [repositório SDL_GameControllerDB](https://github.com/gabomdq/SDL_GameControllerDB) para criar um novo mapeamento. Adicione este mapeamento à nova cópia de `gamecontrollerdb.txt` e seu controle deve ser reconhecido corretamente.
|
||||
|
||||
## Atribuições padrão
|
||||
Controle 1:
|
||||
- **D-Pad:** W/A/S/D
|
||||
- **Triângulo/Quadrado/Círculo/Cruz:** Numpad8/Numpad4/Numpad6/Numpad2
|
||||
- **L1/R1:** Q/E
|
||||
- **L2/R2:** 1/3
|
||||
- **Start:** Enter
|
||||
- **Select:** Backspace
|
||||
|
||||
Atalhos:
|
||||
- **Esc:** Abrir Menu de Pausa
|
||||
- **F11:** Alternar Tela Cheia
|
||||
- **Tab:** Desativar Temporariamente o Limitador de Velocidade
|
||||
- **Espaço:** Pausar/Continuar Emulação
|
||||
|
||||
## Avisos Legais
|
||||
|
||||
Ícone por icons8: https://icons8.com/icon/74847/platforms.undefined.short-title
|
||||
|
||||
"PlayStation" e "PSX" são marcas registradas da Sony Interactive Entertainment Europe Limited. Este projeto não está afiliado de forma alguma com a Sony Interactive Entertainment.
|
||||
14
android/.gitignore
vendored
Normal file
14
android/.gitignore
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
1
android/.idea/.name
generated
Normal file
1
android/.idea/.name
generated
Normal file
@@ -0,0 +1 @@
|
||||
DuckStation
|
||||
116
android/.idea/codeStyles/Project.xml
generated
Normal file
116
android/.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,116 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<codeStyleSettings language="XML">
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
5
android/.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
android/.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
||||
21
android/.idea/gradle.xml
generated
Normal file
21
android/.idea/gradle.xml
generated
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="PLATFORM" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
<option name="useQualifiedModuleNames" value="true" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
25
android/.idea/jarRepositories.xml
generated
Normal file
25
android/.idea/jarRepositories.xml
generated
Normal file
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="BintrayJCenter" />
|
||||
<option name="name" value="BintrayJCenter" />
|
||||
<option name="url" value="https://jcenter.bintray.com/" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="Google" />
|
||||
<option name="name" value="Google" />
|
||||
<option name="url" value="https://dl.google.com/dl/android/maven2/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
||||
9
android/.idea/misc.xml
generated
Normal file
9
android/.idea/misc.xml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
||||
12
android/.idea/runConfigurations.xml
generated
Normal file
12
android/.idea/runConfigurations.xml
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
6
android/.idea/vcs.xml
generated
Normal file
6
android/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
1
android/app/.gitignore
vendored
Normal file
1
android/app/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
83
android/app/build.gradle
Normal file
83
android/app/build.gradle
Normal file
@@ -0,0 +1,83 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
android {
|
||||
compileSdkVersion 29
|
||||
buildToolsVersion "29.0.2"
|
||||
defaultConfig {
|
||||
applicationId "com.github.stenzek.duckstation"
|
||||
minSdkVersion 23
|
||||
targetSdkVersion 29
|
||||
versionCode(getBuildVersionCode())
|
||||
versionName "${getVersion()}"
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "../../CMakeLists.txt"
|
||||
version "3.10.2"
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
defaultConfig {
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
arguments "-DCMAKE_BUILD_TYPE=Release"
|
||||
abiFilters "arm64-v8a", "armeabi-v7a"
|
||||
}
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
main.assets.srcDirs += "../../data"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'androidx.preference:preference:1.1.0-alpha05'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation "androidx.viewpager2:viewpager2:1.0.0"
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.0'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
|
||||
}
|
||||
|
||||
// Adapted from Dolphin.
|
||||
|
||||
def getVersion() {
|
||||
def versionNumber = '0.0-unknown'
|
||||
|
||||
try {
|
||||
versionNumber = 'git describe --tags --exclude latest --exclude preview'.execute([], project.rootDir).text
|
||||
.trim()
|
||||
.replaceAll(/(-0)?-[^-]+$/, "")
|
||||
} catch (Exception e) {
|
||||
logger.error('Cannot find git, defaulting to dummy version number')
|
||||
}
|
||||
|
||||
return versionNumber
|
||||
}
|
||||
|
||||
|
||||
def getBuildVersionCode() {
|
||||
try {
|
||||
def versionNumber = 'git rev-list --first-parent --count HEAD'.execute([], project.rootDir).text
|
||||
.trim()
|
||||
return Integer.valueOf(versionNumber);
|
||||
} catch (Exception e) {
|
||||
logger.error('Cannot find git, defaulting to dummy version number')
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
21
android/app/proguard-rules.pro
vendored
Normal file
21
android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
|
||||
assertEquals("com.github.stenzek.duckstation", appContext.getPackageName());
|
||||
}
|
||||
}
|
||||
23
android/app/src/cpp/CMakeLists.txt
Normal file
23
android/app/src/cpp/CMakeLists.txt
Normal file
@@ -0,0 +1,23 @@
|
||||
set(SRCS
|
||||
android_controller_interface.cpp
|
||||
android_controller_interface.h
|
||||
android_host_interface.cpp
|
||||
android_host_interface.h
|
||||
android_progress_callback.cpp
|
||||
android_progress_callback.h
|
||||
android_settings_interface.cpp
|
||||
android_settings_interface.h
|
||||
)
|
||||
|
||||
add_library(duckstation-native SHARED ${SRCS})
|
||||
target_link_libraries(duckstation-native PRIVATE android frontend-common core common glad imgui)
|
||||
|
||||
find_package(OpenSLES)
|
||||
if(OPENSLES_FOUND)
|
||||
message("Enabling OpenSL ES audio stream")
|
||||
target_sources(duckstation-native PRIVATE
|
||||
opensles_audio_stream.cpp
|
||||
opensles_audio_stream.h)
|
||||
target_link_libraries(duckstation-native PRIVATE OpenSLES::OpenSLES)
|
||||
target_compile_definitions(duckstation-native PRIVATE "-DUSE_OPENSLES=1")
|
||||
endif()
|
||||
186
android/app/src/cpp/android_controller_interface.cpp
Normal file
186
android/app/src/cpp/android_controller_interface.cpp
Normal file
@@ -0,0 +1,186 @@
|
||||
#include "android_controller_interface.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/file_system.h"
|
||||
#include "common/log.h"
|
||||
#include "core/controller.h"
|
||||
#include "core/host_interface.h"
|
||||
#include "core/system.h"
|
||||
#include <cmath>
|
||||
Log_SetChannel(AndroidControllerInterface);
|
||||
|
||||
AndroidControllerInterface::AndroidControllerInterface() = default;
|
||||
|
||||
AndroidControllerInterface::~AndroidControllerInterface() = default;
|
||||
|
||||
ControllerInterface::Backend AndroidControllerInterface::GetBackend() const
|
||||
{
|
||||
return ControllerInterface::Backend::Android;
|
||||
}
|
||||
|
||||
bool AndroidControllerInterface::Initialize(CommonHostInterface* host_interface)
|
||||
{
|
||||
if (!ControllerInterface::Initialize(host_interface))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void AndroidControllerInterface::Shutdown()
|
||||
{
|
||||
ControllerInterface::Shutdown();
|
||||
}
|
||||
|
||||
void AndroidControllerInterface::PollEvents() {}
|
||||
|
||||
void AndroidControllerInterface::ClearBindings()
|
||||
{
|
||||
for (ControllerData& cd : m_controllers)
|
||||
{
|
||||
cd.axis_mapping.fill({});
|
||||
cd.button_mapping.fill({});
|
||||
cd.axis_button_mapping.fill({});
|
||||
cd.button_axis_mapping.fill({});
|
||||
}
|
||||
}
|
||||
|
||||
bool AndroidControllerInterface::BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side,
|
||||
AxisCallback callback)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||
return false;
|
||||
|
||||
if (axis_number < 0 || axis_number >= NUM_AXISES)
|
||||
return false;
|
||||
|
||||
m_controllers[controller_index].axis_mapping[axis_number][axis_side] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AndroidControllerInterface::BindControllerButton(int controller_index, int button_number, ButtonCallback callback)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||
return false;
|
||||
|
||||
if (button_number < 0 || button_number >= NUM_BUTTONS)
|
||||
return false;
|
||||
|
||||
m_controllers[controller_index].button_mapping[button_number] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AndroidControllerInterface::BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||
ButtonCallback callback)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||
return false;
|
||||
|
||||
if (axis_number < 0 || axis_number >= NUM_AXISES)
|
||||
return false;
|
||||
|
||||
m_controllers[controller_index].axis_button_mapping[axis_number][BoolToUInt8(direction)] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AndroidControllerInterface::BindControllerHatToButton(int controller_index, int hat_number,
|
||||
std::string_view hat_position, ButtonCallback callback)
|
||||
{
|
||||
// Hats don't exist in XInput
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AndroidControllerInterface::BindControllerButtonToAxis(int controller_index, int button_number,
|
||||
AxisCallback callback)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= m_controllers.size())
|
||||
return false;
|
||||
|
||||
if (button_number < 0 || button_number >= NUM_BUTTONS)
|
||||
return false;
|
||||
|
||||
m_controllers[controller_index].button_axis_mapping[button_number] = std::move(callback);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AndroidControllerInterface::HandleAxisEvent(u32 index, u32 axis, float value)
|
||||
{
|
||||
Log_DevPrintf("controller %u axis %u %f", index, static_cast<u32>(axis), value);
|
||||
DebugAssert(index < NUM_CONTROLLERS);
|
||||
|
||||
if (DoEventHook(Hook::Type::Axis, index, static_cast<u32>(axis), value))
|
||||
return true;
|
||||
|
||||
const AxisCallback& cb = m_controllers[index].axis_mapping[static_cast<u32>(axis)][AxisSide::Full];
|
||||
if (cb)
|
||||
{
|
||||
cb(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
// set the other direction to false so large movements don't leave the opposite on
|
||||
const bool outside_deadzone = (std::abs(value) >= m_controllers[index].deadzone);
|
||||
const bool positive = (value >= 0.0f);
|
||||
const ButtonCallback& other_button_cb =
|
||||
m_controllers[index].axis_button_mapping[static_cast<u32>(axis)][BoolToUInt8(!positive)];
|
||||
const ButtonCallback& button_cb =
|
||||
m_controllers[index].axis_button_mapping[static_cast<u32>(axis)][BoolToUInt8(positive)];
|
||||
if (button_cb)
|
||||
{
|
||||
button_cb(outside_deadzone);
|
||||
if (other_button_cb)
|
||||
other_button_cb(false);
|
||||
return true;
|
||||
}
|
||||
else if (other_button_cb)
|
||||
{
|
||||
other_button_cb(false);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool AndroidControllerInterface::HandleButtonEvent(u32 index, u32 button, bool pressed)
|
||||
{
|
||||
Log_DevPrintf("controller %u button %u %s", index, button, pressed ? "pressed" : "released");
|
||||
DebugAssert(index < NUM_CONTROLLERS);
|
||||
|
||||
if (DoEventHook(Hook::Type::Button, index, button, pressed ? 1.0f : 0.0f))
|
||||
return true;
|
||||
|
||||
const ButtonCallback& cb = m_controllers[index].button_mapping[button];
|
||||
if (cb)
|
||||
{
|
||||
cb(pressed);
|
||||
return true;
|
||||
}
|
||||
|
||||
const AxisCallback& axis_cb = m_controllers[index].button_axis_mapping[button];
|
||||
if (axis_cb)
|
||||
{
|
||||
axis_cb(pressed ? 1.0f : -1.0f);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
u32 AndroidControllerInterface::GetControllerRumbleMotorCount(int controller_index)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
void AndroidControllerInterface::SetControllerRumbleStrength(int controller_index, const float* strengths,
|
||||
u32 num_motors)
|
||||
{
|
||||
}
|
||||
|
||||
bool AndroidControllerInterface::SetControllerDeadzone(int controller_index, float size /* = 0.25f */)
|
||||
{
|
||||
if (static_cast<u32>(controller_index) >= NUM_CONTROLLERS)
|
||||
return false;
|
||||
|
||||
m_controllers[static_cast<u32>(controller_index)].deadzone = std::clamp(std::abs(size), 0.01f, 0.99f);
|
||||
Log_InfoPrintf("Controller %d deadzone size set to %f", controller_index,
|
||||
m_controllers[static_cast<u32>(controller_index)].deadzone);
|
||||
return true;
|
||||
}
|
||||
67
android/app/src/cpp/android_controller_interface.h
Normal file
67
android/app/src/cpp/android_controller_interface.h
Normal file
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
#include "frontend-common/controller_interface.h"
|
||||
#include "core/types.h"
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
class AndroidControllerInterface final : public ControllerInterface
|
||||
{
|
||||
public:
|
||||
AndroidControllerInterface();
|
||||
~AndroidControllerInterface() override;
|
||||
|
||||
Backend GetBackend() const override;
|
||||
bool Initialize(CommonHostInterface* host_interface) override;
|
||||
void Shutdown() override;
|
||||
|
||||
// Removes all bindings. Call before setting new bindings.
|
||||
void ClearBindings() override;
|
||||
|
||||
// Binding to events. If a binding for this axis/button already exists, returns false.
|
||||
bool BindControllerAxis(int controller_index, int axis_number, AxisSide axis_side, AxisCallback callback) override;
|
||||
bool BindControllerButton(int controller_index, int button_number, ButtonCallback callback) override;
|
||||
bool BindControllerAxisToButton(int controller_index, int axis_number, bool direction,
|
||||
ButtonCallback callback) override;
|
||||
bool BindControllerHatToButton(int controller_index, int hat_number, std::string_view hat_position,
|
||||
ButtonCallback callback) override;
|
||||
bool BindControllerButtonToAxis(int controller_index, int button_number, AxisCallback callback) override;
|
||||
|
||||
// Changing rumble strength.
|
||||
u32 GetControllerRumbleMotorCount(int controller_index) override;
|
||||
void SetControllerRumbleStrength(int controller_index, const float* strengths, u32 num_motors) override;
|
||||
|
||||
// Set deadzone that will be applied on axis-to-button mappings
|
||||
bool SetControllerDeadzone(int controller_index, float size = 0.25f) override;
|
||||
|
||||
void PollEvents() override;
|
||||
|
||||
bool HandleAxisEvent(u32 index, u32 axis, float value);
|
||||
bool HandleButtonEvent(u32 index, u32 button, bool pressed);
|
||||
|
||||
private:
|
||||
enum : u32
|
||||
{
|
||||
NUM_CONTROLLERS = 1,
|
||||
NUM_AXISES = 12,
|
||||
NUM_BUTTONS = 23
|
||||
};
|
||||
|
||||
struct ControllerData
|
||||
{
|
||||
float deadzone = 0.25f;
|
||||
|
||||
std::array<std::array<AxisCallback, 3>, NUM_AXISES> axis_mapping;
|
||||
std::array<ButtonCallback, NUM_BUTTONS> button_mapping;
|
||||
std::array<std::array<ButtonCallback, 2>, NUM_AXISES> axis_button_mapping;
|
||||
std::array<AxisCallback, NUM_BUTTONS> button_axis_mapping;
|
||||
};
|
||||
|
||||
using ControllerDataArray = std::array<ControllerData, NUM_CONTROLLERS>;
|
||||
|
||||
ControllerDataArray m_controllers;
|
||||
|
||||
std::mutex m_event_intercept_mutex;
|
||||
Hook::Callback m_event_intercept_callback;
|
||||
};
|
||||
1677
android/app/src/cpp/android_host_interface.cpp
Normal file
1677
android/app/src/cpp/android_host_interface.cpp
Normal file
File diff suppressed because it is too large
Load Diff
174
android/app/src/cpp/android_host_interface.h
Normal file
174
android/app/src/cpp/android_host_interface.h
Normal file
@@ -0,0 +1,174 @@
|
||||
#pragma once
|
||||
#include "android_settings_interface.h"
|
||||
#include "common/byte_stream.h"
|
||||
#include "common/event.h"
|
||||
#include "common/progress_callback.h"
|
||||
#include "core/host_display.h"
|
||||
#include "frontend-common/common_host_interface.h"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <jni.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
struct ANativeWindow;
|
||||
|
||||
class Controller;
|
||||
|
||||
class AndroidHostInterface final : public CommonHostInterface
|
||||
{
|
||||
public:
|
||||
AndroidHostInterface(jobject java_object, jobject context_object, std::string user_directory);
|
||||
~AndroidHostInterface() override;
|
||||
|
||||
ALWAYS_INLINE ANativeWindow* GetSurface() const { return m_surface; }
|
||||
|
||||
bool Initialize() override;
|
||||
void Shutdown() override;
|
||||
|
||||
const char* GetFrontendName() const override;
|
||||
void RequestExit() override;
|
||||
void RunLater(std::function<void()> func) override;
|
||||
|
||||
void ReportError(const char* message) override;
|
||||
void ReportMessage(const char* message) override;
|
||||
|
||||
std::string GetStringSettingValue(const char* section, const char* key, const char* default_value = "") override;
|
||||
bool GetBoolSettingValue(const char* section, const char* key, bool default_value = false) override;
|
||||
int GetIntSettingValue(const char* section, const char* key, int default_value = 0) override;
|
||||
float GetFloatSettingValue(const char* section, const char* key, float default_value = 0.0f) override;
|
||||
std::unique_ptr<ByteStream> OpenPackageFile(const char* path, u32 flags) override;
|
||||
bool GetMainDisplayRefreshRate(float* refresh_rate) override;
|
||||
|
||||
bool IsEmulationThreadRunning() const { return m_emulation_thread_running.load(); }
|
||||
bool IsEmulationThreadPaused() const;
|
||||
bool IsOnEmulationThread() const;
|
||||
void RunOnEmulationThread(std::function<void()> function, bool blocking = false);
|
||||
void PauseEmulationThread(bool paused);
|
||||
void StopEmulationThreadLoop();
|
||||
|
||||
void EmulationThreadEntryPoint(JNIEnv* env, jobject emulation_activity, SystemBootParameters boot_params,
|
||||
bool resume_state);
|
||||
|
||||
void SurfaceChanged(ANativeWindow* surface, int format, int width, int height);
|
||||
void SetDisplayAlignment(HostDisplay::Alignment alignment);
|
||||
|
||||
void SetControllerType(u32 index, std::string_view type_name);
|
||||
void SetControllerButtonState(u32 index, s32 button_code, bool pressed);
|
||||
void SetControllerAxisState(u32 index, s32 button_code, float value);
|
||||
void HandleControllerButtonEvent(u32 controller_index, u32 button_index, bool pressed);
|
||||
void HandleControllerAxisEvent(u32 controller_index, u32 axis_index, float value);
|
||||
void SetFastForwardEnabled(bool enabled);
|
||||
|
||||
void RefreshGameList(bool invalidate_cache, bool invalidate_database, ProgressCallback* progress_callback);
|
||||
void ApplySettings(bool display_osd_messages) override;
|
||||
|
||||
bool ImportPatchCodesFromString(const std::string& str);
|
||||
|
||||
jobjectArray GetInputProfileNames(JNIEnv* env) const;
|
||||
bool ApplyInputProfile(const char* profile_name);
|
||||
bool SaveInputProfile(const char* profile_name);
|
||||
|
||||
protected:
|
||||
void SetUserDirectory() override;
|
||||
void LoadSettings() override;
|
||||
void UpdateInputMap() override;
|
||||
void RegisterHotkeys() override;
|
||||
|
||||
bool AcquireHostDisplay() override;
|
||||
void ReleaseHostDisplay() override;
|
||||
std::unique_ptr<AudioStream> CreateAudioStream(AudioBackend backend) override;
|
||||
void UpdateControllerInterface() override;
|
||||
|
||||
void OnSystemPaused(bool paused) override;
|
||||
void OnSystemDestroyed() override;
|
||||
void OnRunningGameChanged() override;
|
||||
|
||||
private:
|
||||
void EmulationThreadLoop(JNIEnv* env);
|
||||
|
||||
void CreateImGuiContext();
|
||||
void DestroyImGuiContext();
|
||||
|
||||
void LoadAndConvertSettings();
|
||||
void SetVibration(bool enabled);
|
||||
void UpdateVibration();
|
||||
|
||||
jobject m_java_object = {};
|
||||
jobject m_emulation_activity_object = {};
|
||||
|
||||
AndroidSettingsInterface m_settings_interface;
|
||||
|
||||
ANativeWindow* m_surface = nullptr;
|
||||
|
||||
std::mutex m_mutex;
|
||||
std::condition_variable m_sleep_cv;
|
||||
std::deque<std::function<void()>> m_callback_queue;
|
||||
std::atomic_bool m_callbacks_outstanding{false};
|
||||
|
||||
std::atomic_bool m_emulation_thread_stop_request{false};
|
||||
std::atomic_bool m_emulation_thread_running{false};
|
||||
std::thread::id m_emulation_thread_id{};
|
||||
|
||||
HostDisplay::Alignment m_display_alignment = HostDisplay::Alignment::Center;
|
||||
|
||||
u64 m_last_vibration_update_time = 0;
|
||||
bool m_last_vibration_state = false;
|
||||
bool m_vibration_enabled = false;
|
||||
};
|
||||
|
||||
namespace AndroidHelpers {
|
||||
|
||||
JNIEnv* GetJNIEnv();
|
||||
AndroidHostInterface* GetNativeClass(JNIEnv* env, jobject obj);
|
||||
std::string JStringToString(JNIEnv* env, jstring str);
|
||||
std::unique_ptr<GrowableMemoryByteStream> ReadInputStreamToMemory(JNIEnv* env, jobject obj, u32 chunk_size = 65536);
|
||||
jclass GetStringClass();
|
||||
|
||||
} // namespace AndroidHelpers
|
||||
|
||||
template<typename T>
|
||||
class LocalRefHolder
|
||||
{
|
||||
public:
|
||||
LocalRefHolder() : m_env(nullptr), m_object(nullptr) {}
|
||||
|
||||
LocalRefHolder(JNIEnv* env, T object) : m_env(env), m_object(object) {}
|
||||
|
||||
LocalRefHolder(const LocalRefHolder<T>&) = delete;
|
||||
LocalRefHolder(LocalRefHolder&& move) : m_env(move.m_env), m_object(move.m_object)
|
||||
{
|
||||
move.m_env = nullptr;
|
||||
move.m_object = {};
|
||||
}
|
||||
|
||||
~LocalRefHolder()
|
||||
{
|
||||
if (m_object)
|
||||
m_env->DeleteLocalRef(m_object);
|
||||
}
|
||||
|
||||
operator T() const { return m_object; }
|
||||
T operator*() const { return m_object; }
|
||||
|
||||
LocalRefHolder& operator=(const LocalRefHolder&) = delete;
|
||||
LocalRefHolder& operator=(LocalRefHolder&& move)
|
||||
{
|
||||
if (m_object)
|
||||
m_env->DeleteLocalRef(m_object);
|
||||
m_env = move.m_env;
|
||||
m_object = move.m_object;
|
||||
move.m_env = nullptr;
|
||||
move.m_object = {};
|
||||
return *this;
|
||||
}
|
||||
|
||||
T Get() const { return m_object; }
|
||||
|
||||
private:
|
||||
JNIEnv* m_env;
|
||||
T m_object;
|
||||
};
|
||||
113
android/app/src/cpp/android_progress_callback.cpp
Normal file
113
android/app/src/cpp/android_progress_callback.cpp
Normal file
@@ -0,0 +1,113 @@
|
||||
#include "android_progress_callback.h"
|
||||
#include "android_host_interface.h"
|
||||
#include "common/log.h"
|
||||
#include "common/assert.h"
|
||||
Log_SetChannel(AndroidProgressCallback);
|
||||
|
||||
AndroidProgressCallback::AndroidProgressCallback(JNIEnv* env, jobject java_object)
|
||||
: m_java_object(java_object)
|
||||
{
|
||||
jclass cls = env->GetObjectClass(java_object);
|
||||
m_set_title_method = env->GetMethodID(cls, "setTitle", "(Ljava/lang/String;)V");
|
||||
m_set_status_text_method = env->GetMethodID(cls, "setStatusText", "(Ljava/lang/String;)V");
|
||||
m_set_progress_range_method = env->GetMethodID(cls, "setProgressRange", "(I)V");
|
||||
m_set_progress_value_method = env->GetMethodID(cls, "setProgressValue", "(I)V");
|
||||
m_modal_error_method = env->GetMethodID(cls, "modalError", "(Ljava/lang/String;)V");
|
||||
m_modal_information_method = env->GetMethodID(cls, "modalInformation", "(Ljava/lang/String;)V");
|
||||
m_modal_confirmation_method = env->GetMethodID(cls, "modalConfirmation", "(Ljava/lang/String;)Z");
|
||||
Assert(m_set_status_text_method && m_set_progress_range_method && m_set_progress_value_method && m_modal_error_method && m_modal_information_method && m_modal_confirmation_method);
|
||||
}
|
||||
|
||||
AndroidProgressCallback::~AndroidProgressCallback() = default;
|
||||
|
||||
bool AndroidProgressCallback::IsCancelled() const
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::SetCancellable(bool cancellable)
|
||||
{
|
||||
if (m_cancellable == cancellable)
|
||||
return;
|
||||
|
||||
BaseProgressCallback::SetCancellable(cancellable);
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::SetTitle(const char* title)
|
||||
{
|
||||
Assert(title);
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> text_jstr(env, env->NewStringUTF(title));
|
||||
env->CallVoidMethod(m_java_object, m_set_title_method, text_jstr.Get());
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::SetStatusText(const char* text)
|
||||
{
|
||||
Assert(text);
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> text_jstr(env, env->NewStringUTF(text));
|
||||
env->CallVoidMethod(m_java_object, m_set_status_text_method, text_jstr.Get());
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::SetProgressRange(u32 range)
|
||||
{
|
||||
BaseProgressCallback::SetProgressRange(range);
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
env->CallVoidMethod(m_java_object, m_set_progress_range_method, static_cast<jint>(range));
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::SetProgressValue(u32 value)
|
||||
{
|
||||
const u32 old_value = m_progress_value;
|
||||
BaseProgressCallback::SetProgressValue(value);
|
||||
if (old_value == m_progress_value)
|
||||
return;
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
env->CallVoidMethod(m_java_object, m_set_progress_value_method, static_cast<jint>(value));
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::DisplayError(const char* message)
|
||||
{
|
||||
Log_ErrorPrintf("%s", message);
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::DisplayWarning(const char* message)
|
||||
{
|
||||
Log_WarningPrintf("%s", message);
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::DisplayInformation(const char* message)
|
||||
{
|
||||
Log_InfoPrintf("%s", message);
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::DisplayDebugMessage(const char* message)
|
||||
{
|
||||
Log_DevPrintf("%s", message);
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::ModalError(const char* message)
|
||||
{
|
||||
Assert(message);
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> message_jstr(env, env->NewStringUTF(message));
|
||||
env->CallVoidMethod(m_java_object, m_modal_error_method, message_jstr.Get());
|
||||
}
|
||||
|
||||
bool AndroidProgressCallback::ModalConfirmation(const char* message)
|
||||
{
|
||||
Assert(message);
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> message_jstr(env, env->NewStringUTF(message));
|
||||
return env->CallBooleanMethod(m_java_object, m_modal_confirmation_method, message_jstr.Get());
|
||||
}
|
||||
|
||||
void AndroidProgressCallback::ModalInformation(const char* message)
|
||||
{
|
||||
Assert(message);
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> message_jstr(env, env->NewStringUTF(message));
|
||||
env->CallVoidMethod(m_java_object, m_modal_information_method, message_jstr.Get());
|
||||
}
|
||||
38
android/app/src/cpp/android_progress_callback.h
Normal file
38
android/app/src/cpp/android_progress_callback.h
Normal file
@@ -0,0 +1,38 @@
|
||||
#pragma once
|
||||
#include "common/progress_callback.h"
|
||||
#include <jni.h>
|
||||
|
||||
class AndroidProgressCallback final : public BaseProgressCallback
|
||||
{
|
||||
public:
|
||||
AndroidProgressCallback(JNIEnv* env, jobject java_object);
|
||||
~AndroidProgressCallback();
|
||||
|
||||
bool IsCancelled() const override;
|
||||
|
||||
void SetCancellable(bool cancellable) override;
|
||||
void SetTitle(const char* title) override;
|
||||
void SetStatusText(const char* text) override;
|
||||
void SetProgressRange(u32 range) override;
|
||||
void SetProgressValue(u32 value) override;
|
||||
|
||||
void DisplayError(const char* message) override;
|
||||
void DisplayWarning(const char* message) override;
|
||||
void DisplayInformation(const char* message) override;
|
||||
void DisplayDebugMessage(const char* message) override;
|
||||
|
||||
void ModalError(const char* message) override;
|
||||
bool ModalConfirmation(const char* message) override;
|
||||
void ModalInformation(const char* message) override;
|
||||
|
||||
private:
|
||||
jobject m_java_object;
|
||||
|
||||
jmethodID m_set_title_method;
|
||||
jmethodID m_set_status_text_method;
|
||||
jmethodID m_set_progress_range_method;
|
||||
jmethodID m_set_progress_value_method;
|
||||
jmethodID m_modal_error_method;
|
||||
jmethodID m_modal_confirmation_method;
|
||||
jmethodID m_modal_information_method;
|
||||
};
|
||||
407
android/app/src/cpp/android_settings_interface.cpp
Normal file
407
android/app/src/cpp/android_settings_interface.cpp
Normal file
@@ -0,0 +1,407 @@
|
||||
#include "android_settings_interface.h"
|
||||
#include "android_host_interface.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/log.h"
|
||||
#include "common/string.h"
|
||||
#include "common/string_util.h"
|
||||
#include <algorithm>
|
||||
Log_SetChannel(AndroidSettingsInterface);
|
||||
|
||||
ALWAYS_INLINE TinyString GetSettingKey(const char* section, const char* key)
|
||||
{
|
||||
return TinyString::FromFormat("%s/%s", section, key);
|
||||
}
|
||||
|
||||
AndroidSettingsInterface::AndroidSettingsInterface(jobject java_context)
|
||||
{
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
jclass c_preference_manager = env->FindClass("androidx/preference/PreferenceManager");
|
||||
jclass c_preference_editor = env->FindClass("android/content/SharedPreferences$Editor");
|
||||
jclass c_set = env->FindClass("java/util/Set");
|
||||
jclass c_helper = env->FindClass("com/github/stenzek/duckstation/PreferenceHelpers");
|
||||
jmethodID m_get_default_shared_preferences =
|
||||
env->GetStaticMethodID(c_preference_manager, "getDefaultSharedPreferences",
|
||||
"(Landroid/content/Context;)Landroid/content/SharedPreferences;");
|
||||
Assert(c_preference_manager && c_preference_editor && c_set && c_helper && m_get_default_shared_preferences);
|
||||
m_set_class = reinterpret_cast<jclass>(env->NewGlobalRef(c_set));
|
||||
m_shared_preferences_editor_class = reinterpret_cast<jclass>(env->NewGlobalRef(c_preference_editor));
|
||||
m_helper_class = reinterpret_cast<jclass>(env->NewGlobalRef(c_helper));
|
||||
Assert(m_set_class && m_shared_preferences_editor_class && m_helper_class);
|
||||
|
||||
env->DeleteLocalRef(c_set);
|
||||
env->DeleteLocalRef(c_preference_editor);
|
||||
env->DeleteLocalRef(c_helper);
|
||||
|
||||
jobject shared_preferences =
|
||||
env->CallStaticObjectMethod(c_preference_manager, m_get_default_shared_preferences, java_context);
|
||||
Assert(shared_preferences);
|
||||
m_java_shared_preferences = env->NewGlobalRef(shared_preferences);
|
||||
Assert(m_java_shared_preferences);
|
||||
env->DeleteLocalRef(c_preference_manager);
|
||||
env->DeleteLocalRef(shared_preferences);
|
||||
|
||||
jclass c_shared_preferences = env->GetObjectClass(m_java_shared_preferences);
|
||||
m_shared_preferences_class = reinterpret_cast<jclass>(env->NewGlobalRef(c_shared_preferences));
|
||||
Assert(m_shared_preferences_class);
|
||||
env->DeleteLocalRef(c_shared_preferences);
|
||||
|
||||
m_get_boolean = env->GetMethodID(m_shared_preferences_class, "getBoolean", "(Ljava/lang/String;Z)Z");
|
||||
m_get_int = env->GetMethodID(m_shared_preferences_class, "getInt", "(Ljava/lang/String;I)I");
|
||||
m_get_float = env->GetMethodID(m_shared_preferences_class, "getFloat", "(Ljava/lang/String;F)F");
|
||||
m_get_string = env->GetMethodID(m_shared_preferences_class, "getString",
|
||||
"(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
|
||||
m_get_string_set =
|
||||
env->GetMethodID(m_shared_preferences_class, "getStringSet", "(Ljava/lang/String;Ljava/util/Set;)Ljava/util/Set;");
|
||||
m_set_to_array = env->GetMethodID(m_set_class, "toArray", "()[Ljava/lang/Object;");
|
||||
Assert(m_get_boolean && m_get_int && m_get_float && m_get_string && m_get_string_set && m_set_to_array);
|
||||
|
||||
m_edit = env->GetMethodID(m_shared_preferences_class, "edit", "()Landroid/content/SharedPreferences$Editor;");
|
||||
m_edit_set_string = env->GetMethodID(m_shared_preferences_editor_class, "putString", "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;");
|
||||
m_edit_commit = env->GetMethodID(m_shared_preferences_editor_class, "commit", "()Z");
|
||||
m_edit_remove = env->GetMethodID(m_shared_preferences_editor_class, "remove", "(Ljava/lang/String;)Landroid/content/SharedPreferences$Editor;");
|
||||
Assert(m_edit && m_edit_set_string && m_edit_commit && m_edit_remove);
|
||||
|
||||
m_helper_clear_section = env->GetStaticMethodID(m_helper_class, "clearSection", "(Landroid/content/SharedPreferences;Ljava/lang/String;)V");
|
||||
m_helper_add_to_string_list = env->GetStaticMethodID(m_helper_class, "addToStringList", "(Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/lang/String;)Z");
|
||||
m_helper_remove_from_string_list = env->GetStaticMethodID(m_helper_class, "removeFromStringList", "(Landroid/content/SharedPreferences;Ljava/lang/String;Ljava/lang/String;)Z");
|
||||
m_helper_set_string_list = env->GetStaticMethodID(m_helper_class, "setStringList", "(Landroid/content/SharedPreferences;Ljava/lang/String;[Ljava/lang/String;)V");
|
||||
Assert(m_helper_clear_section && m_helper_add_to_string_list && m_helper_remove_from_string_list && m_helper_set_string_list);
|
||||
}
|
||||
|
||||
AndroidSettingsInterface::~AndroidSettingsInterface()
|
||||
{
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
if (m_java_shared_preferences)
|
||||
env->DeleteGlobalRef(m_java_shared_preferences);
|
||||
if (m_shared_preferences_editor_class)
|
||||
env->DeleteGlobalRef(m_shared_preferences_editor_class);
|
||||
if (m_shared_preferences_class)
|
||||
env->DeleteGlobalRef(m_shared_preferences_class);
|
||||
if (m_set_class)
|
||||
env->DeleteGlobalRef(m_set_class);
|
||||
if (m_helper_class)
|
||||
env->DeleteGlobalRef(m_helper_class);
|
||||
}
|
||||
|
||||
bool AndroidSettingsInterface::Save()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::Clear()
|
||||
{
|
||||
Log_ErrorPrint("Not implemented");
|
||||
}
|
||||
|
||||
int AndroidSettingsInterface::GetIntValue(const char* section, const char* key, int default_value /*= 0*/)
|
||||
{
|
||||
// Some of these settings are string lists...
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> default_value_string(env, env->NewStringUTF(TinyString::FromFormat("%d", default_value)));
|
||||
LocalRefHolder<jstring> string_object(
|
||||
env, reinterpret_cast<jstring>(env->CallObjectMethod(m_java_shared_preferences, m_get_string, key_string.Get(),
|
||||
default_value_string.Get())));
|
||||
if (env->ExceptionCheck())
|
||||
{
|
||||
env->ExceptionClear();
|
||||
|
||||
// it might actually be an int (e.g. seek bar preference)
|
||||
const int int_value =
|
||||
static_cast<int>(env->CallIntMethod(m_java_shared_preferences, m_get_int, key_string.Get(), default_value));
|
||||
if (env->ExceptionCheck())
|
||||
{
|
||||
env->ExceptionClear();
|
||||
Log_DevPrintf("GetIntValue(%s, %s) -> %d (exception)", section, key, default_value);
|
||||
return default_value;
|
||||
}
|
||||
|
||||
Log_DevPrintf("GetIntValue(%s, %s) -> %d (int)", section, key, int_value);
|
||||
return int_value;
|
||||
}
|
||||
|
||||
if (!string_object)
|
||||
return default_value;
|
||||
|
||||
const char* data = env->GetStringUTFChars(string_object, nullptr);
|
||||
Assert(data != nullptr);
|
||||
Log_DevPrintf("GetIntValue(%s, %s) -> %s", section, key, data);
|
||||
|
||||
std::optional<int> value = StringUtil::FromChars<int>(data);
|
||||
env->ReleaseStringUTFChars(string_object, data);
|
||||
return value.value_or(default_value);
|
||||
}
|
||||
|
||||
float AndroidSettingsInterface::GetFloatValue(const char* section, const char* key, float default_value /*= 0.0f*/)
|
||||
{
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> default_value_string(env, env->NewStringUTF(TinyString::FromFormat("%f", default_value)));
|
||||
LocalRefHolder<jstring> string_object(
|
||||
env, reinterpret_cast<jstring>(env->CallObjectMethod(m_java_shared_preferences, m_get_string, key_string.Get(),
|
||||
default_value_string.Get())));
|
||||
if (env->ExceptionCheck())
|
||||
{
|
||||
env->ExceptionClear();
|
||||
Log_DevPrintf("GetFloatValue(%s, %s) -> %f (exception)", section, key, default_value);
|
||||
return default_value;
|
||||
}
|
||||
|
||||
if (!string_object)
|
||||
{
|
||||
Log_DevPrintf("GetFloatValue(%s, %s) -> %f (null)", section, key, default_value);
|
||||
return default_value;
|
||||
}
|
||||
|
||||
const char* data = env->GetStringUTFChars(string_object, nullptr);
|
||||
Assert(data != nullptr);
|
||||
Log_DevPrintf("GetFloatValue(%s, %s) -> %s", section, key, data);
|
||||
|
||||
std::optional<float> value = StringUtil::FromChars<float>(data);
|
||||
env->ReleaseStringUTFChars(string_object, data);
|
||||
return value.value_or(default_value);
|
||||
}
|
||||
|
||||
bool AndroidSettingsInterface::GetBoolValue(const char* section, const char* key, bool default_value /*= false*/)
|
||||
{
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
jboolean bool_value = static_cast<bool>(
|
||||
env->CallBooleanMethod(m_java_shared_preferences, m_get_boolean, key_string.Get(), default_value));
|
||||
if (env->ExceptionCheck())
|
||||
{
|
||||
Log_DevPrintf("GetBoolValue(%s, %s) -> %u (exception)", section, key, static_cast<unsigned>(default_value));
|
||||
env->ExceptionClear();
|
||||
return default_value;
|
||||
}
|
||||
|
||||
Log_DevPrintf("GetBoolValue(%s, %s) -> %u", section, key, static_cast<unsigned>(bool_value));
|
||||
return bool_value;
|
||||
}
|
||||
|
||||
std::string AndroidSettingsInterface::GetStringValue(const char* section, const char* key,
|
||||
const char* default_value /*= ""*/)
|
||||
{
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> default_value_string(env, env->NewStringUTF(default_value));
|
||||
LocalRefHolder<jstring> string_object(
|
||||
env, reinterpret_cast<jstring>(env->CallObjectMethod(m_java_shared_preferences, m_get_string, key_string.Get(),
|
||||
default_value_string.Get())));
|
||||
|
||||
if (env->ExceptionCheck())
|
||||
{
|
||||
env->ExceptionClear();
|
||||
Log_DevPrintf("GetStringValue(%s, %s) -> %s (exception)", section, key, default_value);
|
||||
return default_value;
|
||||
}
|
||||
|
||||
if (!string_object)
|
||||
{
|
||||
Log_DevPrintf("GetStringValue(%s, %s) -> %s (null)", section, key, default_value);
|
||||
return default_value;
|
||||
}
|
||||
|
||||
const std::string ret(AndroidHelpers::JStringToString(env, string_object));
|
||||
Log_DevPrintf("GetStringValue(%s, %s) -> %s", section, key, ret.c_str());
|
||||
return ret;
|
||||
}
|
||||
|
||||
jobject AndroidSettingsInterface::GetPreferencesEditor(JNIEnv* env)
|
||||
{
|
||||
return env->CallObjectMethod(m_java_shared_preferences, m_edit);
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::CheckForException(JNIEnv *env, const char *task)
|
||||
{
|
||||
if (!env->ExceptionCheck())
|
||||
return;
|
||||
|
||||
Log_ErrorPrintf("JNI exception during %s", task);
|
||||
env->ExceptionClear();
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::SetIntValue(const char* section, const char* key, int value)
|
||||
{
|
||||
Log_DevPrintf("SetIntValue(\"%s\", \"%s\", %d)", section, key, value);
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jobject> editor(env, GetPreferencesEditor(env));
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> str_value(env, env->NewStringUTF(TinyString::FromFormat("%d", value)));
|
||||
|
||||
LocalRefHolder<jobject> dummy(env, env->CallObjectMethod(editor, m_edit_set_string, key_string.Get(), str_value.Get()));
|
||||
env->CallBooleanMethod(editor, m_edit_commit);
|
||||
|
||||
CheckForException(env, "SetIntValue");
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::SetFloatValue(const char* section, const char* key, float value)
|
||||
{
|
||||
Log_DevPrintf("SetFloatValue(\"%s\", \"%s\", %f)", section, key, value);
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jobject> editor(env, GetPreferencesEditor(env));
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> str_value(env, env->NewStringUTF(TinyString::FromFormat("%f", value)));
|
||||
|
||||
LocalRefHolder<jobject> dummy(env, env->CallObjectMethod(editor, m_edit_set_string, key_string.Get(), str_value.Get()));
|
||||
env->CallBooleanMethod(editor, m_edit_commit);
|
||||
|
||||
CheckForException(env, "SetFloatValue");
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::SetBoolValue(const char* section, const char* key, bool value)
|
||||
{
|
||||
Log_DevPrintf("SetBoolValue(\"%s\", \"%s\", %u)", section, key, static_cast<unsigned>(value));
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jobject> editor(env, GetPreferencesEditor(env));
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> str_value(env, env->NewStringUTF(value ? "true" : "false"));
|
||||
|
||||
LocalRefHolder<jobject> dummy(env, env->CallObjectMethod(editor, m_edit_set_string, key_string.Get(), str_value.Get()));
|
||||
env->CallBooleanMethod(editor, m_edit_commit);
|
||||
|
||||
CheckForException(env, "SetBoolValue");
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::SetStringValue(const char* section, const char* key, const char* value)
|
||||
{
|
||||
Log_DevPrintf("SetStringValue(\"%s\", \"%s\", \"%s\")", section, key, value);
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jobject> editor(env, GetPreferencesEditor(env));
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> str_value(env, env->NewStringUTF(value));
|
||||
|
||||
LocalRefHolder<jobject> dummy(env, env->CallObjectMethod(editor, m_edit_set_string, key_string.Get(), str_value.Get()));
|
||||
env->CallBooleanMethod(editor, m_edit_commit);
|
||||
|
||||
CheckForException(env, "SetStringValue");
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::DeleteValue(const char* section, const char* key)
|
||||
{
|
||||
Log_DevPrintf("DeleteValue(\"%s\", \"%s\")", section, key);
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jobject> editor(env, GetPreferencesEditor(env));
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jobject> dummy(env, env->CallObjectMethod(editor, m_edit_remove, key_string.Get()));
|
||||
env->CallBooleanMethod(editor, m_edit_commit);
|
||||
|
||||
CheckForException(env, "DeleteValue");
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::ClearSection(const char* section)
|
||||
{
|
||||
Log_DevPrintf("ClearSection(\"%s\")", section);
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> str_section(env, env->NewStringUTF(section));
|
||||
env->CallStaticVoidMethod(m_helper_class, m_helper_clear_section, m_java_shared_preferences, str_section.Get());
|
||||
|
||||
CheckForException(env, "ClearSection");
|
||||
}
|
||||
|
||||
std::vector<std::string> AndroidSettingsInterface::GetStringList(const char* section, const char* key)
|
||||
{
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jobject> values_set(
|
||||
env, env->CallObjectMethod(m_java_shared_preferences, m_get_string_set, key_string.Get(), nullptr));
|
||||
if (env->ExceptionCheck())
|
||||
{
|
||||
env->ExceptionClear();
|
||||
|
||||
// this might just be a string, not a string set
|
||||
LocalRefHolder<jstring> string_object(
|
||||
env, reinterpret_cast<jstring>(env->CallObjectMethod(m_java_shared_preferences, m_get_string, key_string.Get(), nullptr)));
|
||||
|
||||
if (!env->ExceptionCheck()) {
|
||||
std::vector<std::string> ret;
|
||||
if (string_object)
|
||||
ret.push_back(AndroidHelpers::JStringToString(env, string_object));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
env->ExceptionClear();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!values_set)
|
||||
return {};
|
||||
|
||||
LocalRefHolder<jobjectArray> values_array(
|
||||
env, reinterpret_cast<jobjectArray>(env->CallObjectMethod(values_set, m_set_to_array)));
|
||||
if (env->ExceptionCheck())
|
||||
{
|
||||
env->ExceptionClear();
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!values_array)
|
||||
return {};
|
||||
|
||||
jsize size = env->GetArrayLength(values_array);
|
||||
std::vector<std::string> values;
|
||||
values.reserve(size);
|
||||
for (jsize i = 0; i < size; i++)
|
||||
{
|
||||
jstring str = reinterpret_cast<jstring>(env->GetObjectArrayElement(values_array, i));
|
||||
values.push_back(AndroidHelpers::JStringToString(env, str));
|
||||
env->DeleteLocalRef(str);
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
void AndroidSettingsInterface::SetStringList(const char* section, const char* key,
|
||||
const std::vector<std::string>& items)
|
||||
{
|
||||
Log_DevPrintf("SetStringList(\"%s\", \"%s\")", section, key);
|
||||
if (items.empty())
|
||||
{
|
||||
DeleteValue(section, key);
|
||||
return;
|
||||
}
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jobjectArray> items_array(env, env->NewObjectArray(static_cast<jsize>(items.size()), AndroidHelpers::GetStringClass(), nullptr));
|
||||
for (size_t i = 0; i < items.size(); i++)
|
||||
{
|
||||
LocalRefHolder<jstring> item_jstr(env, env->NewStringUTF(items[i].c_str()));
|
||||
env->SetObjectArrayElement(items_array, static_cast<jsize>(i), item_jstr);
|
||||
}
|
||||
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
env->CallStaticVoidMethod(m_helper_class, m_helper_set_string_list, m_java_shared_preferences, key_string.Get(), items_array.Get());
|
||||
|
||||
CheckForException(env, "SetStringList");
|
||||
}
|
||||
|
||||
bool AndroidSettingsInterface::RemoveFromStringList(const char* section, const char* key, const char* item)
|
||||
{
|
||||
Log_DevPrintf("RemoveFromStringList(\"%s\", \"%s\", \"%s\")", section, key, item);
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> item_string(env, env->NewStringUTF(item));
|
||||
const bool result = env->CallStaticBooleanMethod(m_helper_class, m_helper_remove_from_string_list, m_java_shared_preferences, key_string.Get(), item_string.Get());
|
||||
CheckForException(env, "RemoveFromStringList");
|
||||
return result;
|
||||
}
|
||||
|
||||
bool AndroidSettingsInterface::AddToStringList(const char* section, const char* key, const char* item)
|
||||
{
|
||||
Log_DevPrintf("AddToStringList(\"%s\", \"%s\", \"%s\")", section, key, item);
|
||||
|
||||
JNIEnv* env = AndroidHelpers::GetJNIEnv();
|
||||
LocalRefHolder<jstring> key_string(env, env->NewStringUTF(GetSettingKey(section, key)));
|
||||
LocalRefHolder<jstring> item_string(env, env->NewStringUTF(item));
|
||||
const bool result = env->CallStaticBooleanMethod(m_helper_class, m_helper_add_to_string_list, m_java_shared_preferences, key_string.Get(), item_string.Get());
|
||||
CheckForException(env, "AddToStringList");
|
||||
return result;
|
||||
}
|
||||
54
android/app/src/cpp/android_settings_interface.h
Normal file
54
android/app/src/cpp/android_settings_interface.h
Normal file
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
#include "core/settings.h"
|
||||
#include <jni.h>
|
||||
|
||||
class AndroidSettingsInterface : public SettingsInterface
|
||||
{
|
||||
public:
|
||||
AndroidSettingsInterface(jobject java_context);
|
||||
~AndroidSettingsInterface();
|
||||
|
||||
bool Save() override;
|
||||
void Clear() override;
|
||||
|
||||
int GetIntValue(const char* section, const char* key, int default_value = 0) override;
|
||||
float GetFloatValue(const char* section, const char* key, float default_value = 0.0f) override;
|
||||
bool GetBoolValue(const char* section, const char* key, bool default_value = false) override;
|
||||
std::string GetStringValue(const char* section, const char* key, const char* default_value = "") override;
|
||||
|
||||
void SetIntValue(const char* section, const char* key, int value) override;
|
||||
void SetFloatValue(const char* section, const char* key, float value) override;
|
||||
void SetBoolValue(const char* section, const char* key, bool value) override;
|
||||
void SetStringValue(const char* section, const char* key, const char* value) override;
|
||||
void DeleteValue(const char* section, const char* key) override;
|
||||
void ClearSection(const char* section) override;
|
||||
|
||||
std::vector<std::string> GetStringList(const char* section, const char* key) override;
|
||||
void SetStringList(const char* section, const char* key, const std::vector<std::string>& items) override;
|
||||
bool RemoveFromStringList(const char* section, const char* key, const char* item) override;
|
||||
bool AddToStringList(const char* section, const char* key, const char* item) override;
|
||||
|
||||
private:
|
||||
jobject GetPreferencesEditor(JNIEnv* env);
|
||||
void CheckForException(JNIEnv* env, const char* task);
|
||||
|
||||
jclass m_set_class{};
|
||||
jclass m_shared_preferences_class{};
|
||||
jclass m_shared_preferences_editor_class{};
|
||||
jclass m_helper_class{};
|
||||
jobject m_java_shared_preferences{};
|
||||
jmethodID m_get_boolean{};
|
||||
jmethodID m_get_int{};
|
||||
jmethodID m_get_float{};
|
||||
jmethodID m_get_string{};
|
||||
jmethodID m_get_string_set{};
|
||||
jmethodID m_edit{};
|
||||
jmethodID m_edit_set_string{};
|
||||
jmethodID m_edit_commit{};
|
||||
jmethodID m_edit_remove{};
|
||||
jmethodID m_set_to_array{};
|
||||
jmethodID m_helper_clear_section{};
|
||||
jmethodID m_helper_add_to_string_list{};
|
||||
jmethodID m_helper_remove_from_string_list{};
|
||||
jmethodID m_helper_set_string_list{};
|
||||
};
|
||||
209
android/app/src/cpp/opensles_audio_stream.cpp
Normal file
209
android/app/src/cpp/opensles_audio_stream.cpp
Normal file
@@ -0,0 +1,209 @@
|
||||
#include "opensles_audio_stream.h"
|
||||
#include "common/assert.h"
|
||||
#include "common/log.h"
|
||||
#include <cmath>
|
||||
Log_SetChannel(OpenSLESAudioStream);
|
||||
|
||||
// Based off Dolphin's OpenSLESStream class.
|
||||
|
||||
OpenSLESAudioStream::OpenSLESAudioStream() = default;
|
||||
|
||||
OpenSLESAudioStream::~OpenSLESAudioStream()
|
||||
{
|
||||
if (IsOpen())
|
||||
OpenSLESAudioStream::CloseDevice();
|
||||
}
|
||||
|
||||
std::unique_ptr<AudioStream> OpenSLESAudioStream::Create()
|
||||
{
|
||||
return std::make_unique<OpenSLESAudioStream>();
|
||||
}
|
||||
|
||||
bool OpenSLESAudioStream::OpenDevice()
|
||||
{
|
||||
DebugAssert(!IsOpen());
|
||||
|
||||
SLresult res = slCreateEngine(&m_engine, 0, nullptr, 0, nullptr, nullptr);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("slCreateEngine failed: %d", res);
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_engine)->Realize(m_engine, SL_BOOLEAN_FALSE);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("Realize(Engine) failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_engine)->GetInterface(m_engine, SL_IID_ENGINE, &m_engine_engine);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("GetInterface(SL_IID_ENGINE) failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_engine_engine)->CreateOutputMix(m_engine_engine, &m_output_mix, 0, 0, 0);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("CreateOutputMix failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_output_mix)->Realize(m_output_mix, SL_BOOLEAN_FALSE);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("Realize(OutputMix) mix failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
SLDataLocator_AndroidSimpleBufferQueue dloc_bq{SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, NUM_BUFFERS};
|
||||
SLDataFormat_PCM format = {SL_DATAFORMAT_PCM,
|
||||
m_channels,
|
||||
m_output_sample_rate * 1000u,
|
||||
SL_PCMSAMPLEFORMAT_FIXED_16,
|
||||
SL_PCMSAMPLEFORMAT_FIXED_16,
|
||||
SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT,
|
||||
SL_BYTEORDER_LITTLEENDIAN};
|
||||
SLDataSource dsrc{&dloc_bq, &format};
|
||||
SLDataLocator_OutputMix dloc_outputmix{SL_DATALOCATOR_OUTPUTMIX, m_output_mix};
|
||||
SLDataSink dsink{&dloc_outputmix, nullptr};
|
||||
|
||||
const std::array<SLInterfaceID, 2> ap_interfaces = {{SL_IID_BUFFERQUEUE, SL_IID_VOLUME}};
|
||||
const std::array<SLboolean, 2> ap_interfaces_req = {{true, true}};
|
||||
res = (*m_engine_engine)
|
||||
->CreateAudioPlayer(m_engine_engine, &m_player, &dsrc, &dsink, static_cast<u32>(ap_interfaces.size()),
|
||||
ap_interfaces.data(), ap_interfaces_req.data());
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("CreateAudioPlayer failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_player)->Realize(m_player, SL_BOOLEAN_FALSE);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("Realize(AudioPlayer) failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_player)->GetInterface(m_player, SL_IID_PLAY, &m_play_interface);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("GetInterface(SL_IID_PLAY) failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_player)->GetInterface(m_player, SL_IID_BUFFERQUEUE, &m_buffer_queue_interface);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("GetInterface(SL_IID_BUFFERQUEUE) failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_player)->GetInterface(m_player, SL_IID_VOLUME, &m_volume_interface);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("GetInterface(SL_IID_VOLUME) failed: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
res = (*m_buffer_queue_interface)->RegisterCallback(m_buffer_queue_interface, BufferCallback, this);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
{
|
||||
Log_ErrorPrintf("Failed to register callback: %d", res);
|
||||
CloseDevice();
|
||||
return false;
|
||||
}
|
||||
|
||||
for (u32 i = 0; i < NUM_BUFFERS; i++)
|
||||
m_buffers[i] = std::make_unique<SampleType[]>(m_buffer_size * m_channels);
|
||||
|
||||
Log_InfoPrintf("OpenSL ES device opened: %uhz, %u channels, %u buffer size, %u buffers",
|
||||
m_output_sample_rate, m_channels, m_buffer_size, NUM_BUFFERS);
|
||||
return true;
|
||||
}
|
||||
|
||||
void OpenSLESAudioStream::PauseDevice(bool paused)
|
||||
{
|
||||
if (m_paused == paused)
|
||||
return;
|
||||
|
||||
SLresult res = (*m_play_interface)->SetPlayState(m_play_interface, paused ? SL_PLAYSTATE_PAUSED : SL_PLAYSTATE_PLAYING);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
Log_ErrorPrintf("SetPlayState failed: %d", res);
|
||||
|
||||
if (!paused && !m_buffer_enqueued)
|
||||
{
|
||||
m_buffer_enqueued = true;
|
||||
EnqueueBuffer();
|
||||
}
|
||||
|
||||
m_paused = paused;
|
||||
}
|
||||
|
||||
void OpenSLESAudioStream::CloseDevice()
|
||||
{
|
||||
m_buffers = {};
|
||||
m_current_buffer = 0;
|
||||
m_paused = true;
|
||||
m_buffer_enqueued = false;
|
||||
|
||||
if (m_player)
|
||||
{
|
||||
(*m_player)->Destroy(m_player);
|
||||
m_volume_interface = {};
|
||||
m_buffer_queue_interface = {};
|
||||
m_play_interface = {};
|
||||
m_player = {};
|
||||
}
|
||||
if (m_output_mix)
|
||||
{
|
||||
(*m_output_mix)->Destroy(m_output_mix);
|
||||
m_output_mix = {};
|
||||
}
|
||||
(*m_engine)->Destroy(m_engine);
|
||||
m_engine_engine = {};
|
||||
m_engine = {};
|
||||
}
|
||||
|
||||
void OpenSLESAudioStream::SetOutputVolume(u32 volume)
|
||||
{
|
||||
const SLmillibel attenuation = (volume == 0) ?
|
||||
SL_MILLIBEL_MIN :
|
||||
static_cast<SLmillibel>(2000.0f * std::log10(static_cast<float>(volume) / 100.0f));
|
||||
SLresult res = (*m_volume_interface)->SetVolumeLevel(m_volume_interface, attenuation);
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
Log_ErrorPrintf("SetVolumeLevel failed: %d", res);
|
||||
}
|
||||
|
||||
void OpenSLESAudioStream::EnqueueBuffer()
|
||||
{
|
||||
SampleType* samples = m_buffers[m_current_buffer].get();
|
||||
ReadFrames(samples, m_buffer_size, false);
|
||||
|
||||
SLresult res = (*m_buffer_queue_interface)
|
||||
->Enqueue(m_buffer_queue_interface, samples, m_buffer_size * m_channels * sizeof(SampleType));
|
||||
if (res != SL_RESULT_SUCCESS)
|
||||
Log_ErrorPrintf("Enqueue buffer failed: %d", res);
|
||||
|
||||
m_current_buffer = (m_current_buffer + 1) % NUM_BUFFERS;
|
||||
}
|
||||
|
||||
void OpenSLESAudioStream::BufferCallback(SLAndroidSimpleBufferQueueItf buffer_queue, void* context)
|
||||
{
|
||||
OpenSLESAudioStream* const this_ptr = static_cast<OpenSLESAudioStream*>(context);
|
||||
this_ptr->EnqueueBuffer();
|
||||
}
|
||||
|
||||
void OpenSLESAudioStream::FramesAvailable() {}
|
||||
48
android/app/src/cpp/opensles_audio_stream.h
Normal file
48
android/app/src/cpp/opensles_audio_stream.h
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
#include "common/audio_stream.h"
|
||||
#include <SLES/OpenSLES.h>
|
||||
#include <SLES/OpenSLES_Android.h>
|
||||
#include <array>
|
||||
#include <memory>
|
||||
|
||||
class OpenSLESAudioStream final : public AudioStream
|
||||
{
|
||||
public:
|
||||
OpenSLESAudioStream();
|
||||
~OpenSLESAudioStream();
|
||||
|
||||
static std::unique_ptr<AudioStream> Create();
|
||||
|
||||
void SetOutputVolume(u32 volume) override;
|
||||
|
||||
protected:
|
||||
enum : u32
|
||||
{
|
||||
NUM_BUFFERS = 2
|
||||
};
|
||||
|
||||
ALWAYS_INLINE bool IsOpen() const { return (m_engine != nullptr); }
|
||||
|
||||
bool OpenDevice() override;
|
||||
void PauseDevice(bool paused) override;
|
||||
void CloseDevice() override;
|
||||
void FramesAvailable() override;
|
||||
|
||||
void EnqueueBuffer();
|
||||
|
||||
static void BufferCallback(SLAndroidSimpleBufferQueueItf buffer_queue, void* context);
|
||||
|
||||
SLObjectItf m_engine{};
|
||||
SLEngineItf m_engine_engine{};
|
||||
SLObjectItf m_output_mix{};
|
||||
|
||||
SLObjectItf m_player{};
|
||||
SLPlayItf m_play_interface{};
|
||||
SLAndroidSimpleBufferQueueItf m_buffer_queue_interface{};
|
||||
SLVolumeItf m_volume_interface{};
|
||||
|
||||
std::array<std::unique_ptr<SampleType[]>, NUM_BUFFERS> m_buffers;
|
||||
u32 m_current_buffer = 0;
|
||||
bool m_paused = true;
|
||||
bool m_buffer_enqueued = false;
|
||||
};
|
||||
73
android/app/src/main/AndroidManifest.xml
Normal file
73
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.github.stenzek.duckstation">
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".GameDirectoriesActivity"
|
||||
android:label="@string/title_activity_game_directories"
|
||||
android:theme="@style/AppTheme.NoActionBar"></activity>
|
||||
<activity
|
||||
android:name=".EmulationActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:immersive="true"
|
||||
android:label="@string/title_activity_emulation"
|
||||
android:parentActivityName=".MainActivity"
|
||||
android:theme="@style/Theme.AppCompat.DayNight.NoActionBar"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.github.stenzek.duckstation.MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/title_activity_settings"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.github.stenzek.duckstation.MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ControllerMappingActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/title_activity_settings"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.github.stenzek.duckstation.MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".GamePropertiesActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/activity_game_properties"
|
||||
android:parentActivityName=".MainActivity">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="com.github.stenzek.duckstation.MainActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
BIN
android/app/src/main/ic_launcher-web.png
Normal file
BIN
android/app/src/main/ic_launcher-web.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@@ -0,0 +1,174 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.AssetManager;
|
||||
import android.os.Environment;
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
import android.view.Surface;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Locale;
|
||||
|
||||
public class AndroidHostInterface {
|
||||
public final static int DISPLAY_ALIGNMENT_TOP_OR_LEFT = 0;
|
||||
public final static int DISPLAY_ALIGNMENT_CENTER = 1;
|
||||
public final static int DISPLAY_ALIGNMENT_RIGHT_OR_BOTTOM = 2;
|
||||
|
||||
private long mNativePointer;
|
||||
private Context mContext;
|
||||
|
||||
public AndroidHostInterface(Context context) {
|
||||
this.mContext = context;
|
||||
}
|
||||
|
||||
public void reportError(String message) {
|
||||
Toast.makeText(mContext, message, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
||||
public void reportMessage(String message) {
|
||||
Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
public InputStream openAssetStream(String path) {
|
||||
try {
|
||||
return mContext.getAssets().open(path, AssetManager.ACCESS_STREAMING);
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setContext(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
static public native String getScmVersion();
|
||||
|
||||
static public native String getFullScmVersion();
|
||||
|
||||
static public native boolean setThreadAffinity(int[] cpus);
|
||||
|
||||
static public native AndroidHostInterface create(Context context, String userDirectory);
|
||||
|
||||
public native boolean isEmulationThreadRunning();
|
||||
|
||||
public native boolean runEmulationThread(EmulationActivity emulationActivity, String filename, boolean resumeState, String state_filename);
|
||||
|
||||
public native boolean isEmulationThreadPaused();
|
||||
|
||||
public native void pauseEmulationThread(boolean paused);
|
||||
|
||||
public native void stopEmulationThreadLoop();
|
||||
|
||||
public native boolean hasSurface();
|
||||
|
||||
public native void surfaceChanged(Surface surface, int format, int width, int height);
|
||||
|
||||
// TODO: Find a better place for this.
|
||||
public native void setControllerType(int index, String typeName);
|
||||
|
||||
public native void setControllerButtonState(int index, int buttonCode, boolean pressed);
|
||||
|
||||
public native void setControllerAxisState(int index, int axisCode, float value);
|
||||
|
||||
public static native int getControllerButtonCode(String controllerType, String buttonName);
|
||||
|
||||
public static native int getControllerAxisCode(String controllerType, String axisName);
|
||||
|
||||
public static native String[] getControllerButtonNames(String controllerType);
|
||||
|
||||
public static native String[] getControllerAxisNames(String controllerType);
|
||||
|
||||
public native void handleControllerButtonEvent(int controllerIndex, int buttonIndex, boolean pressed);
|
||||
|
||||
public native void handleControllerAxisEvent(int controllerIndex, int axisIndex, float value);
|
||||
|
||||
public native String[] getInputProfileNames();
|
||||
|
||||
public native boolean loadInputProfile(String name);
|
||||
|
||||
public native boolean saveInputProfile(String name);
|
||||
|
||||
public native HotkeyInfo[] getHotkeyInfoList();
|
||||
|
||||
public native void refreshGameList(boolean invalidateCache, boolean invalidateDatabase, AndroidProgressCallback progressCallback);
|
||||
|
||||
public native GameListEntry[] getGameListEntries();
|
||||
|
||||
public native GameListEntry getGameListEntry(String path);
|
||||
|
||||
public native String getGameSettingValue(String path, String key);
|
||||
|
||||
public native void setGameSettingValue(String path, String key, String value);
|
||||
|
||||
public native void resetSystem();
|
||||
|
||||
public native void loadState(boolean global, int slot);
|
||||
|
||||
public native void saveState(boolean global, int slot);
|
||||
|
||||
public native void saveResumeState(boolean waitForCompletion);
|
||||
|
||||
public native void applySettings();
|
||||
|
||||
public native void setDisplayAlignment(int alignment);
|
||||
|
||||
public native PatchCode[] getPatchCodeList();
|
||||
|
||||
public native void setPatchCodeEnabled(int index, boolean enabled);
|
||||
|
||||
public native boolean importPatchCodesFromString(String str);
|
||||
|
||||
public native void addOSDMessage(String message, float duration);
|
||||
|
||||
public native boolean hasAnyBIOSImages();
|
||||
|
||||
public native String importBIOSImage(byte[] data);
|
||||
|
||||
public native boolean isFastForwardEnabled();
|
||||
|
||||
public native void setFastForwardEnabled(boolean enabled);
|
||||
|
||||
public native String[] getMediaPlaylistPaths();
|
||||
|
||||
public native int getMediaPlaylistIndex();
|
||||
|
||||
public native boolean setMediaPlaylistIndex(int index);
|
||||
|
||||
public native boolean setMediaFilename(String filename);
|
||||
|
||||
public native SaveStateInfo[] getSaveStateInfo(boolean includeEmpty);
|
||||
|
||||
static {
|
||||
System.loadLibrary("duckstation-native");
|
||||
}
|
||||
|
||||
static private AndroidHostInterface mInstance;
|
||||
|
||||
static public boolean createInstance(Context context) {
|
||||
// Set user path.
|
||||
String externalStorageDirectory = Environment.getExternalStorageDirectory().getAbsolutePath();
|
||||
if (externalStorageDirectory.isEmpty())
|
||||
externalStorageDirectory = "/sdcard";
|
||||
|
||||
externalStorageDirectory += "/duckstation";
|
||||
Log.i("AndroidHostInterface", "User directory: " + externalStorageDirectory);
|
||||
mInstance = create(context, externalStorageDirectory);
|
||||
return mInstance != null;
|
||||
}
|
||||
|
||||
static public boolean hasInstance() {
|
||||
return mInstance != null;
|
||||
}
|
||||
|
||||
static public AndroidHostInterface getInstance() {
|
||||
return mInstance;
|
||||
}
|
||||
|
||||
static public boolean hasInstanceAndEmulationThreadIsRunning() {
|
||||
return hasInstance() && getInstance().isEmulationThreadRunning();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.ProgressDialog;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
public class AndroidProgressCallback {
|
||||
private Activity mContext;
|
||||
private ProgressDialog mDialog;
|
||||
|
||||
public AndroidProgressCallback(Activity context) {
|
||||
mContext = context;
|
||||
mDialog = new ProgressDialog(context);
|
||||
mDialog.setMessage(context.getString(R.string.android_progress_callback_please_wait));
|
||||
mDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
||||
mDialog.setIndeterminate(false);
|
||||
mDialog.setMax(100);
|
||||
mDialog.setProgress(0);
|
||||
mDialog.show();
|
||||
}
|
||||
|
||||
public void dismiss() {
|
||||
mDialog.dismiss();
|
||||
}
|
||||
|
||||
public void setTitle(String text) {
|
||||
mContext.runOnUiThread(() -> {
|
||||
mDialog.setTitle(text);
|
||||
});
|
||||
}
|
||||
|
||||
public void setStatusText(String text) {
|
||||
mContext.runOnUiThread(() -> {
|
||||
mDialog.setMessage(text);
|
||||
});
|
||||
}
|
||||
|
||||
public void setProgressRange(int range) {
|
||||
mContext.runOnUiThread(() -> {
|
||||
mDialog.setMax(range);
|
||||
});
|
||||
}
|
||||
|
||||
public void setProgressValue(int value) {
|
||||
mContext.runOnUiThread(() -> {
|
||||
mDialog.setProgress(value);
|
||||
});
|
||||
}
|
||||
|
||||
public void modalError(String message) {
|
||||
Object lock = new Object();
|
||||
mContext.runOnUiThread(() -> {
|
||||
new AlertDialog.Builder(mContext)
|
||||
.setTitle("Error")
|
||||
.setMessage(message)
|
||||
.setPositiveButton(mContext.getString(R.string.android_progress_callback_ok), (dialog, button) -> {
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setOnDismissListener((dialogInterface) -> {
|
||||
synchronized (lock) {
|
||||
lock.notify();
|
||||
}
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
});
|
||||
|
||||
synchronized (lock) {
|
||||
try {
|
||||
lock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void modalInformation(String message) {
|
||||
Object lock = new Object();
|
||||
mContext.runOnUiThread(() -> {
|
||||
new AlertDialog.Builder(mContext)
|
||||
.setTitle(mContext.getString(R.string.android_progress_callback_information))
|
||||
.setMessage(message)
|
||||
.setPositiveButton(mContext.getString(R.string.android_progress_callback_ok), (dialog, button) -> {
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setOnDismissListener((dialogInterface) -> {
|
||||
synchronized (lock) {
|
||||
lock.notify();
|
||||
}
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
});
|
||||
|
||||
synchronized (lock) {
|
||||
try {
|
||||
lock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class ConfirmationResult {
|
||||
public boolean result = false;
|
||||
}
|
||||
|
||||
public boolean modalConfirmation(String message) {
|
||||
ConfirmationResult result = new ConfirmationResult();
|
||||
mContext.runOnUiThread(() -> {
|
||||
new AlertDialog.Builder(mContext)
|
||||
.setTitle(mContext.getString(R.string.android_progress_callback_confirmation))
|
||||
.setMessage(message)
|
||||
.setPositiveButton(mContext.getString(R.string.android_progress_callback_yes), (dialog, button) -> {
|
||||
result.result = true;
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setNegativeButton(mContext.getString(R.string.android_progress_callback_no), (dialog, button) -> {
|
||||
result.result = false;
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setOnDismissListener((dialogInterface) -> {
|
||||
synchronized (result) {
|
||||
result.notify();
|
||||
}
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
});
|
||||
|
||||
synchronized (result) {
|
||||
try {
|
||||
result.wait();
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
|
||||
return result.result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
public enum ConsoleRegion {
|
||||
AutoDetect,
|
||||
NTSC_J,
|
||||
NTSC_U,
|
||||
PAL
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.SharedPreferences;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Log;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
public class ControllerBindingDialog extends AlertDialog {
|
||||
private boolean mIsAxis;
|
||||
private String mSettingKey;
|
||||
private String mCurrentBinding;
|
||||
|
||||
public ControllerBindingDialog(Context context, String buttonName, String settingKey, String currentBinding, boolean isAxis) {
|
||||
super(context);
|
||||
|
||||
mIsAxis = isAxis;
|
||||
mSettingKey = settingKey;
|
||||
mCurrentBinding = currentBinding;
|
||||
if (mCurrentBinding == null)
|
||||
mCurrentBinding = getContext().getString(R.string.controller_binding_dialog_no_binding);
|
||||
|
||||
setTitle(buttonName);
|
||||
updateMessage();
|
||||
setButton(BUTTON_POSITIVE, context.getString(R.string.controller_binding_dialog_cancel), (dialogInterface, button) -> dismiss());
|
||||
setButton(BUTTON_NEGATIVE, context.getString(R.string.controller_binding_dialog_clear), (dialogInterface, button) -> {
|
||||
mCurrentBinding = null;
|
||||
updateBinding();
|
||||
dismiss();
|
||||
});
|
||||
|
||||
setOnKeyListener(new DialogInterface.OnKeyListener() {
|
||||
@Override
|
||||
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
|
||||
if (onKeyDown(keyCode, event))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateMessage() {
|
||||
setMessage(String.format(getContext().getString(R.string.controller_binding_dialog_message), mCurrentBinding));
|
||||
}
|
||||
|
||||
private void updateBinding() {
|
||||
SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(getContext()).edit();
|
||||
if (mCurrentBinding != null) {
|
||||
ArraySet<String> values = new ArraySet<>();
|
||||
values.add(mCurrentBinding);
|
||||
editor.putStringSet(mSettingKey, values);
|
||||
} else {
|
||||
try {
|
||||
editor.remove(mSettingKey);
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (mIsAxis || !EmulationSurfaceView.isDPadOrButtonEvent(event))
|
||||
return super.onKeyUp(keyCode, event);
|
||||
|
||||
int buttonIndex = EmulationSurfaceView.getButtonIndexForKeyCode(keyCode);
|
||||
if (buttonIndex < 0)
|
||||
return super.onKeyUp(keyCode, event);
|
||||
|
||||
// TODO: Multiple controllers
|
||||
final int controllerIndex = 0;
|
||||
mCurrentBinding = String.format("Controller%d/Button%d", controllerIndex, buttonIndex);
|
||||
updateMessage();
|
||||
updateBinding();
|
||||
dismiss();
|
||||
return true;
|
||||
}
|
||||
|
||||
private int mUpdatedAxisCode = -1;
|
||||
|
||||
private void setAxisCode(int axisCode, boolean positive) {
|
||||
final int axisIndex = EmulationSurfaceView.getAxisIndexForAxisCode(axisCode);
|
||||
if (mUpdatedAxisCode >= 0 || axisIndex < 0)
|
||||
return;
|
||||
|
||||
mUpdatedAxisCode = axisCode;
|
||||
|
||||
final int controllerIndex = 0;
|
||||
if (mIsAxis)
|
||||
mCurrentBinding = String.format("Controller%d/Axis%d", controllerIndex, axisIndex);
|
||||
else
|
||||
mCurrentBinding = String.format("Controller%d/%cAxis%d", controllerIndex, (positive) ? '+' : '-', axisIndex);
|
||||
|
||||
updateBinding();
|
||||
updateMessage();
|
||||
dismiss();
|
||||
}
|
||||
|
||||
final static float DETECT_THRESHOLD = 0.25f;
|
||||
|
||||
private HashMap<Integer, float[]> mStartingAxisValues = new HashMap<>();
|
||||
|
||||
private boolean doAxisDetection(MotionEvent event) {
|
||||
if ((event.getSource() & (InputDevice.SOURCE_JOYSTICK | InputDevice.SOURCE_GAMEPAD | InputDevice.SOURCE_DPAD)) == 0)
|
||||
return false;
|
||||
|
||||
final int[] axisCodes = EmulationSurfaceView.getKnownAxisCodes();
|
||||
final int deviceId = event.getDeviceId();
|
||||
|
||||
if (!mStartingAxisValues.containsKey(deviceId)) {
|
||||
final float[] axisValues = new float[axisCodes.length];
|
||||
for (int axisIndex = 0; axisIndex < axisCodes.length; axisIndex++) {
|
||||
final int axisCode = axisCodes[axisIndex];
|
||||
|
||||
// these are binary, so start at zero
|
||||
if (axisCode == MotionEvent.AXIS_HAT_X || axisCode == MotionEvent.AXIS_HAT_Y)
|
||||
axisValues[axisIndex] = 0.0f;
|
||||
else
|
||||
axisValues[axisIndex] = event.getAxisValue(axisCode);
|
||||
}
|
||||
|
||||
mStartingAxisValues.put(deviceId, axisValues);
|
||||
}
|
||||
|
||||
final float[] axisValues = mStartingAxisValues.get(deviceId);
|
||||
for (int axisIndex = 0; axisIndex < axisCodes.length; axisIndex++) {
|
||||
final float newValue = event.getAxisValue(axisCodes[axisIndex]);
|
||||
if (Math.abs(newValue - axisValues[axisIndex]) >= DETECT_THRESHOLD) {
|
||||
setAxisCode(axisCodes[axisIndex], newValue >= 0.0f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
|
||||
if (doAxisDetection(event))
|
||||
return true;
|
||||
|
||||
return super.onGenericMotionEvent(event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class ControllerBindingPreference extends Preference {
|
||||
private enum Type {
|
||||
BUTTON,
|
||||
AXIS,
|
||||
HOTKEY
|
||||
}
|
||||
|
||||
private String mBindingName;
|
||||
private String mValue;
|
||||
private TextView mValueView;
|
||||
private Type mType = Type.BUTTON;
|
||||
|
||||
private static int getIconForButton(String buttonName) {
|
||||
if (buttonName.equals("Up")) {
|
||||
return R.drawable.ic_controller_up_button_pressed;
|
||||
} else if (buttonName.equals("Right")) {
|
||||
return R.drawable.ic_controller_right_button_pressed;
|
||||
} else if (buttonName.equals("Down")) {
|
||||
return R.drawable.ic_controller_down_button_pressed;
|
||||
} else if (buttonName.equals("Left")) {
|
||||
return R.drawable.ic_controller_left_button_pressed;
|
||||
} else if (buttonName.equals("Triangle")) {
|
||||
return R.drawable.ic_controller_triangle_button_pressed;
|
||||
} else if (buttonName.equals("Circle")) {
|
||||
return R.drawable.ic_controller_circle_button_pressed;
|
||||
} else if (buttonName.equals("Cross")) {
|
||||
return R.drawable.ic_controller_cross_button_pressed;
|
||||
} else if (buttonName.equals("Square")) {
|
||||
return R.drawable.ic_controller_square_button_pressed;
|
||||
} else if (buttonName.equals("Start")) {
|
||||
return R.drawable.ic_controller_start_button_pressed;
|
||||
} else if (buttonName.equals("Select")) {
|
||||
return R.drawable.ic_controller_select_button_pressed;
|
||||
} else if (buttonName.equals("L1")) {
|
||||
return R.drawable.ic_controller_l1_button_pressed;
|
||||
} else if (buttonName.equals("L2")) {
|
||||
return R.drawable.ic_controller_l2_button_pressed;
|
||||
} else if (buttonName.equals("R1")) {
|
||||
return R.drawable.ic_controller_r1_button_pressed;
|
||||
} else if (buttonName.equals("R2")) {
|
||||
return R.drawable.ic_controller_r2_button_pressed;
|
||||
}
|
||||
|
||||
return R.drawable.ic_baseline_radio_button_unchecked_24;
|
||||
}
|
||||
|
||||
private static int getIconForAxis(String axisName) {
|
||||
return R.drawable.ic_baseline_radio_button_checked_24;
|
||||
}
|
||||
|
||||
private static int getIconForHotkey(String hotkeyDisplayName) {
|
||||
return R.drawable.ic_baseline_category_24;
|
||||
}
|
||||
|
||||
public ControllerBindingPreference(Context context, AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
setWidgetLayoutResource(R.layout.layout_controller_binding_preference);
|
||||
setIconSpaceReserved(false);
|
||||
}
|
||||
|
||||
public ControllerBindingPreference(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
setWidgetLayoutResource(R.layout.layout_controller_binding_preference);
|
||||
setIconSpaceReserved(false);
|
||||
}
|
||||
|
||||
public ControllerBindingPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
setWidgetLayoutResource(R.layout.layout_controller_binding_preference);
|
||||
setIconSpaceReserved(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder holder) {
|
||||
super.onBindViewHolder(holder);
|
||||
|
||||
ImageView iconView = ((ImageView) holder.findViewById(R.id.controller_binding_icon));
|
||||
TextView nameView = ((TextView) holder.findViewById(R.id.controller_binding_name));
|
||||
mValueView = ((TextView) holder.findViewById(R.id.controller_binding_value));
|
||||
|
||||
int drawableId = R.drawable.ic_baseline_radio_button_checked_24;
|
||||
switch (mType) {
|
||||
case BUTTON:
|
||||
drawableId = getIconForButton(mBindingName);
|
||||
break;
|
||||
case AXIS:
|
||||
drawableId = getIconForAxis(mBindingName);
|
||||
break;
|
||||
case HOTKEY:
|
||||
drawableId = getIconForHotkey(mBindingName);
|
||||
break;
|
||||
}
|
||||
|
||||
iconView.setImageDrawable(ContextCompat.getDrawable(getContext(), drawableId));
|
||||
nameView.setText(mBindingName);
|
||||
updateValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onClick() {
|
||||
ControllerBindingDialog dialog = new ControllerBindingDialog(getContext(), mBindingName, getKey(), mValue, (mType == Type.AXIS));
|
||||
dialog.setOnDismissListener((dismissedDialog) -> updateValue());
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
public void initButton(int controllerIndex, String buttonName) {
|
||||
mBindingName = buttonName;
|
||||
mType = Type.BUTTON;
|
||||
setKey(String.format("Controller%d/Button%s", controllerIndex, buttonName));
|
||||
updateValue();
|
||||
}
|
||||
|
||||
public void initAxis(int controllerIndex, String axisName) {
|
||||
mBindingName = axisName;
|
||||
mType = Type.AXIS;
|
||||
setKey(String.format("Controller%d/Axis%s", controllerIndex, axisName));
|
||||
updateValue();
|
||||
}
|
||||
|
||||
public void initHotkey(HotkeyInfo hotkeyInfo) {
|
||||
mBindingName = hotkeyInfo.getDisplayName();
|
||||
mType = Type.HOTKEY;
|
||||
setKey(hotkeyInfo.getBindingConfigKey());
|
||||
updateValue();
|
||||
}
|
||||
|
||||
private void updateValue(String value) {
|
||||
mValue = value;
|
||||
if (mValueView != null) {
|
||||
if (value != null)
|
||||
mValueView.setText(value);
|
||||
else
|
||||
mValueView.setText(getContext().getString(R.string.controller_binding_dialog_no_binding));
|
||||
}
|
||||
}
|
||||
|
||||
public void updateValue() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
Set<String> values = PreferenceHelpers.getStringSet(prefs, getKey());
|
||||
if (values != null) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String value : values) {
|
||||
if (sb.length() > 0)
|
||||
sb.append(", ");
|
||||
sb.append(value);
|
||||
}
|
||||
|
||||
updateValue(sb.toString());
|
||||
} else {
|
||||
updateValue(null);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearBinding(SharedPreferences.Editor prefEditor) {
|
||||
try {
|
||||
prefEditor.remove(getKey());
|
||||
} catch (Exception e) {
|
||||
|
||||
}
|
||||
|
||||
updateValue(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class ControllerMappingActivity extends AppCompatActivity {
|
||||
|
||||
private static final int NUM_CONTROLLER_PORTS = 2;
|
||||
|
||||
private ArrayList<ControllerBindingPreference> mPreferences = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.settings_activity);
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, new SettingsCollectionFragment(this))
|
||||
.commit();
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setTitle(R.string.controller_mapping_activity_title);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
getMenuInflater().inflate(R.menu.menu_controller_mapping, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
final int id = item.getItemId();
|
||||
|
||||
//noinspection SimplifiableIfStatement
|
||||
if (id == R.id.action_load_profile) {
|
||||
doLoadProfile();
|
||||
return true;
|
||||
} else if (id == R.id.action_save_profile) {
|
||||
doSaveProfile();
|
||||
return true;
|
||||
} else if (id == R.id.action_clear_bindings) {
|
||||
doClearBindings();
|
||||
return true;
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void displayError(String text) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.emulation_activity_error)
|
||||
.setMessage(text)
|
||||
.setNegativeButton(R.string.main_activity_ok, ((dialog, which) -> dialog.dismiss()))
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void doLoadProfile() {
|
||||
final String[] profileNames = AndroidHostInterface.getInstance().getInputProfileNames();
|
||||
if (profileNames == null) {
|
||||
displayError(getString(R.string.controller_mapping_activity_no_profiles_found));
|
||||
return;
|
||||
}
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.controller_mapping_activity_select_input_profile)
|
||||
.setItems(profileNames, (dialog, choice) -> {
|
||||
doLoadProfile(profileNames[choice]);
|
||||
dialog.dismiss();
|
||||
})
|
||||
.setNegativeButton("Cancel", ((dialog, which) -> dialog.dismiss()))
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void doLoadProfile(String profileName) {
|
||||
if (!AndroidHostInterface.getInstance().loadInputProfile(profileName)) {
|
||||
displayError(String.format(getString(R.string.controller_mapping_activity_failed_to_load_profile), profileName));
|
||||
return;
|
||||
}
|
||||
|
||||
updateAllBindings();
|
||||
}
|
||||
|
||||
private void doSaveProfile() {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
final EditText input = new EditText(this);
|
||||
builder.setTitle(R.string.controller_mapping_activity_input_profile_name);
|
||||
builder.setView(input);
|
||||
builder.setPositiveButton(R.string.controller_mapping_activity_save, (dialog, which) -> {
|
||||
final String name = input.getText().toString();
|
||||
if (name.isEmpty()) {
|
||||
displayError(getString(R.string.controller_mapping_activity_name_must_be_provided));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!AndroidHostInterface.getInstance().saveInputProfile(name)) {
|
||||
displayError(getString(R.string.controller_mapping_activity_failed_to_save_input_profile));
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.makeText(ControllerMappingActivity.this, String.format(ControllerMappingActivity.this.getString(R.string.controller_mapping_activity_input_profile_saved), name),
|
||||
Toast.LENGTH_LONG).show();
|
||||
});
|
||||
builder.setNegativeButton(R.string.controller_mapping_activity_cancel, (dialog, which) -> dialog.dismiss());
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void doClearBindings() {
|
||||
SharedPreferences.Editor prefEdit = PreferenceManager.getDefaultSharedPreferences(this).edit();
|
||||
for (ControllerBindingPreference pref : mPreferences)
|
||||
pref.clearBinding(prefEdit);
|
||||
prefEdit.commit();
|
||||
}
|
||||
|
||||
private void updateAllBindings() {
|
||||
for (ControllerBindingPreference pref : mPreferences)
|
||||
pref.updateValue();
|
||||
}
|
||||
|
||||
public static class ControllerPortFragment extends PreferenceFragmentCompat {
|
||||
private ControllerMappingActivity activity;
|
||||
private int controllerIndex;
|
||||
|
||||
public ControllerPortFragment(ControllerMappingActivity activity, int controllerIndex) {
|
||||
this.activity = activity;
|
||||
this.controllerIndex = controllerIndex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
final SharedPreferences sp = getPreferenceManager().getSharedPreferences();
|
||||
final String defaultControllerType = controllerIndex == 0 ? "DigitalController" : "None";
|
||||
String controllerType = sp.getString(String.format("Controller%d/Type", controllerIndex), defaultControllerType);
|
||||
String[] controllerButtons = AndroidHostInterface.getControllerButtonNames(controllerType);
|
||||
String[] axisButtons = AndroidHostInterface.getControllerAxisNames(controllerType);
|
||||
|
||||
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
|
||||
if (controllerButtons != null) {
|
||||
for (String buttonName : controllerButtons) {
|
||||
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
|
||||
cbp.initButton(controllerIndex, buttonName);
|
||||
ps.addPreference(cbp);
|
||||
activity.mPreferences.add(cbp);
|
||||
}
|
||||
}
|
||||
if (axisButtons != null) {
|
||||
for (String axisName : axisButtons) {
|
||||
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
|
||||
cbp.initAxis(controllerIndex, axisName);
|
||||
ps.addPreference(cbp);
|
||||
activity.mPreferences.add(cbp);
|
||||
}
|
||||
}
|
||||
|
||||
setPreferenceScreen(ps);
|
||||
}
|
||||
}
|
||||
|
||||
public static class HotkeyFragment extends PreferenceFragmentCompat {
|
||||
private ControllerMappingActivity activity;
|
||||
private HotkeyInfo[] mHotkeyInfo;
|
||||
|
||||
public HotkeyFragment(ControllerMappingActivity activity) {
|
||||
this.activity = activity;
|
||||
this.mHotkeyInfo = AndroidHostInterface.getInstance().getHotkeyInfoList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
|
||||
if (mHotkeyInfo != null) {
|
||||
for (HotkeyInfo hotkeyInfo : mHotkeyInfo) {
|
||||
final ControllerBindingPreference cbp = new ControllerBindingPreference(getContext(), null);
|
||||
cbp.initHotkey(hotkeyInfo);
|
||||
ps.addPreference(cbp);
|
||||
activity.mPreferences.add(cbp);
|
||||
}
|
||||
}
|
||||
|
||||
setPreferenceScreen(ps);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SettingsCollectionFragment extends Fragment {
|
||||
private ControllerMappingActivity activity;
|
||||
private SettingsCollectionAdapter adapter;
|
||||
private ViewPager2 viewPager;
|
||||
|
||||
public SettingsCollectionFragment(ControllerMappingActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_controller_mapping, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
adapter = new SettingsCollectionAdapter(activity, this);
|
||||
viewPager = view.findViewById(R.id.view_pager);
|
||||
viewPager.setAdapter(adapter);
|
||||
|
||||
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
|
||||
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
|
||||
if (position == NUM_CONTROLLER_PORTS)
|
||||
tab.setText("Hotkeys");
|
||||
else
|
||||
tab.setText(String.format("Port %d", position + 1));
|
||||
}).attach();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SettingsCollectionAdapter extends FragmentStateAdapter {
|
||||
private ControllerMappingActivity activity;
|
||||
|
||||
public SettingsCollectionAdapter(@NonNull ControllerMappingActivity activity, @NonNull Fragment fragment) {
|
||||
super(fragment);
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
if (position != NUM_CONTROLLER_PORTS)
|
||||
return new ControllerPortFragment(activity, position + 1);
|
||||
else
|
||||
return new HotkeyFragment(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return NUM_CONTROLLER_PORTS + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
public enum DiscRegion {
|
||||
NTSC_J,
|
||||
NTSC_U,
|
||||
PAL,
|
||||
Other
|
||||
}
|
||||
@@ -0,0 +1,720 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Vibrator;
|
||||
import android.util.Log;
|
||||
import android.view.Display;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SurfaceHolder;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
/**
|
||||
* An example full-screen activity that shows and hides the system UI (i.e.
|
||||
* status bar and navigation/system bar) with user interaction.
|
||||
*/
|
||||
public class EmulationActivity extends AppCompatActivity implements SurfaceHolder.Callback {
|
||||
/**
|
||||
* Settings interfaces.
|
||||
*/
|
||||
private SharedPreferences mPreferences;
|
||||
private boolean mWasDestroyed = false;
|
||||
private boolean mStopRequested = false;
|
||||
private boolean mApplySettingsOnSurfaceRestored = false;
|
||||
private String mGameTitle = null;
|
||||
private EmulationSurfaceView mContentView;
|
||||
|
||||
private boolean getBooleanSetting(String key, boolean defaultValue) {
|
||||
return mPreferences.getBoolean(key, defaultValue);
|
||||
}
|
||||
|
||||
private void setBooleanSetting(String key, boolean value) {
|
||||
SharedPreferences.Editor editor = mPreferences.edit();
|
||||
editor.putBoolean(key, value);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
private String getStringSetting(String key, String defaultValue) {
|
||||
return mPreferences.getString(key, defaultValue);
|
||||
}
|
||||
|
||||
private void setStringSetting(String key, String value) {
|
||||
SharedPreferences.Editor editor = mPreferences.edit();
|
||||
editor.putString(key, value);
|
||||
editor.apply();
|
||||
}
|
||||
|
||||
private void reportErrorOnUIThread(String message) {
|
||||
// Toast.makeText(this, message, Toast.LENGTH_LONG);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.emulation_activity_error)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.emulation_activity_ok, (dialog, button) -> {
|
||||
dialog.dismiss();
|
||||
enableFullscreenImmersive();
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
public void reportError(String message) {
|
||||
Log.e("EmulationActivity", message);
|
||||
|
||||
Object lock = new Object();
|
||||
runOnUiThread(() -> {
|
||||
// Toast.makeText(this, message, Toast.LENGTH_LONG);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.emulation_activity_error)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.emulation_activity_ok, (dialog, button) -> {
|
||||
dialog.dismiss();
|
||||
enableFullscreenImmersive();
|
||||
synchronized (lock) {
|
||||
lock.notify();
|
||||
}
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
});
|
||||
|
||||
synchronized (lock) {
|
||||
try {
|
||||
lock.wait();
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private EmulationThread mEmulationThread;
|
||||
|
||||
private void stopEmulationThread() {
|
||||
if (mEmulationThread == null)
|
||||
return;
|
||||
|
||||
mEmulationThread.stopAndJoin();
|
||||
mEmulationThread = null;
|
||||
}
|
||||
|
||||
public void onEmulationStarted() {
|
||||
runOnUiThread(() -> {
|
||||
updateRequestedOrientation();
|
||||
updateOrientation();
|
||||
});
|
||||
}
|
||||
|
||||
public void onEmulationStopped() {
|
||||
runOnUiThread(() -> {
|
||||
if (!mWasDestroyed && !mStopRequested)
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
public void onGameTitleChanged(String title) {
|
||||
runOnUiThread(() -> {
|
||||
mGameTitle = title;
|
||||
});
|
||||
}
|
||||
|
||||
public float getRefreshRate() {
|
||||
WindowManager windowManager = getWindowManager();
|
||||
if (windowManager == null) {
|
||||
windowManager = ((WindowManager) getSystemService(Context.WINDOW_SERVICE));
|
||||
if (windowManager == null)
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
Display display = windowManager.getDefaultDisplay();
|
||||
if (display == null)
|
||||
return -1.0f;
|
||||
|
||||
return display.getRefreshRate();
|
||||
}
|
||||
|
||||
public void openPauseMenu() {
|
||||
runOnUiThread(() -> {
|
||||
showMenu();
|
||||
});
|
||||
}
|
||||
|
||||
private void doApplySettings() {
|
||||
AndroidHostInterface.getInstance().applySettings();
|
||||
updateRequestedOrientation();
|
||||
updateControllers();
|
||||
updateSustainedPerformanceMode();
|
||||
}
|
||||
|
||||
private void applySettings() {
|
||||
if (!AndroidHostInterface.getInstance().isEmulationThreadRunning())
|
||||
return;
|
||||
|
||||
if (AndroidHostInterface.getInstance().hasSurface()) {
|
||||
doApplySettings();
|
||||
} else {
|
||||
mApplySettingsOnSurfaceRestored = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// Ends the activity if it was restored without properly being created.
|
||||
private boolean checkActivityIsValid() {
|
||||
if (!AndroidHostInterface.hasInstance() || !AndroidHostInterface.getInstance().isEmulationThreadRunning()) {
|
||||
finish();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceCreated(SurfaceHolder holder) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
|
||||
// Once we get a surface, we can boot.
|
||||
AndroidHostInterface.getInstance().surfaceChanged(holder.getSurface(), format, width, height);
|
||||
|
||||
if (mEmulationThread != null) {
|
||||
updateOrientation();
|
||||
|
||||
if (mApplySettingsOnSurfaceRestored) {
|
||||
mApplySettingsOnSurfaceRestored = false;
|
||||
doApplySettings();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
final String bootPath = getIntent().getStringExtra("bootPath");
|
||||
final boolean saveStateOnExit = getBooleanSetting("Main/SaveStateOnExit", true);
|
||||
final boolean resumeState = getIntent().getBooleanExtra("resumeState", saveStateOnExit);
|
||||
final String bootSaveStatePath = getIntent().getStringExtra("saveStatePath");
|
||||
|
||||
mEmulationThread = EmulationThread.create(this, bootPath, resumeState, bootSaveStatePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||
Log.i("EmulationActivity", "Surface destroyed");
|
||||
|
||||
// Save the resume state in case we never get back again...
|
||||
if (AndroidHostInterface.getInstance().isEmulationThreadRunning() && !mStopRequested)
|
||||
AndroidHostInterface.getInstance().saveResumeState(true);
|
||||
|
||||
AndroidHostInterface.getInstance().surfaceChanged(null, 0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
mPreferences = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Log.i("EmulationActivity", "OnCreate");
|
||||
|
||||
// we might be coming from a third-party launcher if the host interface isn't setup
|
||||
if (!AndroidHostInterface.hasInstance() && !AndroidHostInterface.createInstance(this)) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
enableFullscreenImmersive();
|
||||
setContentView(R.layout.activity_emulation);
|
||||
|
||||
mContentView = findViewById(R.id.fullscreen_content);
|
||||
mContentView.getHolder().addCallback(this);
|
||||
mContentView.setFocusableInTouchMode(true);
|
||||
mContentView.setFocusable(true);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
mContentView.setFocusedByDefault(true);
|
||||
}
|
||||
mContentView.requestFocus();
|
||||
|
||||
// Sort out rotation.
|
||||
updateOrientation();
|
||||
updateSustainedPerformanceMode();
|
||||
|
||||
// Hook up controller input.
|
||||
updateControllers();
|
||||
registerInputDeviceListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
super.onPostCreate(savedInstanceState);
|
||||
enableFullscreenImmersive();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostResume() {
|
||||
super.onPostResume();
|
||||
enableFullscreenImmersive();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
Log.i("EmulationActivity", "OnStop");
|
||||
if (mEmulationThread != null) {
|
||||
mWasDestroyed = true;
|
||||
stopEmulationThread();
|
||||
}
|
||||
|
||||
unregisterInputDeviceListener();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (!checkActivityIsValid()) {
|
||||
// we must've got killed off in the background :(
|
||||
return;
|
||||
}
|
||||
|
||||
if (requestCode == REQUEST_CODE_SETTINGS) {
|
||||
if (AndroidHostInterface.getInstance().isEmulationThreadRunning()) {
|
||||
applySettings();
|
||||
}
|
||||
} else if (requestCode == REQUEST_IMPORT_PATCH_CODES) {
|
||||
if (data == null)
|
||||
return;
|
||||
|
||||
importPatchesFromFile(data.getData());
|
||||
} else if (requestCode == REQUEST_CHANGE_DISC_FILE) {
|
||||
if (data == null)
|
||||
return;
|
||||
|
||||
String path = GameDirectoriesActivity.getPathFromUri(this, data.getData());
|
||||
if (path == null)
|
||||
return;
|
||||
|
||||
AndroidHostInterface.getInstance().setMediaFilename(path);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
showMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (mContentView.onKeyDown(event.getKeyCode(), event))
|
||||
return true;
|
||||
} else if (event.getAction() == KeyEvent.ACTION_UP) {
|
||||
if (mContentView.onKeyUp(event.getKeyCode(), event))
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchGenericMotionEvent(MotionEvent ev) {
|
||||
if (mContentView.onGenericMotionEvent(ev))
|
||||
return true;
|
||||
|
||||
return super.dispatchGenericMotionEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
if (checkActivityIsValid())
|
||||
updateOrientation(newConfig.orientation);
|
||||
}
|
||||
|
||||
private void updateRequestedOrientation() {
|
||||
final String orientation = getStringSetting("Main/EmulationScreenOrientation", "unspecified");
|
||||
if (orientation.equals("portrait"))
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT);
|
||||
else if (orientation.equals("landscape"))
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE);
|
||||
else
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
}
|
||||
|
||||
private void updateOrientation() {
|
||||
final int orientation = getResources().getConfiguration().orientation;
|
||||
updateOrientation(orientation);
|
||||
}
|
||||
|
||||
private void updateOrientation(int newOrientation) {
|
||||
if (newOrientation == Configuration.ORIENTATION_PORTRAIT)
|
||||
AndroidHostInterface.getInstance().setDisplayAlignment(AndroidHostInterface.DISPLAY_ALIGNMENT_TOP_OR_LEFT);
|
||||
else
|
||||
AndroidHostInterface.getInstance().setDisplayAlignment(AndroidHostInterface.DISPLAY_ALIGNMENT_CENTER);
|
||||
|
||||
if (mTouchscreenController != null)
|
||||
mTouchscreenController.updateOrientation();
|
||||
}
|
||||
|
||||
private void enableFullscreenImmersive() {
|
||||
getWindow().getDecorView().setSystemUiVisibility(
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
|
||||
View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
|
||||
if (mContentView != null)
|
||||
mContentView.requestFocus();
|
||||
}
|
||||
|
||||
private static final int REQUEST_CODE_SETTINGS = 0;
|
||||
private static final int REQUEST_IMPORT_PATCH_CODES = 1;
|
||||
private static final int REQUEST_CHANGE_DISC_FILE = 2;
|
||||
|
||||
private void onMenuClosed() {
|
||||
enableFullscreenImmersive();
|
||||
|
||||
if (AndroidHostInterface.getInstance().isEmulationThreadPaused())
|
||||
AndroidHostInterface.getInstance().pauseEmulationThread(false);
|
||||
}
|
||||
|
||||
private void showMenu() {
|
||||
if (getBooleanSetting("Main/PauseOnMenu", false) &&
|
||||
!AndroidHostInterface.getInstance().isEmulationThreadPaused()) {
|
||||
AndroidHostInterface.getInstance().pauseEmulationThread(true);
|
||||
}
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
if (mGameTitle != null && !mGameTitle.isEmpty())
|
||||
builder.setTitle(mGameTitle);
|
||||
builder.setItems(R.array.emulation_menu, (dialogInterface, i) -> {
|
||||
switch (i) {
|
||||
case 0: // Load State
|
||||
{
|
||||
showSaveStateMenu(false);
|
||||
return;
|
||||
}
|
||||
|
||||
case 1: // Save State
|
||||
{
|
||||
showSaveStateMenu(true);
|
||||
return;
|
||||
}
|
||||
|
||||
case 2: // Toggle Fast Forward
|
||||
{
|
||||
AndroidHostInterface.getInstance().setFastForwardEnabled(!AndroidHostInterface.getInstance().isFastForwardEnabled());
|
||||
onMenuClosed();
|
||||
return;
|
||||
}
|
||||
|
||||
case 3: // More Options
|
||||
{
|
||||
showMoreMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
case 4: // Quit
|
||||
{
|
||||
mStopRequested = true;
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.setOnCancelListener(dialogInterface -> onMenuClosed());
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void showSaveStateMenu(boolean saving) {
|
||||
final SaveStateInfo[] infos = AndroidHostInterface.getInstance().getSaveStateInfo(true);
|
||||
if (infos == null) {
|
||||
onMenuClosed();
|
||||
return;
|
||||
}
|
||||
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
final ListView listView = new ListView(this);
|
||||
listView.setAdapter(new SaveStateInfo.ListAdapter(this, infos));
|
||||
builder.setView(listView);
|
||||
builder.setOnDismissListener((dialog) -> {
|
||||
onMenuClosed();
|
||||
});
|
||||
|
||||
final AlertDialog dialog = builder.create();
|
||||
|
||||
listView.setOnItemClickListener((parent, view, position, id) -> {
|
||||
SaveStateInfo info = infos[position];
|
||||
if (saving) {
|
||||
AndroidHostInterface.getInstance().saveState(info.isGlobal(), info.getSlot());
|
||||
} else {
|
||||
AndroidHostInterface.getInstance().loadState(info.isGlobal(), info.getSlot());
|
||||
}
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
private void showMoreMenu() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
if (mGameTitle != null && !mGameTitle.isEmpty())
|
||||
builder.setTitle(mGameTitle);
|
||||
|
||||
builder.setItems(R.array.emulation_more_menu, (dialogInterface, i) -> {
|
||||
switch (i) {
|
||||
case 0: // Reset
|
||||
{
|
||||
AndroidHostInterface.getInstance().resetSystem();
|
||||
onMenuClosed();
|
||||
return;
|
||||
}
|
||||
|
||||
case 1: // Patch Codes
|
||||
{
|
||||
showPatchesMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
case 2: // Change Disc
|
||||
{
|
||||
showDiscChangeMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
case 3: // Settings
|
||||
{
|
||||
Intent intent = new Intent(EmulationActivity.this, SettingsActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
startActivityForResult(intent, REQUEST_CODE_SETTINGS);
|
||||
return;
|
||||
}
|
||||
|
||||
case 4: // Change Touchscreen Controller
|
||||
{
|
||||
showTouchscreenControllerMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
case 5: // Edit Touchscreen Controller Layout
|
||||
{
|
||||
if (mTouchscreenController != null)
|
||||
mTouchscreenController.startLayoutEditing();
|
||||
|
||||
onMenuClosed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
builder.setOnCancelListener(dialogInterface -> onMenuClosed());
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void showTouchscreenControllerMenu() {
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
builder.setItems(R.array.settings_touchscreen_controller_view_entries, (dialogInterface, i) -> {
|
||||
String[] values = getResources().getStringArray(R.array.settings_touchscreen_controller_view_values);
|
||||
setStringSetting("Controller1/TouchscreenControllerView", values[i]);
|
||||
updateControllers();
|
||||
onMenuClosed();
|
||||
});
|
||||
builder.setOnCancelListener(dialogInterface -> onMenuClosed());
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void showPatchesMenu() {
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
|
||||
final PatchCode[] codes = AndroidHostInterface.getInstance().getPatchCodeList();
|
||||
if (codes != null) {
|
||||
CharSequence[] items = new CharSequence[codes.length];
|
||||
boolean[] itemsChecked = new boolean[codes.length];
|
||||
for (int i = 0; i < codes.length; i++) {
|
||||
final PatchCode cc = codes[i];
|
||||
items[i] = cc.getDescription();
|
||||
itemsChecked[i] = cc.isEnabled();
|
||||
}
|
||||
|
||||
builder.setMultiChoiceItems(items, itemsChecked, (dialogInterface, i, checked) -> {
|
||||
AndroidHostInterface.getInstance().setPatchCodeEnabled(i, checked);
|
||||
});
|
||||
}
|
||||
|
||||
builder.setNegativeButton(R.string.emulation_activity_ok, (dialogInterface, i) -> {
|
||||
dialogInterface.dismiss();
|
||||
});
|
||||
builder.setNeutralButton(R.string.emulation_activity_import_patch_codes, (dialogInterface, i) -> {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
intent.setType("*/*");
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
startActivityForResult(Intent.createChooser(intent, getString(R.string.emulation_activity_choose_patch_code_file)), REQUEST_IMPORT_PATCH_CODES);
|
||||
});
|
||||
|
||||
builder.setOnDismissListener(dialogInterface -> onMenuClosed());
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
private void importPatchesFromFile(Uri uri) {
|
||||
String str = FileUtil.readFileFromUri(this, uri, 512 * 1024);
|
||||
if (str == null || !AndroidHostInterface.getInstance().importPatchCodesFromString(str)) {
|
||||
reportErrorOnUIThread(getString(R.string.emulation_activity_failed_to_import_patch_codes));
|
||||
}
|
||||
}
|
||||
|
||||
private void showDiscChangeMenu() {
|
||||
final String[] paths = AndroidHostInterface.getInstance().getMediaPlaylistPaths();
|
||||
final int currentPath = AndroidHostInterface.getInstance().getMediaPlaylistIndex();
|
||||
final int numPaths = (paths != null) ? paths.length : 0;
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this);
|
||||
|
||||
CharSequence[] items = new CharSequence[numPaths + 1];
|
||||
for (int i = 0; i < numPaths; i++)
|
||||
items[i] = GameListEntry.getFileNameForPath(paths[i]);
|
||||
items[numPaths] = "Select New File...";
|
||||
|
||||
builder.setSingleChoiceItems(items, (currentPath < numPaths) ? currentPath : -1, (dialogInterface, i) -> {
|
||||
dialogInterface.dismiss();
|
||||
onMenuClosed();
|
||||
|
||||
if (i < numPaths) {
|
||||
AndroidHostInterface.getInstance().setMediaPlaylistIndex(i);
|
||||
} else {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
startActivityForResult(Intent.createChooser(intent, getString(R.string.main_activity_choose_disc_image)), REQUEST_CHANGE_DISC_FILE);
|
||||
}
|
||||
});
|
||||
builder.setOnCancelListener(dialogInterface -> onMenuClosed());
|
||||
builder.create().show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Touchscreen controller overlay
|
||||
*/
|
||||
TouchscreenControllerView mTouchscreenController;
|
||||
|
||||
public void updateControllers() {
|
||||
final String controllerType = getStringSetting("Controller1/Type", "DigitalController");
|
||||
final String viewType = getStringSetting("Controller1/TouchscreenControllerView", "digital");
|
||||
final boolean autoHideTouchscreenController = getBooleanSetting("Controller1/AutoHideTouchscreenController", false);
|
||||
final boolean hapticFeedback = getBooleanSetting("Controller1/HapticFeedback", false);
|
||||
final boolean vibration = getBooleanSetting("Controller1/Vibration", false);
|
||||
final FrameLayout activityLayout = findViewById(R.id.frameLayout);
|
||||
|
||||
Log.i("EmulationActivity", "Controller type: " + controllerType);
|
||||
Log.i("EmulationActivity", "View type: " + viewType);
|
||||
|
||||
final boolean hasAnyControllers = mContentView.initControllerMapping(controllerType);
|
||||
|
||||
if (controllerType.equals("none") || viewType.equals("none") || (hasAnyControllers && autoHideTouchscreenController)) {
|
||||
if (mTouchscreenController != null) {
|
||||
activityLayout.removeView(mTouchscreenController);
|
||||
mTouchscreenController = null;
|
||||
mVibratorService = null;
|
||||
}
|
||||
} else {
|
||||
if (mTouchscreenController == null) {
|
||||
mTouchscreenController = new TouchscreenControllerView(this);
|
||||
if (vibration)
|
||||
mVibratorService = (Vibrator) getSystemService(VIBRATOR_SERVICE);
|
||||
|
||||
activityLayout.addView(mTouchscreenController);
|
||||
}
|
||||
|
||||
mTouchscreenController.init(0, controllerType, viewType, hapticFeedback);
|
||||
}
|
||||
}
|
||||
|
||||
private InputManager.InputDeviceListener mInputDeviceListener;
|
||||
|
||||
private void registerInputDeviceListener() {
|
||||
if (mInputDeviceListener != null)
|
||||
return;
|
||||
|
||||
mInputDeviceListener = new InputManager.InputDeviceListener() {
|
||||
@Override
|
||||
public void onInputDeviceAdded(int i) {
|
||||
Log.i("EmulationActivity", String.format("InputDeviceAdded %d", i));
|
||||
updateControllers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceRemoved(int i) {
|
||||
Log.i("EmulationActivity", String.format("InputDeviceRemoved %d", i));
|
||||
updateControllers();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceChanged(int i) {
|
||||
Log.i("EmulationActivity", String.format("InputDeviceChanged %d", i));
|
||||
updateControllers();
|
||||
}
|
||||
};
|
||||
|
||||
InputManager inputManager = ((InputManager) getSystemService(Context.INPUT_SERVICE));
|
||||
if (inputManager != null)
|
||||
inputManager.registerInputDeviceListener(mInputDeviceListener, null);
|
||||
}
|
||||
|
||||
private void unregisterInputDeviceListener() {
|
||||
if (mInputDeviceListener == null)
|
||||
return;
|
||||
|
||||
InputManager inputManager = ((InputManager) getSystemService(Context.INPUT_SERVICE));
|
||||
if (inputManager != null)
|
||||
inputManager.unregisterInputDeviceListener(mInputDeviceListener);
|
||||
|
||||
mInputDeviceListener = null;
|
||||
}
|
||||
|
||||
private Vibrator mVibratorService;
|
||||
|
||||
public void setVibration(boolean enabled) {
|
||||
if (mVibratorService == null)
|
||||
return;
|
||||
|
||||
runOnUiThread(() -> {
|
||||
if (mVibratorService == null)
|
||||
return;
|
||||
|
||||
if (enabled)
|
||||
mVibratorService.vibrate(1000);
|
||||
else
|
||||
mVibratorService.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
private boolean mSustainedPerformanceModeEnabled = false;
|
||||
|
||||
private void updateSustainedPerformanceMode() {
|
||||
final boolean enabled = getBooleanSetting("Main/SustainedPerformanceMode", false);
|
||||
if (mSustainedPerformanceModeEnabled == enabled)
|
||||
return;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
getWindow().setSustainedPerformanceMode(enabled);
|
||||
Log.i("EmulationActivity", String.format("%s sustained performance mode.", enabled ? "enabling" : "disabling"));
|
||||
} else {
|
||||
Log.e("EmulationActivity", "Sustained performance mode not supported.");
|
||||
}
|
||||
mSustainedPerformanceModeEnabled = enabled;
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.InputDevice;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.SurfaceView;
|
||||
|
||||
import androidx.core.util.Pair;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class EmulationSurfaceView extends SurfaceView {
|
||||
public EmulationSurfaceView(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public EmulationSurfaceView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public EmulationSurfaceView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
public static boolean isDPadOrButtonEvent(KeyEvent event) {
|
||||
final int source = event.getSource();
|
||||
return (source & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD ||
|
||||
(source & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD ||
|
||||
(source & InputDevice.SOURCE_JOYSTICK) == InputDevice.SOURCE_JOYSTICK;
|
||||
}
|
||||
|
||||
private boolean isExternalKeyCode(int keyCode) {
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_BACK:
|
||||
case KeyEvent.KEYCODE_HOME:
|
||||
case KeyEvent.KEYCODE_MENU:
|
||||
case KeyEvent.KEYCODE_VOLUME_UP:
|
||||
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
||||
case KeyEvent.KEYCODE_VOLUME_MUTE:
|
||||
case KeyEvent.KEYCODE_POWER:
|
||||
case KeyEvent.KEYCODE_CAMERA:
|
||||
case KeyEvent.KEYCODE_CALL:
|
||||
case KeyEvent.KEYCODE_ENDCALL:
|
||||
case KeyEvent.KEYCODE_VOICE_ASSIST:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static final int[] buttonKeyCodes = new int[]{
|
||||
KeyEvent.KEYCODE_BUTTON_A, // 0/Cross
|
||||
KeyEvent.KEYCODE_BUTTON_B, // 1/Circle
|
||||
KeyEvent.KEYCODE_BUTTON_X, // 2/Square
|
||||
KeyEvent.KEYCODE_BUTTON_Y, // 3/Triangle
|
||||
KeyEvent.KEYCODE_BUTTON_SELECT, // 4/Select
|
||||
KeyEvent.KEYCODE_BUTTON_MODE, // 5/Analog
|
||||
KeyEvent.KEYCODE_BUTTON_START, // 6/Start
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBL, // 7/L3
|
||||
KeyEvent.KEYCODE_BUTTON_THUMBR, // 8/R3
|
||||
KeyEvent.KEYCODE_BUTTON_L1, // 9/L1
|
||||
KeyEvent.KEYCODE_BUTTON_R1, // 10/R1
|
||||
KeyEvent.KEYCODE_DPAD_UP, // 11/Up
|
||||
KeyEvent.KEYCODE_DPAD_DOWN, // 12/Down
|
||||
KeyEvent.KEYCODE_DPAD_LEFT, // 13/Left
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT, // 14/Right
|
||||
KeyEvent.KEYCODE_BUTTON_L2, // 15
|
||||
KeyEvent.KEYCODE_BUTTON_R2, // 16
|
||||
KeyEvent.KEYCODE_BUTTON_C, // 17
|
||||
KeyEvent.KEYCODE_BUTTON_Z, // 18
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN, // 19
|
||||
KeyEvent.KEYCODE_VOLUME_UP, // 20
|
||||
KeyEvent.KEYCODE_MENU, // 21
|
||||
KeyEvent.KEYCODE_CAMERA, // 22
|
||||
};
|
||||
private static final int[] axisCodes = new int[]{
|
||||
MotionEvent.AXIS_X, // 0/LeftX
|
||||
MotionEvent.AXIS_Y, // 1/LeftY
|
||||
MotionEvent.AXIS_Z, // 2/RightX
|
||||
MotionEvent.AXIS_RZ, // 3/RightY
|
||||
MotionEvent.AXIS_LTRIGGER, // 4/L2
|
||||
MotionEvent.AXIS_RTRIGGER, // 5/R2
|
||||
MotionEvent.AXIS_RX, // 6
|
||||
MotionEvent.AXIS_RY, // 7
|
||||
MotionEvent.AXIS_HAT_X, // 8
|
||||
MotionEvent.AXIS_HAT_Y, // 9
|
||||
MotionEvent.AXIS_GAS, // 10
|
||||
MotionEvent.AXIS_BRAKE, // 11
|
||||
};
|
||||
|
||||
public static int getButtonIndexForKeyCode(int keyCode) {
|
||||
for (int buttonIndex = 0; buttonIndex < buttonKeyCodes.length; buttonIndex++) {
|
||||
if (buttonKeyCodes[buttonIndex] == keyCode)
|
||||
return buttonIndex;
|
||||
}
|
||||
|
||||
Log.e("EmulationSurfaceView", String.format("Button code %d not found", keyCode));
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static int[] getKnownAxisCodes() {
|
||||
return axisCodes;
|
||||
}
|
||||
|
||||
public static int getAxisIndexForAxisCode(int axisCode) {
|
||||
for (int axisIndex = 0; axisIndex < axisCodes.length; axisIndex++) {
|
||||
if (axisCodes[axisIndex] == axisCode)
|
||||
return axisIndex;
|
||||
}
|
||||
|
||||
Log.e("EmulationSurfaceView", String.format("Axis code %d not found", axisCode));
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
private class ButtonMapping {
|
||||
public ButtonMapping(int deviceId, int deviceButton, int controllerIndex, int button) {
|
||||
this.deviceId = deviceId;
|
||||
this.deviceAxisOrButton = deviceButton;
|
||||
this.controllerIndex = controllerIndex;
|
||||
this.buttonMapping = button;
|
||||
}
|
||||
|
||||
public int deviceId;
|
||||
public int deviceAxisOrButton;
|
||||
public int controllerIndex;
|
||||
public int buttonMapping;
|
||||
}
|
||||
|
||||
private class AxisMapping {
|
||||
public AxisMapping(int deviceId, int deviceAxis, InputDevice.MotionRange motionRange, int controllerIndex, int axis) {
|
||||
this.deviceId = deviceId;
|
||||
this.deviceAxisOrButton = deviceAxis;
|
||||
this.deviceMotionRange = motionRange;
|
||||
this.controllerIndex = controllerIndex;
|
||||
this.axisMapping = axis;
|
||||
this.positiveButton = -1;
|
||||
this.negativeButton = -1;
|
||||
}
|
||||
|
||||
public AxisMapping(int deviceId, int deviceAxis, InputDevice.MotionRange motionRange, int controllerIndex, int positiveButton, int negativeButton) {
|
||||
this.deviceId = deviceId;
|
||||
this.deviceAxisOrButton = deviceAxis;
|
||||
this.deviceMotionRange = motionRange;
|
||||
this.controllerIndex = controllerIndex;
|
||||
this.axisMapping = -1;
|
||||
this.positiveButton = positiveButton;
|
||||
this.negativeButton = negativeButton;
|
||||
}
|
||||
|
||||
public int deviceId;
|
||||
public int deviceAxisOrButton;
|
||||
public InputDevice.MotionRange deviceMotionRange;
|
||||
public int controllerIndex;
|
||||
public int axisMapping;
|
||||
public int positiveButton;
|
||||
public int negativeButton;
|
||||
}
|
||||
|
||||
private ArrayList<ButtonMapping> mControllerKeyMapping;
|
||||
private ArrayList<AxisMapping> mControllerAxisMapping;
|
||||
|
||||
private boolean handleControllerKey(int deviceId, int keyCode, int repeatCount, boolean pressed) {
|
||||
boolean result = false;
|
||||
for (ButtonMapping mapping : mControllerKeyMapping) {
|
||||
if (mapping.deviceId != deviceId || mapping.deviceAxisOrButton != keyCode)
|
||||
continue;
|
||||
|
||||
if (repeatCount == 0) {
|
||||
AndroidHostInterface.getInstance().handleControllerButtonEvent(0, mapping.buttonMapping, pressed);
|
||||
Log.d("EmulationSurfaceView", String.format("handleControllerKey %d -> %d %d", keyCode, mapping.buttonMapping, pressed ? 1 : 0));
|
||||
}
|
||||
|
||||
result = true;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||
if (!isDPadOrButtonEvent(event))
|
||||
return false;
|
||||
|
||||
if (handleControllerKey(event.getDeviceId(), keyCode, event.getRepeatCount(), true))
|
||||
return true;
|
||||
|
||||
// eat non-external button events anyway
|
||||
return !isExternalKeyCode(keyCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||
if (!isDPadOrButtonEvent(event))
|
||||
return false;
|
||||
|
||||
if (handleControllerKey(event.getDeviceId(), keyCode, 0, false))
|
||||
return true;
|
||||
|
||||
// eat non-external button events anyway
|
||||
return !isExternalKeyCode(keyCode);
|
||||
}
|
||||
|
||||
private float clamp(float value, float min, float max) {
|
||||
return (value < min) ? min : ((value > max) ? max : value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onGenericMotionEvent(MotionEvent event) {
|
||||
final int source = event.getSource();
|
||||
if ((source & (InputDevice.SOURCE_JOYSTICK | InputDevice.SOURCE_GAMEPAD | InputDevice.SOURCE_DPAD)) == 0)
|
||||
return false;
|
||||
|
||||
final int deviceId = event.getDeviceId();
|
||||
for (AxisMapping mapping : mControllerAxisMapping) {
|
||||
if (mapping.deviceId != deviceId)
|
||||
continue;
|
||||
|
||||
final float axisValue = event.getAxisValue(mapping.deviceAxisOrButton);
|
||||
float emuValue;
|
||||
|
||||
switch (mapping.deviceAxisOrButton) {
|
||||
case MotionEvent.AXIS_BRAKE:
|
||||
case MotionEvent.AXIS_GAS:
|
||||
case MotionEvent.AXIS_LTRIGGER:
|
||||
case MotionEvent.AXIS_RTRIGGER:
|
||||
// Scale 0..1 -> -1..1.
|
||||
emuValue = (clamp(axisValue, 0.0f, 1.0f) * 2.0f) - 1.0f;
|
||||
break;
|
||||
|
||||
default:
|
||||
// Everything else should already by -1..1 as per Android documentation.
|
||||
emuValue = clamp(axisValue, -1.0f, 1.0f);
|
||||
break;
|
||||
}
|
||||
|
||||
Log.d("EmulationSurfaceView", String.format("axis %d value %f emuvalue %f", mapping.deviceAxisOrButton, axisValue, emuValue));
|
||||
|
||||
if (mapping.axisMapping >= 0) {
|
||||
AndroidHostInterface.getInstance().handleControllerAxisEvent(0, mapping.axisMapping, emuValue);
|
||||
}
|
||||
|
||||
final float DEAD_ZONE = 0.25f;
|
||||
if (mapping.negativeButton >= 0) {
|
||||
AndroidHostInterface.getInstance().handleControllerButtonEvent(0, mapping.negativeButton, (emuValue <= -DEAD_ZONE));
|
||||
}
|
||||
if (mapping.positiveButton >= 0) {
|
||||
AndroidHostInterface.getInstance().handleControllerButtonEvent(0, mapping.positiveButton, (emuValue >= DEAD_ZONE));
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean addControllerKeyMapping(int deviceId, int keyCode, int controllerIndex) {
|
||||
int mapping = getButtonIndexForKeyCode(keyCode);
|
||||
Log.i("EmulationSurfaceView", String.format("Map %d to %d", keyCode, mapping));
|
||||
if (mapping >= 0) {
|
||||
mControllerKeyMapping.add(new ButtonMapping(deviceId, keyCode, controllerIndex, mapping));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean addControllerAxisMapping(int deviceId, List<InputDevice.MotionRange> motionRanges, int axis, int controllerIndex) {
|
||||
InputDevice.MotionRange range = null;
|
||||
for (InputDevice.MotionRange curRange : motionRanges) {
|
||||
if (curRange.getAxis() == axis) {
|
||||
range = curRange;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (range == null)
|
||||
return false;
|
||||
|
||||
int mapping = getAxisIndexForAxisCode(axis);
|
||||
int negativeButton = -1;
|
||||
int positiveButton = -1;
|
||||
|
||||
if (mapping >= 0) {
|
||||
Log.i("EmulationSurfaceView", String.format("Map axis %d to %d", axis, mapping));
|
||||
mControllerAxisMapping.add(new AxisMapping(deviceId, axis, range, controllerIndex, mapping));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (negativeButton >= 0 && negativeButton >= 0) {
|
||||
Log.i("EmulationSurfaceView", String.format("Map axis %d to buttons %d %d", axis, negativeButton, positiveButton));
|
||||
mControllerAxisMapping.add(new AxisMapping(deviceId, axis, range, controllerIndex, positiveButton, negativeButton));
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.w("EmulationSurfaceView", String.format("Axis %d was not mapped", axis));
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean isJoystickDevice(int deviceId) {
|
||||
if (deviceId < 0)
|
||||
return false;
|
||||
|
||||
final InputDevice dev = InputDevice.getDevice(deviceId);
|
||||
if (dev == null)
|
||||
return false;
|
||||
|
||||
final int sources = dev.getSources();
|
||||
if ((sources & InputDevice.SOURCE_CLASS_JOYSTICK) != 0)
|
||||
return true;
|
||||
|
||||
if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD)
|
||||
return true;
|
||||
|
||||
return (sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD;
|
||||
}
|
||||
|
||||
public boolean initControllerMapping(String controllerType) {
|
||||
mControllerKeyMapping = new ArrayList<>();
|
||||
mControllerAxisMapping = new ArrayList<>();
|
||||
|
||||
final int[] deviceIds = InputDevice.getDeviceIds();
|
||||
for (int deviceId : deviceIds) {
|
||||
if (!isJoystickDevice(deviceId))
|
||||
continue;
|
||||
|
||||
InputDevice device = InputDevice.getDevice(deviceId);
|
||||
List<InputDevice.MotionRange> motionRanges = device.getMotionRanges();
|
||||
int controllerIndex = 0;
|
||||
|
||||
for (int keyCode : buttonKeyCodes) {
|
||||
addControllerKeyMapping(deviceId, keyCode, controllerIndex);
|
||||
}
|
||||
|
||||
if (motionRanges != null) {
|
||||
for (int axisCode : axisCodes) {
|
||||
addControllerAxisMapping(deviceId, motionRanges, axisCode, controllerIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return !mControllerKeyMapping.isEmpty() || !mControllerKeyMapping.isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.Process;
|
||||
import android.util.Log;
|
||||
import android.view.Surface;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class EmulationThread extends Thread {
|
||||
private EmulationActivity emulationActivity;
|
||||
private String filename;
|
||||
private boolean resumeState;
|
||||
private String stateFilename;
|
||||
|
||||
private EmulationThread(EmulationActivity emulationActivity, String filename, boolean resumeState, String stateFilename) {
|
||||
super("EmulationThread");
|
||||
this.emulationActivity = emulationActivity;
|
||||
this.filename = filename;
|
||||
this.resumeState = resumeState;
|
||||
this.stateFilename = stateFilename;
|
||||
}
|
||||
|
||||
public static EmulationThread create(EmulationActivity emulationActivity, String filename, boolean resumeState, String stateFilename) {
|
||||
Log.i("EmulationThread", String.format("Starting emulation thread (%s)...", filename));
|
||||
|
||||
EmulationThread thread = new EmulationThread(emulationActivity, filename, resumeState, stateFilename);
|
||||
thread.start();
|
||||
return thread;
|
||||
}
|
||||
|
||||
private void setExclusiveCores() {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
int[] cores = Process.getExclusiveCores();
|
||||
if (cores == null || cores.length == 0)
|
||||
throw new Exception("Invalid return value from getExclusiveCores()");
|
||||
|
||||
AndroidHostInterface.setThreadAffinity(cores);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e("EmulationThread", "getExclusiveCores() failed");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
Process.setThreadPriority(Process.THREAD_PRIORITY_MORE_FAVORABLE);
|
||||
setExclusiveCores();
|
||||
} catch (Exception e) {
|
||||
Log.i("EmulationThread", "Failed to set priority for emulation thread: " + e.getMessage());
|
||||
}
|
||||
|
||||
AndroidHostInterface.getInstance().runEmulationThread(emulationActivity, filename, resumeState, stateFilename);
|
||||
Log.i("EmulationThread", "Emulation thread exiting.");
|
||||
}
|
||||
|
||||
public void stopAndJoin() {
|
||||
AndroidHostInterface.getInstance().stopEmulationThreadLoop();
|
||||
try {
|
||||
join();
|
||||
} catch (InterruptedException e) {
|
||||
Log.i("EmulationThread", "join() interrupted: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
// https://stackoverflow.com/questions/34927748/android-5-0-documentfile-from-tree-uri
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.storage.StorageManager;
|
||||
import android.provider.DocumentsContract;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public final class FileUtil {
|
||||
static String TAG = "TAG";
|
||||
private static final String PRIMARY_VOLUME_NAME = "primary";
|
||||
|
||||
@Nullable
|
||||
public static String getFullPathFromTreeUri(@Nullable final Uri treeUri, Context con) {
|
||||
if (treeUri == null) return null;
|
||||
String volumePath = getVolumePath(getVolumeIdFromTreeUri(treeUri), con);
|
||||
if (volumePath == null) return File.separator;
|
||||
if (volumePath.endsWith(File.separator))
|
||||
volumePath = volumePath.substring(0, volumePath.length() - 1);
|
||||
|
||||
String documentPath = getDocumentPathFromTreeUri(treeUri);
|
||||
if (documentPath.endsWith(File.separator))
|
||||
documentPath = documentPath.substring(0, documentPath.length() - 1);
|
||||
|
||||
if (documentPath.length() > 0) {
|
||||
if (documentPath.startsWith(File.separator))
|
||||
return volumePath + documentPath;
|
||||
else
|
||||
return volumePath + File.separator + documentPath;
|
||||
} else return volumePath;
|
||||
}
|
||||
|
||||
@SuppressLint("ObsoleteSdkInt")
|
||||
private static String getVolumePath(final String volumeId, Context context) {
|
||||
if (volumeId == null)
|
||||
return null;
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) return null;
|
||||
try {
|
||||
StorageManager mStorageManager =
|
||||
(StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
|
||||
Class<?> storageVolumeClazz = Class.forName("android.os.storage.StorageVolume");
|
||||
Method getVolumeList = mStorageManager.getClass().getMethod("getVolumeList");
|
||||
Method getUuid = storageVolumeClazz.getMethod("getUuid");
|
||||
Method getPath = storageVolumeClazz.getMethod("getPath");
|
||||
Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
|
||||
Object result = getVolumeList.invoke(mStorageManager);
|
||||
|
||||
final int length = Array.getLength(result);
|
||||
for (int i = 0; i < length; i++) {
|
||||
Object storageVolumeElement = Array.get(result, i);
|
||||
String uuid = (String) getUuid.invoke(storageVolumeElement);
|
||||
Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);
|
||||
|
||||
// primary volume?
|
||||
if (primary && PRIMARY_VOLUME_NAME.equals(volumeId))
|
||||
return (String) getPath.invoke(storageVolumeElement);
|
||||
|
||||
// other volumes?
|
||||
if (uuid != null && uuid.equals(volumeId))
|
||||
return (String) getPath.invoke(storageVolumeElement);
|
||||
}
|
||||
// not found.
|
||||
return null;
|
||||
} catch (Exception ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private static String getVolumeIdFromTreeUri(final Uri treeUri) {
|
||||
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
|
||||
final String[] split = docId.split(":");
|
||||
if (split.length > 0) return split[0];
|
||||
else return null;
|
||||
}
|
||||
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private static String getDocumentPathFromTreeUri(final Uri treeUri) {
|
||||
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
|
||||
final String[] split = docId.split(":");
|
||||
if ((split.length >= 2) && (split[1] != null)) return split[1];
|
||||
else return File.separator;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static String getFullPathFromUri(@Nullable final Uri treeUri, Context con) {
|
||||
if (treeUri == null) return null;
|
||||
String volumePath = getVolumePath(getVolumeIdFromUri(treeUri), con);
|
||||
if (volumePath == null) return File.separator;
|
||||
if (volumePath.endsWith(File.separator))
|
||||
volumePath = volumePath.substring(0, volumePath.length() - 1);
|
||||
|
||||
String documentPath = getDocumentPathFromUri(treeUri);
|
||||
if (documentPath.endsWith(File.separator))
|
||||
documentPath = documentPath.substring(0, documentPath.length() - 1);
|
||||
|
||||
if (documentPath.length() > 0) {
|
||||
if (documentPath.startsWith(File.separator))
|
||||
return volumePath + documentPath;
|
||||
else
|
||||
return volumePath + File.separator + documentPath;
|
||||
} else return volumePath;
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private static String getVolumeIdFromUri(final Uri treeUri) {
|
||||
try {
|
||||
final String docId = DocumentsContract.getDocumentId(treeUri);
|
||||
final String[] split = docId.split(":");
|
||||
if (split.length > 0) return split[0];
|
||||
else return null;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
private static String getDocumentPathFromUri(final Uri treeUri) {
|
||||
try {
|
||||
final String docId = DocumentsContract.getDocumentId(treeUri);
|
||||
final String[] split = docId.split(":");
|
||||
if ((split.length >= 2) && (split[1] != null)) return split[1];
|
||||
else return File.separator;
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String readFileFromUri(final Context context, final Uri uri, int maxSize) {
|
||||
InputStream stream = null;
|
||||
try {
|
||||
stream = context.getContentResolver().openInputStream(uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder os = new StringBuilder();
|
||||
try {
|
||||
char[] buffer = new char[1024];
|
||||
InputStreamReader reader = new InputStreamReader(stream, Charset.forName(StandardCharsets.UTF_8.name()));
|
||||
int len;
|
||||
while ((len = reader.read(buffer)) > 0) {
|
||||
os.append(buffer, 0, len);
|
||||
if (os.length() > maxSize)
|
||||
return null;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (os.length() == 0)
|
||||
return null;
|
||||
|
||||
return os.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.Property;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.ListFragment;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class GameDirectoriesActivity extends AppCompatActivity {
|
||||
private static final int REQUEST_ADD_DIRECTORY_TO_GAME_LIST = 1;
|
||||
|
||||
private class DirectoryListAdapter extends RecyclerView.Adapter {
|
||||
private class Entry {
|
||||
private String mPath;
|
||||
private boolean mRecursive;
|
||||
|
||||
public Entry(String path, boolean recursive) {
|
||||
mPath = path;
|
||||
mRecursive = recursive;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return mPath;
|
||||
}
|
||||
|
||||
public boolean isRecursive() {
|
||||
return mRecursive;
|
||||
}
|
||||
|
||||
public void toggleRecursive() {
|
||||
mRecursive = !mRecursive;
|
||||
}
|
||||
}
|
||||
|
||||
private class EntryComparator implements Comparator<Entry> {
|
||||
@Override
|
||||
public int compare(Entry left, Entry right) {
|
||||
return left.getPath().compareTo(right.getPath());
|
||||
}
|
||||
}
|
||||
|
||||
private Context mContext;
|
||||
private Entry[] mEntries;
|
||||
|
||||
public DirectoryListAdapter(Context context) {
|
||||
mContext = context;
|
||||
reload();
|
||||
}
|
||||
|
||||
public void reload() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
|
||||
ArrayList<Entry> entries = new ArrayList<>();
|
||||
|
||||
try {
|
||||
Set<String> paths = prefs.getStringSet("GameList/Paths", null);
|
||||
if (paths != null) {
|
||||
for (String path : paths)
|
||||
entries.add(new Entry(path, false));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
try {
|
||||
Set<String> paths = prefs.getStringSet("GameList/RecursivePaths", null);
|
||||
if (paths != null) {
|
||||
for (String path : paths)
|
||||
entries.add(new Entry(path, true));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
}
|
||||
|
||||
mEntries = new Entry[entries.size()];
|
||||
entries.toArray(mEntries);
|
||||
Arrays.sort(mEntries, new EntryComparator());
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
private int mPosition;
|
||||
private Entry mEntry;
|
||||
private TextView mPathView;
|
||||
private TextView mRecursiveView;
|
||||
private ImageButton mToggleRecursiveView;
|
||||
private ImageButton mRemoveView;
|
||||
|
||||
public ViewHolder(View rootView) {
|
||||
super(rootView);
|
||||
mPathView = rootView.findViewById(R.id.path);
|
||||
mRecursiveView = rootView.findViewById(R.id.recursive);
|
||||
mToggleRecursiveView = rootView.findViewById(R.id.toggle_recursive);
|
||||
mToggleRecursiveView.setOnClickListener(this);
|
||||
mRemoveView = rootView.findViewById(R.id.remove);
|
||||
mRemoveView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
public void bindData(int position, Entry entry) {
|
||||
mPosition = position;
|
||||
mEntry = entry;
|
||||
updateText();
|
||||
}
|
||||
|
||||
private void updateText() {
|
||||
mPathView.setText(mEntry.getPath());
|
||||
mRecursiveView.setText(getString(mEntry.isRecursive() ? R.string.game_directories_scanning_subdirectories : R.string.game_directories_not_scanning_subdirectories));
|
||||
mToggleRecursiveView.setImageDrawable(getDrawable(mEntry.isRecursive() ? R.drawable.ic_baseline_folder_24 : R.drawable.ic_baseline_folder_open_24));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
if (mToggleRecursiveView == v) {
|
||||
removeSearchDirectory(mContext, mEntry.getPath(), mEntry.isRecursive());
|
||||
mEntry.toggleRecursive();
|
||||
addSearchDirectory(mContext, mEntry.getPath(), mEntry.isRecursive());
|
||||
updateText();
|
||||
} else if (mRemoveView == v) {
|
||||
removeSearchDirectory(mContext, mEntry.getPath(), mEntry.isRecursive());
|
||||
reload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
final View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
((ViewHolder) holder).bindData(position, mEntries[position]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return R.layout.layout_game_directory_entry;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return mEntries[position].getPath().hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return mEntries.length;
|
||||
}
|
||||
}
|
||||
|
||||
DirectoryListAdapter mDirectoryListAdapter;
|
||||
RecyclerView mRecyclerView;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
setContentView(R.layout.activity_game_directories);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
mDirectoryListAdapter = new DirectoryListAdapter(this);
|
||||
mRecyclerView = findViewById(R.id.recycler_view);
|
||||
mRecyclerView.setAdapter(mDirectoryListAdapter);
|
||||
mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
|
||||
|
||||
findViewById(R.id.fab).setOnClickListener((v) -> startAddGameDirectory());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
getMenuInflater().inflate(R.menu.menu_edit_game_directories, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (item.getItemId() == R.id.add_directory) {
|
||||
startAddGameDirectory();
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.add_path) {
|
||||
startAddPath();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
public static String getPathFromTreeUri(Context context, Uri treeUri) {
|
||||
String path = FileUtil.getFullPathFromTreeUri(treeUri, context);
|
||||
if (path.length() < 5) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.main_activity_error)
|
||||
.setMessage(R.string.main_activity_get_path_from_directory_error)
|
||||
.setPositiveButton(R.string.main_activity_ok, (dialog, button) -> {
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
return null;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public static String getPathFromUri(Context context, Uri uri) {
|
||||
String path = FileUtil.getFullPathFromUri(uri, context);
|
||||
if (path.length() < 5) {
|
||||
new AlertDialog.Builder(context)
|
||||
.setTitle(R.string.main_activity_error)
|
||||
.setMessage(R.string.main_activity_get_path_from_file_error)
|
||||
.setPositiveButton(R.string.main_activity_ok, (dialog, button) -> {
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
return null;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
public static void addSearchDirectory(Context context, String path, boolean recursive) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = recursive ? "GameList/RecursivePaths" : "GameList/Paths";
|
||||
PreferenceHelpers.addToStringList(prefs, key, path);
|
||||
Log.i("GameDirectoriesActivity", "Added path '" + path + "' to game list search directories");
|
||||
}
|
||||
|
||||
public static void removeSearchDirectory(Context context, String path, boolean recursive) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final String key = recursive ? "GameList/RecursivePaths" : "GameList/Paths";
|
||||
PreferenceHelpers.removeFromStringList(prefs, key, path);
|
||||
Log.i("GameDirectoriesActivity", "Removed path '" + path + "' from game list search directories");
|
||||
}
|
||||
|
||||
private void startAddGameDirectory() {
|
||||
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
i.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
i.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
|
||||
startActivityForResult(Intent.createChooser(i, getString(R.string.main_activity_choose_directory)),
|
||||
REQUEST_ADD_DIRECTORY_TO_GAME_LIST);
|
||||
}
|
||||
|
||||
private void startAddPath() {
|
||||
final EditText text = new EditText(this);
|
||||
text.setSingleLine();
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.edit_game_directories_add_path)
|
||||
.setMessage(R.string.edit_game_directories_add_path_summary)
|
||||
.setView(text)
|
||||
.setPositiveButton("Add", (dialog, which) -> {
|
||||
final String path = text.getText().toString();
|
||||
if (!path.isEmpty()) {
|
||||
addSearchDirectory(GameDirectoriesActivity.this, path, true);
|
||||
mDirectoryListAdapter.reload();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("Cancel", (dialog, which) -> dialog.dismiss())
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
switch (requestCode) {
|
||||
case REQUEST_ADD_DIRECTORY_TO_GAME_LIST: {
|
||||
if (resultCode != RESULT_OK)
|
||||
return;
|
||||
|
||||
String path = getPathFromTreeUri(this, data.getData());
|
||||
if (path == null)
|
||||
return;
|
||||
|
||||
addSearchDirectory(this, path, true);
|
||||
mDirectoryListAdapter.reload();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
|
||||
public class GameList {
|
||||
private Activity mContext;
|
||||
private GameListEntry[] mEntries;
|
||||
private ListViewAdapter mAdapter;
|
||||
|
||||
public GameList(Activity context) {
|
||||
mContext = context;
|
||||
mAdapter = new ListViewAdapter();
|
||||
mEntries = new GameListEntry[0];
|
||||
}
|
||||
|
||||
private class GameListEntryComparator implements Comparator<GameListEntry> {
|
||||
@Override
|
||||
public int compare(GameListEntry left, GameListEntry right) {
|
||||
return left.getTitle().compareTo(right.getTitle());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void refresh(boolean invalidateCache, boolean invalidateDatabase, Activity parentActivity) {
|
||||
// Search and get entries from native code
|
||||
AndroidProgressCallback progressCallback = new AndroidProgressCallback(mContext);
|
||||
AsyncTask.execute(() -> {
|
||||
AndroidHostInterface.getInstance().refreshGameList(invalidateCache, invalidateDatabase, progressCallback);
|
||||
GameListEntry[] newEntries = AndroidHostInterface.getInstance().getGameListEntries();
|
||||
Arrays.sort(newEntries, new GameListEntryComparator());
|
||||
|
||||
mContext.runOnUiThread(() -> {
|
||||
try {
|
||||
progressCallback.dismiss();
|
||||
} catch (Exception e) {
|
||||
Log.e("GameList", "Exception dismissing refresh progress");
|
||||
e.printStackTrace();
|
||||
}
|
||||
mEntries = newEntries;
|
||||
mAdapter.notifyDataSetChanged();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public int getEntryCount() {
|
||||
return mEntries.length;
|
||||
}
|
||||
|
||||
public GameListEntry getEntry(int index) {
|
||||
return mEntries[index];
|
||||
}
|
||||
|
||||
private class ListViewAdapter extends BaseAdapter {
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mEntries.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return mEntries[position];
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getViewTypeCount() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(mContext)
|
||||
.inflate(R.layout.game_list_view_entry, parent, false);
|
||||
}
|
||||
|
||||
mEntries[position].fillView(convertView);
|
||||
return convertView;
|
||||
}
|
||||
}
|
||||
|
||||
public BaseAdapter getListViewAdapter() {
|
||||
return mAdapter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
public class GameListEntry {
|
||||
public enum EntryType {
|
||||
Disc,
|
||||
PSExe,
|
||||
Playlist,
|
||||
PSF
|
||||
}
|
||||
|
||||
public enum CompatibilityRating {
|
||||
Unknown,
|
||||
DoesntBoot,
|
||||
CrashesInIntro,
|
||||
CrashesInGame,
|
||||
GraphicalAudioIssues,
|
||||
NoIssues,
|
||||
}
|
||||
|
||||
private String mPath;
|
||||
private String mCode;
|
||||
private String mTitle;
|
||||
private String mFileTitle;
|
||||
private long mSize;
|
||||
private String mModifiedTime;
|
||||
private DiscRegion mRegion;
|
||||
private EntryType mType;
|
||||
private CompatibilityRating mCompatibilityRating;
|
||||
private String mCoverPath;
|
||||
|
||||
public GameListEntry(String path, String code, String title, String fileTitle, long size, String modifiedTime, String region,
|
||||
String type, String compatibilityRating, String coverPath) {
|
||||
mPath = path;
|
||||
mCode = code;
|
||||
mTitle = title;
|
||||
mFileTitle = fileTitle;
|
||||
mSize = size;
|
||||
mModifiedTime = modifiedTime;
|
||||
mCoverPath = coverPath;
|
||||
|
||||
try {
|
||||
mRegion = DiscRegion.valueOf(region);
|
||||
} catch (IllegalArgumentException e) {
|
||||
mRegion = DiscRegion.NTSC_U;
|
||||
}
|
||||
|
||||
try {
|
||||
mType = EntryType.valueOf(type);
|
||||
} catch (IllegalArgumentException e) {
|
||||
mType = EntryType.Disc;
|
||||
}
|
||||
|
||||
try {
|
||||
mCompatibilityRating = CompatibilityRating.valueOf(compatibilityRating);
|
||||
} catch (IllegalArgumentException e) {
|
||||
mCompatibilityRating = CompatibilityRating.Unknown;
|
||||
}
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return mPath;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return mCode;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return mTitle;
|
||||
}
|
||||
|
||||
public String getFileTitle() {
|
||||
return mFileTitle;
|
||||
}
|
||||
|
||||
public String getModifiedTime() {
|
||||
return mModifiedTime;
|
||||
}
|
||||
|
||||
public DiscRegion getRegion() {
|
||||
return mRegion;
|
||||
}
|
||||
|
||||
public EntryType getType() {
|
||||
return mType;
|
||||
}
|
||||
|
||||
public CompatibilityRating getCompatibilityRating() {
|
||||
return mCompatibilityRating;
|
||||
}
|
||||
|
||||
public static String getFileNameForPath(String path) {
|
||||
int lastSlash = path.lastIndexOf('/');
|
||||
if (lastSlash > 0 && lastSlash < path.length() - 1)
|
||||
return path.substring(lastSlash + 1);
|
||||
else
|
||||
return path;
|
||||
}
|
||||
|
||||
private String getSubTitle() {
|
||||
String fileName = getFileNameForPath(mPath);
|
||||
String sizeString = String.format("%.2f MB", (double) mSize / 1048576.0);
|
||||
return String.format("%s (%s)", fileName, sizeString);
|
||||
}
|
||||
|
||||
public void fillView(View view) {
|
||||
((TextView) view.findViewById(R.id.game_list_view_entry_title)).setText(mTitle);
|
||||
((TextView) view.findViewById(R.id.game_list_view_entry_subtitle)).setText(getSubTitle());
|
||||
|
||||
int regionDrawableId;
|
||||
switch (mRegion) {
|
||||
case NTSC_J:
|
||||
regionDrawableId = R.drawable.flag_jp;
|
||||
break;
|
||||
case PAL:
|
||||
regionDrawableId = R.drawable.flag_eu;
|
||||
break;
|
||||
case Other:
|
||||
regionDrawableId = R.drawable.ic_baseline_help_24;
|
||||
break;
|
||||
case NTSC_U:
|
||||
default:
|
||||
regionDrawableId = R.drawable.flag_us;
|
||||
break;
|
||||
}
|
||||
|
||||
((ImageView) view.findViewById(R.id.game_list_view_entry_region_icon))
|
||||
.setImageDrawable(ContextCompat.getDrawable(view.getContext(), regionDrawableId));
|
||||
|
||||
int typeDrawableId;
|
||||
switch (mType) {
|
||||
case PSExe:
|
||||
typeDrawableId = R.drawable.ic_emblem_system;
|
||||
break;
|
||||
|
||||
case Playlist:
|
||||
typeDrawableId = R.drawable.ic_baseline_playlist_play_24;
|
||||
break;
|
||||
|
||||
case PSF:
|
||||
typeDrawableId = R.drawable.ic_baseline_library_music_24;
|
||||
break;
|
||||
|
||||
case Disc:
|
||||
default:
|
||||
typeDrawableId = R.drawable.ic_media_cdrom;
|
||||
break;
|
||||
}
|
||||
|
||||
ImageView icon = ((ImageView) view.findViewById(R.id.game_list_view_entry_type_icon));
|
||||
icon.setImageDrawable(ContextCompat.getDrawable(view.getContext(), typeDrawableId));
|
||||
|
||||
if (mCoverPath != null) {
|
||||
new ImageLoadTask(icon).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mCoverPath);
|
||||
}
|
||||
|
||||
int compatibilityDrawableId;
|
||||
switch (mCompatibilityRating) {
|
||||
case DoesntBoot:
|
||||
compatibilityDrawableId = R.drawable.ic_star_1;
|
||||
break;
|
||||
case CrashesInIntro:
|
||||
compatibilityDrawableId = R.drawable.ic_star_2;
|
||||
break;
|
||||
case CrashesInGame:
|
||||
compatibilityDrawableId = R.drawable.ic_star_3;
|
||||
break;
|
||||
case GraphicalAudioIssues:
|
||||
compatibilityDrawableId = R.drawable.ic_star_4;
|
||||
break;
|
||||
case NoIssues:
|
||||
compatibilityDrawableId = R.drawable.ic_star_5;
|
||||
break;
|
||||
case Unknown:
|
||||
default:
|
||||
compatibilityDrawableId = R.drawable.ic_star_0;
|
||||
break;
|
||||
}
|
||||
|
||||
((ImageView) view.findViewById(R.id.game_list_view_compatibility_icon))
|
||||
.setImageDrawable(ContextCompat.getDrawable(view.getContext(), compatibilityDrawableId));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Bundle;
|
||||
import android.util.Property;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.ListFragment;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class GamePropertiesActivity extends AppCompatActivity {
|
||||
PropertyListAdapter mPropertiesListAdapter;
|
||||
GameListEntry mGameListEntry;
|
||||
|
||||
public ListAdapter getPropertyListAdapter() {
|
||||
if (mPropertiesListAdapter != null)
|
||||
return mPropertiesListAdapter;
|
||||
|
||||
mPropertiesListAdapter = new PropertyListAdapter(this);
|
||||
mPropertiesListAdapter.addItem("title", "Title", mGameListEntry.getTitle());
|
||||
mPropertiesListAdapter.addItem("filetitle", "File Title", mGameListEntry.getFileTitle());
|
||||
mPropertiesListAdapter.addItem("serial", "Serial", mGameListEntry.getCode());
|
||||
mPropertiesListAdapter.addItem("type", "Type", mGameListEntry.getType().toString());
|
||||
mPropertiesListAdapter.addItem("path", "Path", mGameListEntry.getPath());
|
||||
mPropertiesListAdapter.addItem("region", "Region", mGameListEntry.getRegion().toString());
|
||||
mPropertiesListAdapter.addItem("compatibility", "Compatibility Rating", mGameListEntry.getCompatibilityRating().toString());
|
||||
return mPropertiesListAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
String path = getIntent().getStringExtra("path");
|
||||
if (path == null || path.isEmpty()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
mGameListEntry = AndroidHostInterface.getInstance().getGameListEntry(path);
|
||||
if (mGameListEntry == null) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
setContentView(R.layout.settings_activity);
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, new SettingsCollectionFragment(this))
|
||||
.commit();
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
setTitle(mGameListEntry.getTitle());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void displayError(String text) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.emulation_activity_error)
|
||||
.setMessage(text)
|
||||
.setNegativeButton(R.string.main_activity_ok, ((dialog, which) -> dialog.dismiss()))
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void createBooleanGameSetting(PreferenceScreen ps, String key, int titleId) {
|
||||
GameSettingPreference pref = new GameSettingPreference(ps.getContext(), mGameListEntry.getPath(), key, titleId);
|
||||
ps.addPreference(pref);
|
||||
}
|
||||
|
||||
private void createListGameSetting(PreferenceScreen ps, String key, int titleId, int entryId, int entryValuesId) {
|
||||
GameSettingPreference pref = new GameSettingPreference(ps.getContext(), mGameListEntry.getPath(), key, titleId, entryId, entryValuesId);
|
||||
ps.addPreference(pref);
|
||||
}
|
||||
|
||||
public static class GameSettingsFragment extends PreferenceFragmentCompat {
|
||||
private GamePropertiesActivity activity;
|
||||
|
||||
public GameSettingsFragment(GamePropertiesActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
|
||||
activity.createListGameSetting(ps, "CPUOverclock", R.string.settings_cpu_overclocking, R.array.settings_advanced_cpu_overclock_entries, R.array.settings_advanced_cpu_overclock_values);
|
||||
activity.createListGameSetting(ps, "CDROMReadSpeedup", R.string.settings_cdrom_read_speedup, R.array.settings_cdrom_read_speedup_entries, R.array.settings_cdrom_read_speedup_values);
|
||||
|
||||
activity.createListGameSetting(ps, "DisplayAspectRatio", R.string.settings_aspect_ratio, R.array.settings_display_aspect_ratio_names, R.array.settings_display_aspect_ratio_values);
|
||||
activity.createListGameSetting(ps, "DisplayCropMode", R.string.settings_crop_mode, R.array.settings_display_crop_mode_entries, R.array.settings_display_crop_mode_values);
|
||||
activity.createListGameSetting(ps, "GPUDownsampleMode", R.string.settings_downsample_mode, R.array.settings_downsample_mode_entries, R.array.settings_downsample_mode_values);
|
||||
activity.createBooleanGameSetting(ps, "DisplayLinearUpscaling", R.string.settings_linear_upscaling);
|
||||
activity.createBooleanGameSetting(ps, "DisplayIntegerUpscaling", R.string.settings_integer_upscaling);
|
||||
activity.createBooleanGameSetting(ps, "DisplayForce4_3For24Bit", R.string.settings_force_4_3_for_24bit);
|
||||
|
||||
activity.createListGameSetting(ps, "GPUResolutionScale", R.string.settings_gpu_resolution_scale, R.array.settings_gpu_resolution_scale_entries, R.array.settings_gpu_resolution_scale_values);
|
||||
activity.createListGameSetting(ps, "GPUMSAA", R.string.settings_msaa, R.array.settings_gpu_msaa_entries, R.array.settings_gpu_msaa_values);
|
||||
activity.createBooleanGameSetting(ps, "GPUTrueColor", R.string.settings_true_color);
|
||||
activity.createBooleanGameSetting(ps, "GPUScaledDithering", R.string.settings_scaled_dithering);
|
||||
activity.createListGameSetting(ps, "GPUTextureFilter", R.string.settings_texture_filtering, R.array.settings_gpu_texture_filter_names, R.array.settings_gpu_texture_filter_values);
|
||||
activity.createBooleanGameSetting(ps, "GPUForceNTSCTimings", R.string.settings_force_ntsc_timings);
|
||||
activity.createBooleanGameSetting(ps, "GPUWidescreenHack", R.string.settings_widescreen_hack);
|
||||
activity.createBooleanGameSetting(ps, "GPUPGXP", R.string.settings_pgxp_geometry_correction);
|
||||
activity.createBooleanGameSetting(ps, "GPUPGXPDepthBuffer", R.string.settings_pgxp_depth_buffer);
|
||||
|
||||
setPreferenceScreen(ps);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ControllerSettingsFragment extends PreferenceFragmentCompat {
|
||||
private GamePropertiesActivity activity;
|
||||
|
||||
public ControllerSettingsFragment(GamePropertiesActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
private void createInputProfileSetting(PreferenceScreen ps) {
|
||||
final GameSettingPreference pref = new GameSettingPreference(ps.getContext(), activity.mGameListEntry.getPath(), "InputProfileName", R.string.settings_input_profile);
|
||||
|
||||
final String[] inputProfileNames = AndroidHostInterface.getInstance().getInputProfileNames();
|
||||
pref.setEntries(inputProfileNames);
|
||||
pref.setEntryValues(inputProfileNames);
|
||||
ps.addPreference(pref);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
final PreferenceScreen ps = getPreferenceManager().createPreferenceScreen(getContext());
|
||||
|
||||
activity.createListGameSetting(ps, "Controller1Type", R.string.settings_controller_type, R.array.settings_controller_type_entries, R.array.settings_controller_type_values);
|
||||
activity.createListGameSetting(ps, "MemoryCard1Type", R.string.settings_memory_card_1_type, R.array.settings_memory_card_mode_entries, R.array.settings_memory_card_mode_values);
|
||||
activity.createListGameSetting(ps, "MemoryCard2Type", R.string.settings_memory_card_2_type, R.array.settings_memory_card_mode_entries, R.array.settings_memory_card_mode_values);
|
||||
createInputProfileSetting(ps);
|
||||
|
||||
setPreferenceScreen(ps);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SettingsCollectionFragment extends Fragment {
|
||||
private GamePropertiesActivity activity;
|
||||
private SettingsCollectionAdapter adapter;
|
||||
private ViewPager2 viewPager;
|
||||
|
||||
public SettingsCollectionFragment(GamePropertiesActivity activity) {
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_controller_mapping, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
adapter = new SettingsCollectionAdapter(activity, this);
|
||||
viewPager = view.findViewById(R.id.view_pager);
|
||||
viewPager.setAdapter(adapter);
|
||||
|
||||
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
|
||||
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
|
||||
switch (position) {
|
||||
case 0:
|
||||
tab.setText(R.string.game_properties_tab_summary);
|
||||
break;
|
||||
case 1:
|
||||
tab.setText(R.string.game_properties_tab_game_settings);
|
||||
break;
|
||||
case 2:
|
||||
tab.setText(R.string.game_properties_tab_controller_settings);
|
||||
break;
|
||||
}
|
||||
}).attach();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SettingsCollectionAdapter extends FragmentStateAdapter {
|
||||
private GamePropertiesActivity activity;
|
||||
|
||||
public SettingsCollectionAdapter(@NonNull GamePropertiesActivity activity, @NonNull Fragment fragment) {
|
||||
super(fragment);
|
||||
this.activity = activity;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
switch (position) {
|
||||
case 0: { // Summary
|
||||
ListFragment lf = new ListFragment();
|
||||
lf.setListAdapter(activity.getPropertyListAdapter());
|
||||
return lf;
|
||||
}
|
||||
|
||||
case 1: { // Game Settings
|
||||
return new GameSettingsFragment(activity);
|
||||
}
|
||||
|
||||
case 2: { // Controller Settings
|
||||
return new ControllerSettingsFragment(activity);
|
||||
}
|
||||
|
||||
// TODO: Memory Card Editor
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.preference.ListPreference;
|
||||
|
||||
public class GameSettingPreference extends ListPreference {
|
||||
private String mGamePath;
|
||||
|
||||
/**
|
||||
* Creates a boolean game property preference.
|
||||
*/
|
||||
public GameSettingPreference(Context context, String gamePath, String settingKey, int titleId) {
|
||||
super(context);
|
||||
mGamePath = gamePath;
|
||||
setPersistent(false);
|
||||
setTitle(titleId);
|
||||
setKey(settingKey);
|
||||
setIconSpaceReserved(false);
|
||||
setSummaryProvider(SimpleSummaryProvider.getInstance());
|
||||
|
||||
setEntries(R.array.settings_boolean_entries);
|
||||
setEntryValues(R.array.settings_boolean_values);
|
||||
|
||||
updateValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a list game property preference.
|
||||
*/
|
||||
public GameSettingPreference(Context context, String gamePath, String settingKey, int titleId, int entryArray, int entryValuesArray) {
|
||||
super(context);
|
||||
mGamePath = gamePath;
|
||||
setPersistent(false);
|
||||
setTitle(titleId);
|
||||
setKey(settingKey);
|
||||
setIconSpaceReserved(false);
|
||||
setSummaryProvider(SimpleSummaryProvider.getInstance());
|
||||
|
||||
setEntries(entryArray);
|
||||
setEntryValues(entryValuesArray);
|
||||
|
||||
updateValue();
|
||||
}
|
||||
|
||||
private void updateValue() {
|
||||
final String value = AndroidHostInterface.getInstance().getGameSettingValue(mGamePath, getKey());
|
||||
if (value == null)
|
||||
super.setValue("null");
|
||||
else
|
||||
super.setValue(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(String value) {
|
||||
super.setValue(value);
|
||||
if (value.equals("null"))
|
||||
AndroidHostInterface.getInstance().setGameSettingValue(mGamePath, getKey(), null);
|
||||
else
|
||||
AndroidHostInterface.getInstance().setGameSettingValue(mGamePath, getKey(), value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEntries(CharSequence[] entries) {
|
||||
final int length = (entries != null) ? entries.length : 0;
|
||||
CharSequence[] newEntries = new CharSequence[length + 1];
|
||||
newEntries[0] = getContext().getString(R.string.game_properties_preference_use_global_setting);
|
||||
if (entries != null)
|
||||
System.arraycopy(entries, 0, newEntries, 1, entries.length);
|
||||
super.setEntries(newEntries);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEntryValues(CharSequence[] entryValues) {
|
||||
final int length = (entryValues != null) ? entryValues.length : 0;
|
||||
CharSequence[] newEntryValues = new CharSequence[length + 1];
|
||||
newEntryValues[0] = "null";
|
||||
if (entryValues != null)
|
||||
System.arraycopy(entryValues, 0, newEntryValues, 1, length);
|
||||
super.setEntryValues(newEntryValues);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
public class HotkeyInfo {
|
||||
private String mCategory;
|
||||
private String mName;
|
||||
private String mDisplayName;
|
||||
|
||||
public HotkeyInfo(String category, String name, String displayName) {
|
||||
mCategory = category;
|
||||
mName = name;
|
||||
mDisplayName = displayName;
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return mCategory;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return mName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return mDisplayName;
|
||||
}
|
||||
|
||||
public String getBindingConfigKey() {
|
||||
return String.format("Hotkeys/%s", mName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.os.AsyncTask;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class ImageLoadTask extends AsyncTask<String, Void, Bitmap> {
|
||||
private WeakReference<ImageView> mView;
|
||||
|
||||
public ImageLoadTask(ImageView view) {
|
||||
mView = new WeakReference<>(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap doInBackground(String... strings) {
|
||||
try {
|
||||
return BitmapFactory.decodeFile(strings[0]);
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(Bitmap bitmap) {
|
||||
ImageView iv = mView.get();
|
||||
if (iv != null)
|
||||
iv.setImageBitmap(bitmap);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
private static final int REQUEST_EXTERNAL_STORAGE_PERMISSIONS = 1;
|
||||
private static final int REQUEST_ADD_DIRECTORY_TO_GAME_LIST = 2;
|
||||
private static final int REQUEST_IMPORT_BIOS_IMAGE = 3;
|
||||
private static final int REQUEST_START_FILE = 4;
|
||||
private static final int REQUEST_SETTINGS = 5;
|
||||
private static final int REQUEST_EDIT_GAME_DIRECTORIES = 6;
|
||||
|
||||
private GameList mGameList;
|
||||
private ListView mGameListView;
|
||||
private boolean mHasExternalStoragePermissions = false;
|
||||
|
||||
private void setLanguage() {
|
||||
String language = PreferenceManager.getDefaultSharedPreferences(this).getString("Main/Language", "none");
|
||||
if (language == null || language.equals("none")) {
|
||||
return;
|
||||
}
|
||||
|
||||
String[] parts = language.split("-");
|
||||
if (parts.length < 2)
|
||||
return;
|
||||
|
||||
Locale locale = new Locale(parts[0], parts[1]);
|
||||
Locale.setDefault(locale);
|
||||
|
||||
Resources res = getResources();
|
||||
Configuration config = res.getConfiguration();
|
||||
config.setLocale(locale);
|
||||
res.updateConfiguration(config, res.getDisplayMetrics());
|
||||
}
|
||||
|
||||
private void setTheme() {
|
||||
String theme = PreferenceManager.getDefaultSharedPreferences(this).getString("Main/Theme", "follow_system");
|
||||
if (theme == null)
|
||||
return;
|
||||
|
||||
if (theme.equals("follow_system")) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM);
|
||||
} else if (theme.equals("light")) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
|
||||
} else if (theme.equals("dark")) {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadSettings() {
|
||||
setLanguage();
|
||||
setTheme();
|
||||
}
|
||||
|
||||
private boolean shouldResumeStateByDefault() {
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
return prefs.getBoolean("Main/SaveStateOnExit", true);
|
||||
}
|
||||
|
||||
private static String getTitleString() {
|
||||
String scmVersion = AndroidHostInterface.getScmVersion();
|
||||
final int gitHashPos = scmVersion.indexOf("-g");
|
||||
if (gitHashPos > 0)
|
||||
scmVersion = scmVersion.substring(0, gitHashPos);
|
||||
|
||||
return String.format("DuckStation %s", scmVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
loadSettings();
|
||||
|
||||
setContentView(R.layout.activity_main);
|
||||
Toolbar toolbar = findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setTitle(getTitleString());
|
||||
|
||||
findViewById(R.id.fab_add_game_directory).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
startAddGameDirectory();
|
||||
}
|
||||
});
|
||||
findViewById(R.id.fab_resume).setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
startEmulation(null, shouldResumeStateByDefault());
|
||||
}
|
||||
});
|
||||
|
||||
// Set up game list view.
|
||||
mGameList = new GameList(this);
|
||||
mGameListView = findViewById(R.id.game_list_view);
|
||||
mGameListView.setAdapter(mGameList.getListViewAdapter());
|
||||
mGameListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
startEmulation(mGameList.getEntry(position).getPath(), shouldResumeStateByDefault());
|
||||
}
|
||||
});
|
||||
mGameListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
|
||||
@Override
|
||||
public boolean onItemLongClick(AdapterView<?> parent, View view, int position,
|
||||
long id) {
|
||||
PopupMenu menu = new PopupMenu(MainActivity.this, view,
|
||||
Gravity.RIGHT | Gravity.TOP);
|
||||
menu.getMenuInflater().inflate(R.menu.menu_game_list_entry, menu.getMenu());
|
||||
menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
|
||||
@Override
|
||||
public boolean onMenuItemClick(MenuItem item) {
|
||||
int id = item.getItemId();
|
||||
if (id == R.id.game_list_entry_menu_start_game) {
|
||||
startEmulation(mGameList.getEntry(position).getPath(), false);
|
||||
return true;
|
||||
} else if (id == R.id.game_list_entry_menu_resume_game) {
|
||||
startEmulation(mGameList.getEntry(position).getPath(), true);
|
||||
return true;
|
||||
} else if (id == R.id.game_list_entry_menu_properties) {
|
||||
openGameProperties(mGameList.getEntry(position).getPath());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
menu.show();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
mHasExternalStoragePermissions = checkForExternalStoragePermissions();
|
||||
if (mHasExternalStoragePermissions)
|
||||
completeStartup();
|
||||
}
|
||||
|
||||
private void completeStartup() {
|
||||
if (!AndroidHostInterface.hasInstance() && !AndroidHostInterface.createInstance(this)) {
|
||||
Log.i("MainActivity", "Failed to create host interface");
|
||||
throw new RuntimeException("Failed to create host interface");
|
||||
}
|
||||
|
||||
AndroidHostInterface.getInstance().setContext(this);
|
||||
mGameList.refresh(false, false, this);
|
||||
}
|
||||
|
||||
private void startAddGameDirectory() {
|
||||
if (!checkForExternalStoragePermissions())
|
||||
return;
|
||||
|
||||
Intent i = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
i.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
i.putExtra(Intent.EXTRA_LOCAL_ONLY, true);
|
||||
startActivityForResult(Intent.createChooser(i, getString(R.string.main_activity_choose_directory)),
|
||||
REQUEST_ADD_DIRECTORY_TO_GAME_LIST);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
getMenuInflater().inflate(R.menu.menu_main, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
// Handle action bar item clicks here. The action bar will
|
||||
// automatically handle clicks on the Home/Up button, so long
|
||||
// as you specify a parent activity in AndroidManifest.xml.
|
||||
int id = item.getItemId();
|
||||
|
||||
//noinspection SimplifiableIfStatement
|
||||
if (id == R.id.action_resume) {
|
||||
startEmulation(null, true);
|
||||
} else if (id == R.id.action_start_bios) {
|
||||
startEmulation(null, false);
|
||||
} else if (id == R.id.action_start_file) {
|
||||
startStartFile();
|
||||
} else if (id == R.id.action_edit_game_directories) {
|
||||
Intent intent = new Intent(this, GameDirectoriesActivity.class);
|
||||
startActivityForResult(intent, REQUEST_EDIT_GAME_DIRECTORIES);
|
||||
return true;
|
||||
} else if (id == R.id.action_scan_for_new_games) {
|
||||
mGameList.refresh(false, false, this);
|
||||
} else if (id == R.id.action_rescan_all_games) {
|
||||
mGameList.refresh(true, true, this);
|
||||
} else if (id == R.id.action_import_bios) {
|
||||
importBIOSImage();
|
||||
} else if (id == R.id.action_settings) {
|
||||
Intent intent = new Intent(this, SettingsActivity.class);
|
||||
startActivityForResult(intent, REQUEST_SETTINGS);
|
||||
return true;
|
||||
} else if (id == R.id.action_controller_mapping) {
|
||||
Intent intent = new Intent(this, ControllerMappingActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
} else if (id == R.id.action_show_version) {
|
||||
showVersion();
|
||||
return true;
|
||||
} else if (id == R.id.action_github_respository) {
|
||||
openGithubRepository();
|
||||
return true;
|
||||
} else if (id == R.id.action_discord_server) {
|
||||
openDiscordServer();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
switch (requestCode) {
|
||||
case REQUEST_ADD_DIRECTORY_TO_GAME_LIST: {
|
||||
if (resultCode != RESULT_OK)
|
||||
return;
|
||||
|
||||
String path = GameDirectoriesActivity.getPathFromTreeUri(this, data.getData());
|
||||
if (path == null)
|
||||
return;
|
||||
|
||||
GameDirectoriesActivity.addSearchDirectory(this, path, true);
|
||||
mGameList.refresh(false, false, this);
|
||||
}
|
||||
break;
|
||||
|
||||
case REQUEST_IMPORT_BIOS_IMAGE: {
|
||||
if (resultCode != RESULT_OK)
|
||||
return;
|
||||
|
||||
onImportBIOSImageResult(data.getData());
|
||||
}
|
||||
break;
|
||||
|
||||
case REQUEST_START_FILE: {
|
||||
if (resultCode != RESULT_OK)
|
||||
return;
|
||||
|
||||
String path = GameDirectoriesActivity.getPathFromUri(this, data.getData());
|
||||
if (path == null)
|
||||
return;
|
||||
|
||||
startEmulation(path, shouldResumeStateByDefault());
|
||||
}
|
||||
break;
|
||||
|
||||
case REQUEST_SETTINGS: {
|
||||
loadSettings();
|
||||
}
|
||||
break;
|
||||
|
||||
case REQUEST_EDIT_GAME_DIRECTORIES: {
|
||||
mGameList.refresh(false, false, this);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkForExternalStoragePermissions() {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) ==
|
||||
PackageManager.PERMISSION_GRANTED &&
|
||||
ContextCompat
|
||||
.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) ==
|
||||
PackageManager.PERMISSION_GRANTED) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ActivityCompat.requestPermissions(this,
|
||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||
REQUEST_EXTERNAL_STORAGE_PERMISSIONS);
|
||||
return false;
|
||||
}
|
||||
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions,
|
||||
int[] grantResults) {
|
||||
// check that all were successful
|
||||
for (int i = 0; i < grantResults.length; i++) {
|
||||
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
|
||||
if (!mHasExternalStoragePermissions) {
|
||||
mHasExternalStoragePermissions = true;
|
||||
completeStartup();
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this,
|
||||
R.string.main_activity_external_storage_permissions_error,
|
||||
Toast.LENGTH_LONG);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean openGameProperties(String path) {
|
||||
Intent intent = new Intent(this, GamePropertiesActivity.class);
|
||||
intent.putExtra("path", path);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean startEmulation(String bootPath, boolean resumeState) {
|
||||
if (!doBIOSCheck())
|
||||
return false;
|
||||
|
||||
Intent intent = new Intent(this, EmulationActivity.class);
|
||||
intent.putExtra("bootPath", bootPath);
|
||||
intent.putExtra("resumeState", resumeState);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void startStartFile() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
startActivityForResult(Intent.createChooser(intent, getString(R.string.main_activity_choose_disc_image)), REQUEST_START_FILE);
|
||||
}
|
||||
|
||||
private boolean doBIOSCheck() {
|
||||
if (AndroidHostInterface.getInstance().hasAnyBIOSImages())
|
||||
return true;
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.main_activity_missing_bios_image)
|
||||
.setMessage(R.string.main_activity_missing_bios_image_prompt)
|
||||
.setPositiveButton(R.string.main_activity_yes, (dialog, button) -> importBIOSImage())
|
||||
.setNegativeButton(R.string.main_activity_no, (dialog, button) -> {
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void importBIOSImage() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("*/*");
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
startActivityForResult(Intent.createChooser(intent, getString(R.string.main_activity_choose_bios_image)), REQUEST_IMPORT_BIOS_IMAGE);
|
||||
}
|
||||
|
||||
private void onImportBIOSImageResult(Uri uri) {
|
||||
// This should really be 512K but just in case we wanted to support the other BIOSes in the future...
|
||||
final int MAX_BIOS_SIZE = 2 * 1024 * 1024;
|
||||
|
||||
InputStream stream = null;
|
||||
try {
|
||||
stream = getContentResolver().openInputStream(uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
Toast.makeText(this, R.string.main_activity_failed_to_open_bios_image, Toast.LENGTH_LONG);
|
||||
return;
|
||||
}
|
||||
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
try {
|
||||
byte[] buffer = new byte[512 * 1024];
|
||||
int len;
|
||||
while ((len = stream.read(buffer)) > 0) {
|
||||
os.write(buffer, 0, len);
|
||||
if (os.size() > MAX_BIOS_SIZE) {
|
||||
throw new IOException(getString(R.string.main_activity_bios_image_too_large));
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(getString(R.string.main_activity_failed_to_read_bios_image_prefix) + e.getMessage())
|
||||
.setPositiveButton(R.string.main_activity_ok, (dialog, button) -> {
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
String importResult = AndroidHostInterface.getInstance().importBIOSImage(os.toByteArray());
|
||||
String message = (importResult == null) ? getString(R.string.main_activity_invalid_error) : ("BIOS '" + importResult + "' imported.");
|
||||
|
||||
new AlertDialog.Builder(this)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.main_activity_ok, (dialog, button) -> {
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showVersion() {
|
||||
final String message = AndroidHostInterface.getFullScmVersion();
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(R.string.main_activity_show_version_title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.main_activity_ok, (dialog, button) -> {
|
||||
})
|
||||
.setNeutralButton(R.string.main_activity_copy, (dialog, button) -> {
|
||||
ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
|
||||
if (clipboard != null)
|
||||
clipboard.setPrimaryClip(ClipData.newPlainText(getString(R.string.main_activity_show_version_title), message));
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private void openGithubRepository() {
|
||||
final String url = "https://github.com/stenzek/duckstation";
|
||||
final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
startActivity(browserIntent);
|
||||
}
|
||||
|
||||
private void openDiscordServer() {
|
||||
final String url = "https://discord.gg/Buktv3t";
|
||||
final Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
startActivity(browserIntent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
public class PatchCode {
|
||||
private int mIndex;
|
||||
private String mDescription;
|
||||
private boolean mEnabled;
|
||||
|
||||
public PatchCode(int index, String description, boolean enabled) {
|
||||
mIndex = index;
|
||||
mDescription = description;
|
||||
mEnabled = enabled;
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return mIndex;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return mDescription;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return mEnabled;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.SharedPreferences;
|
||||
import android.util.ArraySet;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public class PreferenceHelpers {
|
||||
/**
|
||||
* Clears all preferences in the specified section (starting with sectionName/).
|
||||
* We really don't want to have to do this with JNI...
|
||||
*
|
||||
* @param prefs Preferences object.
|
||||
* @param sectionName Section to clear keys for.
|
||||
*/
|
||||
public static void clearSection(SharedPreferences prefs, String sectionName) {
|
||||
String testSectionName = sectionName + "/";
|
||||
SharedPreferences.Editor editor = prefs.edit();
|
||||
for (String keyName : prefs.getAll().keySet()) {
|
||||
if (keyName.startsWith(testSectionName)) {
|
||||
editor.remove(keyName);
|
||||
}
|
||||
}
|
||||
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
public static Set<String> getStringSet(SharedPreferences prefs, String keyName) {
|
||||
Set<String> values = null;
|
||||
try {
|
||||
values = prefs.getStringSet(keyName, null);
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
String singleValue = prefs.getString(keyName, null);
|
||||
if (singleValue != null) {
|
||||
values = new ArraySet<>();
|
||||
values.add(singleValue);
|
||||
}
|
||||
} catch (Exception e2) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
public static boolean addToStringList(SharedPreferences prefs, String keyName, String valueToAdd) {
|
||||
Set<String> values = getStringSet(prefs, keyName);
|
||||
if (values == null) {
|
||||
values = new ArraySet<>();
|
||||
} else {
|
||||
// We need to copy it otherwise the put doesn't save.
|
||||
Set<String> valuesCopy = new ArraySet<>();
|
||||
valuesCopy.addAll(values);
|
||||
values = valuesCopy;
|
||||
}
|
||||
|
||||
final boolean result = values.add(valueToAdd);
|
||||
prefs.edit().putStringSet(keyName, values).commit();
|
||||
return result;
|
||||
}
|
||||
|
||||
public static boolean removeFromStringList(SharedPreferences prefs, String keyName, String valueToRemove) {
|
||||
Set<String> values = getStringSet(prefs, keyName);
|
||||
if (values == null)
|
||||
return false;
|
||||
|
||||
// We need to copy it otherwise the put doesn't save.
|
||||
Set<String> valuesCopy = new ArraySet<>();
|
||||
valuesCopy.addAll(values);
|
||||
values = valuesCopy;
|
||||
|
||||
final boolean result = values.remove(valueToRemove);
|
||||
prefs.edit().putStringSet(keyName, values).commit();
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void setStringList(SharedPreferences prefs, String keyName, String[] values) {
|
||||
Set<String> valueSet = new ArraySet<String>();
|
||||
for (String value : values)
|
||||
valueSet.add(value);
|
||||
|
||||
prefs.edit().putStringSet(keyName, valueSet).commit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
public class PropertyListAdapter extends BaseAdapter {
|
||||
private class Item {
|
||||
public String key;
|
||||
public String title;
|
||||
public String value;
|
||||
|
||||
Item(String key, String title, String value) {
|
||||
this.key = key;
|
||||
this.title = title;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private Context mContext;
|
||||
private ArrayList<Item> mItems = new ArrayList<>();
|
||||
|
||||
public PropertyListAdapter(Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public Item getItemByKey(String key) {
|
||||
for (Item it : mItems) {
|
||||
if (it.key.equals(key))
|
||||
return it;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public int addItem(String key, String title, String value) {
|
||||
if (getItemByKey(key) != null)
|
||||
return -1;
|
||||
|
||||
Item it = new Item(key, title, value);
|
||||
int position = mItems.size();
|
||||
mItems.add(it);
|
||||
return position;
|
||||
}
|
||||
|
||||
public boolean removeItem(Item item) {
|
||||
return mItems.remove(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mItems.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return mItems.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(mContext)
|
||||
.inflate(R.layout.layout_game_property_entry, parent, false);
|
||||
}
|
||||
|
||||
TextView titleView = (TextView) convertView.findViewById(R.id.property_title);
|
||||
TextView valueView = (TextView) convertView.findViewById(R.id.property_value);
|
||||
Item prop = mItems.get(position);
|
||||
titleView.setText(prop.title);
|
||||
valueView.setText(prop.value);
|
||||
return convertView;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class SaveStateInfo {
|
||||
private String mPath;
|
||||
private String mGameTitle;
|
||||
private String mGameCode;
|
||||
private String mMediaPath;
|
||||
private String mTimestamp;
|
||||
private int mSlot;
|
||||
private boolean mGlobal;
|
||||
private Bitmap mScreenshot;
|
||||
|
||||
public SaveStateInfo(String path, String gameTitle, String gameCode, String mediaPath, String timestamp, int slot, boolean global,
|
||||
int screenshotWidth, int screenshotHeight, byte[] screenshotData) {
|
||||
mPath = path;
|
||||
mGameTitle = gameTitle;
|
||||
mGameCode = gameCode;
|
||||
mMediaPath = mediaPath;
|
||||
mTimestamp = timestamp;
|
||||
mSlot = slot;
|
||||
mGlobal = global;
|
||||
|
||||
if (screenshotData != null) {
|
||||
try {
|
||||
mScreenshot = Bitmap.createBitmap(screenshotWidth, screenshotHeight, Bitmap.Config.ARGB_8888);
|
||||
mScreenshot.copyPixelsFromBuffer(ByteBuffer.wrap(screenshotData));
|
||||
} catch (Exception e) {
|
||||
mScreenshot = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean exists() {
|
||||
return mPath != null;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return mPath;
|
||||
}
|
||||
|
||||
public String getGameTitle() {
|
||||
return mGameTitle;
|
||||
}
|
||||
|
||||
public String getGameCode() {
|
||||
return mGameCode;
|
||||
}
|
||||
|
||||
public String getMediaPath() {
|
||||
return mMediaPath;
|
||||
}
|
||||
|
||||
public String getTimestamp() {
|
||||
return mTimestamp;
|
||||
}
|
||||
|
||||
public int getSlot() {
|
||||
return mSlot;
|
||||
}
|
||||
|
||||
public boolean isGlobal() {
|
||||
return mGlobal;
|
||||
}
|
||||
|
||||
public Bitmap getScreenshot() {
|
||||
return mScreenshot;
|
||||
}
|
||||
|
||||
private void fillView(Context context, View view) {
|
||||
ImageView imageView = (ImageView) view.findViewById(R.id.image);
|
||||
TextView summaryView = (TextView) view.findViewById(R.id.summary);
|
||||
TextView gameView = (TextView) view.findViewById(R.id.game);
|
||||
TextView pathView = (TextView) view.findViewById(R.id.path);
|
||||
TextView timestampView = (TextView) view.findViewById(R.id.timestamp);
|
||||
|
||||
if (mScreenshot != null)
|
||||
imageView.setImageBitmap(mScreenshot);
|
||||
else
|
||||
imageView.setImageDrawable(context.getDrawable(R.drawable.ic_baseline_not_interested_60));
|
||||
|
||||
String summaryText;
|
||||
if (mGlobal)
|
||||
summaryView.setText(String.format(context.getString(R.string.save_state_info_global_save_n), mSlot));
|
||||
else if (mSlot == 0)
|
||||
summaryView.setText(R.string.save_state_info_quick_save);
|
||||
else
|
||||
summaryView.setText(String.format(context.getString(R.string.save_state_info_game_save_n), mSlot));
|
||||
|
||||
if (exists()) {
|
||||
gameView.setText(String.format("%s - %s", mGameCode, mGameTitle));
|
||||
|
||||
int lastSlashPosition = mMediaPath.lastIndexOf('/');
|
||||
if (lastSlashPosition >= 0)
|
||||
pathView.setText(mMediaPath.substring(lastSlashPosition + 1));
|
||||
else
|
||||
pathView.setText(mMediaPath);
|
||||
|
||||
timestampView.setText(mTimestamp);
|
||||
} else {
|
||||
gameView.setText(R.string.save_state_info_slot_is_empty);
|
||||
pathView.setText("");
|
||||
timestampView.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
public static class ListAdapter extends BaseAdapter {
|
||||
private final Context mContext;
|
||||
private final SaveStateInfo[] mInfos;
|
||||
|
||||
public ListAdapter(Context context, SaveStateInfo[] infos) {
|
||||
mContext = context;
|
||||
mInfos = infos;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mInfos.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(int position) {
|
||||
return mInfos[position];
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(mContext).inflate(R.layout.save_state_view_entry, parent, false);
|
||||
}
|
||||
|
||||
mInfos[position].fillView(mContext, convertView);
|
||||
return convertView;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import com.google.android.material.tabs.TabLayoutMediator;
|
||||
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.settings_activity);
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, new SettingsCollectionFragment())
|
||||
.commit();
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
public static class SettingsFragment extends PreferenceFragmentCompat {
|
||||
private int resourceId;
|
||||
|
||||
public SettingsFragment(int resourceId) {
|
||||
this.resourceId = resourceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
|
||||
setPreferencesFromResource(resourceId, rootKey);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SettingsCollectionFragment extends Fragment {
|
||||
private SettingsCollectionAdapter adapter;
|
||||
private ViewPager2 viewPager;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.fragment_settings_collection, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
adapter = new SettingsCollectionAdapter(this);
|
||||
viewPager = view.findViewById(R.id.view_pager);
|
||||
viewPager.setAdapter(adapter);
|
||||
|
||||
TabLayout tabLayout = view.findViewById(R.id.tab_layout);
|
||||
new TabLayoutMediator(tabLayout, viewPager,
|
||||
(tab, position) -> tab.setText(getResources().getStringArray(R.array.settings_tabs)[position])
|
||||
).attach();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SettingsCollectionAdapter extends FragmentStateAdapter {
|
||||
public SettingsCollectionAdapter(@NonNull Fragment fragment) {
|
||||
super(fragment);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Fragment createFragment(int position) {
|
||||
switch (position) {
|
||||
case 0: // General
|
||||
return new SettingsFragment(R.xml.general_preferences);
|
||||
|
||||
case 1: // Display
|
||||
return new SettingsFragment(R.xml.display_preferences);
|
||||
|
||||
case 2: // Audio
|
||||
return new SettingsFragment(R.xml.audio_preferences);
|
||||
|
||||
case 3: // Enhancements
|
||||
return new SettingsFragment(R.xml.enhancements_preferences);
|
||||
|
||||
case 4: // Controllers
|
||||
return new SettingsFragment(R.xml.controllers_preferences);
|
||||
|
||||
case 5: // Advanced
|
||||
return new SettingsFragment(R.xml.advanced_preferences);
|
||||
|
||||
default:
|
||||
return new Fragment();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
public final class TouchscreenControllerAxisView extends View {
|
||||
private Drawable mBaseDrawable;
|
||||
private Drawable mStickUnpressedDrawable;
|
||||
private Drawable mStickPressedDrawable;
|
||||
private boolean mPressed = false;
|
||||
private int mPointerId = 0;
|
||||
private float mXValue = 0.0f;
|
||||
private float mYValue = 0.0f;
|
||||
private int mDrawXPos = 0;
|
||||
private int mDrawYPos = 0;
|
||||
|
||||
private String mConfigName;
|
||||
private int mControllerIndex = -1;
|
||||
private int mXAxisCode = -1;
|
||||
private int mYAxisCode = -1;
|
||||
private int mLeftButtonCode = -1;
|
||||
private int mRightButtonCode = -1;
|
||||
private int mUpButtonCode = -1;
|
||||
private int mDownButtonCode = -1;
|
||||
|
||||
public TouchscreenControllerAxisView(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public TouchscreenControllerAxisView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public TouchscreenControllerAxisView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
mBaseDrawable = getContext().getDrawable(R.drawable.ic_controller_analog_base);
|
||||
mBaseDrawable.setCallback(this);
|
||||
mStickUnpressedDrawable = getContext().getDrawable(R.drawable.ic_controller_analog_stick_unpressed);
|
||||
mStickUnpressedDrawable.setCallback(this);
|
||||
mStickPressedDrawable = getContext().getDrawable(R.drawable.ic_controller_analog_stick_pressed);
|
||||
mStickPressedDrawable.setCallback(this);
|
||||
}
|
||||
|
||||
public String getConfigName() {
|
||||
return mConfigName;
|
||||
}
|
||||
|
||||
public void setConfigName(String configName) {
|
||||
mConfigName = configName;
|
||||
}
|
||||
|
||||
public void setControllerAxis(int controllerIndex, int xCode, int yCode) {
|
||||
mControllerIndex = controllerIndex;
|
||||
mXAxisCode = xCode;
|
||||
mYAxisCode = yCode;
|
||||
mLeftButtonCode = -1;
|
||||
mRightButtonCode = -1;
|
||||
mUpButtonCode = -1;
|
||||
mDownButtonCode = -1;
|
||||
}
|
||||
|
||||
public void setControllerButtons(int controllerIndex, int leftCode, int rightCode, int upCode, int downCode) {
|
||||
mControllerIndex = controllerIndex;
|
||||
mXAxisCode = -1;
|
||||
mYAxisCode = -1;
|
||||
mLeftButtonCode = leftCode;
|
||||
mRightButtonCode = rightCode;
|
||||
mUpButtonCode = upCode;
|
||||
mDownButtonCode = downCode;
|
||||
}
|
||||
|
||||
public void setUnpressed() {
|
||||
if (!mPressed && mXValue == 0.0f && mYValue == 0.0f)
|
||||
return;
|
||||
|
||||
mPressed = false;
|
||||
mXValue = 0.0f;
|
||||
mYValue = 0.0f;
|
||||
mDrawXPos = 0;
|
||||
mDrawYPos = 0;
|
||||
invalidate();
|
||||
updateControllerState();
|
||||
}
|
||||
|
||||
public void setPressed(int pointerId, float pointerX, float pointerY) {
|
||||
final float dx = pointerX - (float) (getX() + (float) (getWidth() / 2));
|
||||
final float dy = pointerY - (float) (getY() + (float) (getHeight() / 2));
|
||||
// Log.i("SetPressed", String.format("px=%f,py=%f dx=%f,dy=%f", pointerX, pointerY, dx, dy));
|
||||
|
||||
final float pointerDistance = Math.max(Math.abs(dx), Math.abs(dy));
|
||||
final float angle = (float) Math.atan2((double) dy, (double) dx);
|
||||
|
||||
final float maxDistance = (float) Math.min((getWidth() - getPaddingLeft() - getPaddingRight()) / 2, (getHeight() - getPaddingTop() - getPaddingBottom()) / 2);
|
||||
final float length = Math.min(pointerDistance / maxDistance, 1.0f);
|
||||
// Log.i("SetPressed", String.format("pointerDist=%f,angle=%f,w=%d,h=%d,maxDist=%f,length=%f", pointerDistance, angle, getWidth(), getHeight(), maxDistance, length));
|
||||
|
||||
final float xValue = (float) Math.cos((double) angle) * length;
|
||||
final float yValue = (float) Math.sin((double) angle) * length;
|
||||
mDrawXPos = (int) (xValue * maxDistance);
|
||||
mDrawYPos = (int) (yValue * maxDistance);
|
||||
|
||||
boolean doUpdate = (pointerId != mPointerId || !mPressed || (xValue != mXValue || yValue != mYValue));
|
||||
mPointerId = pointerId;
|
||||
mPressed = true;
|
||||
mXValue = xValue;
|
||||
mYValue = yValue;
|
||||
// Log.i("SetPressed", String.format("xval=%f,yval=%f,drawX=%d,drawY=%d", mXValue, mYValue, mDrawXPos, mDrawYPos));
|
||||
|
||||
if (doUpdate) {
|
||||
invalidate();
|
||||
updateControllerState();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateControllerState() {
|
||||
final float BUTTON_THRESHOLD = 0.33f;
|
||||
|
||||
AndroidHostInterface hostInterface = AndroidHostInterface.getInstance();
|
||||
if (mXAxisCode >= 0)
|
||||
hostInterface.setControllerAxisState(mControllerIndex, mXAxisCode, mXValue);
|
||||
if (mYAxisCode >= 0)
|
||||
hostInterface.setControllerAxisState(mControllerIndex, mYAxisCode, mYValue);
|
||||
|
||||
if (mLeftButtonCode >= 0)
|
||||
hostInterface.setControllerButtonState(mControllerIndex, mLeftButtonCode, (mXValue <= -BUTTON_THRESHOLD));
|
||||
if (mRightButtonCode >= 0)
|
||||
hostInterface.setControllerButtonState(mControllerIndex, mRightButtonCode, (mXValue >= BUTTON_THRESHOLD));
|
||||
if (mUpButtonCode >= 0)
|
||||
hostInterface.setControllerButtonState(mControllerIndex, mUpButtonCode, (mYValue <= -BUTTON_THRESHOLD));
|
||||
if (mDownButtonCode >= 0)
|
||||
hostInterface.setControllerButtonState(mControllerIndex, mDownButtonCode, (mYValue >= BUTTON_THRESHOLD));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
final int paddingLeft = getPaddingLeft();
|
||||
final int paddingTop = getPaddingTop();
|
||||
final int paddingRight = getPaddingRight();
|
||||
final int paddingBottom = getPaddingBottom();
|
||||
final int contentWidth = getWidth() - paddingLeft - paddingRight;
|
||||
final int contentHeight = getHeight() - paddingTop - paddingBottom;
|
||||
|
||||
mBaseDrawable.setBounds(paddingLeft, paddingTop,
|
||||
paddingLeft + contentWidth, paddingTop + contentHeight);
|
||||
mBaseDrawable.draw(canvas);
|
||||
|
||||
final int stickWidth = contentWidth / 3;
|
||||
final int stickHeight = contentHeight / 3;
|
||||
final int halfStickWidth = stickWidth / 2;
|
||||
final int halfStickHeight = stickHeight / 2;
|
||||
final int centerX = getWidth() / 2;
|
||||
final int centerY = getHeight() / 2;
|
||||
final int drawX = centerX + mDrawXPos;
|
||||
final int drawY = centerY + mDrawYPos;
|
||||
|
||||
Drawable stickDrawable = mPressed ? mStickPressedDrawable : mStickUnpressedDrawable;
|
||||
stickDrawable.setBounds(drawX - halfStickWidth, drawY - halfStickHeight, drawX + halfStickWidth, drawY + halfStickHeight);
|
||||
stickDrawable.draw(canvas);
|
||||
}
|
||||
|
||||
public boolean isPressed() {
|
||||
return mPressed;
|
||||
}
|
||||
|
||||
public boolean hasPointerId() {
|
||||
return mPointerId >= 0;
|
||||
}
|
||||
|
||||
public int getPointerId() {
|
||||
return mPointerId;
|
||||
}
|
||||
|
||||
public void setPointerId(int mPointerId) {
|
||||
this.mPointerId = mPointerId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.HapticFeedbackConstants;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* TODO: document your custom view class.
|
||||
*/
|
||||
public final class TouchscreenControllerButtonView extends View {
|
||||
private Drawable mUnpressedDrawable;
|
||||
private Drawable mPressedDrawable;
|
||||
private boolean mPressed = false;
|
||||
private boolean mHapticFeedback = false;
|
||||
private int mControllerIndex = -1;
|
||||
private int mButtonCode = -1;
|
||||
private String mConfigName;
|
||||
|
||||
public TouchscreenControllerButtonView(Context context) {
|
||||
super(context);
|
||||
init(context, null, 0);
|
||||
}
|
||||
|
||||
public TouchscreenControllerButtonView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context, attrs, 0);
|
||||
}
|
||||
|
||||
public TouchscreenControllerButtonView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
init(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
private void init(Context context, AttributeSet attrs, int defStyle) {
|
||||
// Load attributes
|
||||
final TypedArray a = getContext().obtainStyledAttributes(
|
||||
attrs, R.styleable.TouchscreenControllerButtonView, defStyle, 0);
|
||||
|
||||
if (a.hasValue(R.styleable.TouchscreenControllerButtonView_unpressedDrawable)) {
|
||||
mUnpressedDrawable = a.getDrawable(R.styleable.TouchscreenControllerButtonView_unpressedDrawable);
|
||||
mUnpressedDrawable.setCallback(this);
|
||||
}
|
||||
|
||||
if (a.hasValue(R.styleable.TouchscreenControllerButtonView_pressedDrawable)) {
|
||||
mPressedDrawable = a.getDrawable(R.styleable.TouchscreenControllerButtonView_pressedDrawable);
|
||||
mPressedDrawable.setCallback(this);
|
||||
}
|
||||
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
final int paddingLeft = getPaddingLeft();
|
||||
final int paddingTop = getPaddingTop();
|
||||
final int paddingRight = getPaddingRight();
|
||||
final int paddingBottom = getPaddingBottom();
|
||||
final int contentWidth = getWidth() - paddingLeft - paddingRight;
|
||||
final int contentHeight = getHeight() - paddingTop - paddingBottom;
|
||||
|
||||
// Draw the example drawable on top of the text.
|
||||
Drawable drawable = mPressed ? mPressedDrawable : mUnpressedDrawable;
|
||||
if (drawable != null) {
|
||||
drawable.setBounds(paddingLeft, paddingTop,
|
||||
paddingLeft + contentWidth, paddingTop + contentHeight);
|
||||
drawable.draw(canvas);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isPressed() {
|
||||
return mPressed;
|
||||
}
|
||||
|
||||
public void setPressed(boolean pressed) {
|
||||
if (pressed == mPressed)
|
||||
return;
|
||||
|
||||
mPressed = pressed;
|
||||
invalidate();
|
||||
updateControllerState();
|
||||
|
||||
if (mHapticFeedback) {
|
||||
performHapticFeedback(pressed ? HapticFeedbackConstants.VIRTUAL_KEY : HapticFeedbackConstants.VIRTUAL_KEY_RELEASE);
|
||||
}
|
||||
}
|
||||
|
||||
public void setButtonCode(int controllerIndex, int code) {
|
||||
mControllerIndex = controllerIndex;
|
||||
mButtonCode = code;
|
||||
}
|
||||
|
||||
public void setConfigName(String name) {
|
||||
mConfigName = name;
|
||||
}
|
||||
|
||||
public String getConfigName() {
|
||||
return mConfigName;
|
||||
}
|
||||
|
||||
public void setHapticFeedback(boolean enabled) {
|
||||
mHapticFeedback = enabled;
|
||||
}
|
||||
|
||||
private void updateControllerState() {
|
||||
if (mButtonCode >= 0)
|
||||
AndroidHostInterface.getInstance().setControllerButtonState(mControllerIndex, mButtonCode, mPressed);
|
||||
}
|
||||
|
||||
public Drawable getPressedDrawable() {
|
||||
return mPressedDrawable;
|
||||
}
|
||||
|
||||
public void setPressedDrawable(Drawable pressedDrawable) {
|
||||
mPressedDrawable = pressedDrawable;
|
||||
}
|
||||
|
||||
public Drawable getUnpressedDrawable() {
|
||||
return mUnpressedDrawable;
|
||||
}
|
||||
|
||||
public void setUnpressedDrawable(Drawable unpressedDrawable) {
|
||||
mUnpressedDrawable = unpressedDrawable;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,445 @@
|
||||
package com.github.stenzek.duckstation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Rect;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* TODO: document your custom view class.
|
||||
*/
|
||||
public class TouchscreenControllerView extends FrameLayout {
|
||||
private int mControllerIndex;
|
||||
private String mControllerType;
|
||||
private String mViewType;
|
||||
private View mMainView;
|
||||
private ArrayList<TouchscreenControllerButtonView> mButtonViews = new ArrayList<>();
|
||||
private ArrayList<TouchscreenControllerAxisView> mAxisViews = new ArrayList<>();
|
||||
private boolean mHapticFeedback;
|
||||
private String mLayoutOrientation;
|
||||
private boolean mEditingLayout = false;
|
||||
private View mMovingView = null;
|
||||
private String mMovingName = null;
|
||||
private float mMovingLastX = 0.0f;
|
||||
private float mMovingLastY = 0.0f;
|
||||
private ConstraintLayout mEditLayout = null;
|
||||
|
||||
public TouchscreenControllerView(Context context) {
|
||||
super(context);
|
||||
setFocusable(false);
|
||||
setFocusableInTouchMode(false);
|
||||
}
|
||||
|
||||
public TouchscreenControllerView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public TouchscreenControllerView(Context context, AttributeSet attrs, int defStyle) {
|
||||
super(context, attrs, defStyle);
|
||||
}
|
||||
|
||||
private String getConfigKeyForXTranslation(String name) {
|
||||
return String.format("TouchscreenController/%s/%s%sXTranslation", mViewType, name, mLayoutOrientation);
|
||||
}
|
||||
|
||||
private String getConfigKeyForYTranslation(String name) {
|
||||
return String.format("TouchscreenController/%s/%s%sYTranslation", mViewType, name, mLayoutOrientation);
|
||||
}
|
||||
|
||||
private void saveTranslationForButton(String name, float xTranslation, float yTranslation) {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
final SharedPreferences.Editor editor = prefs.edit();
|
||||
editor.putFloat(getConfigKeyForXTranslation(name), xTranslation);
|
||||
editor.putFloat(getConfigKeyForYTranslation(name), yTranslation);
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
private void clearTranslationForAllButtons() {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
final SharedPreferences.Editor editor = prefs.edit();
|
||||
|
||||
for (TouchscreenControllerButtonView buttonView : mButtonViews) {
|
||||
editor.remove(getConfigKeyForXTranslation(buttonView.getConfigName()));
|
||||
editor.remove(getConfigKeyForYTranslation(buttonView.getConfigName()));
|
||||
buttonView.setTranslationX(0.0f);
|
||||
buttonView.setTranslationY(0.0f);
|
||||
}
|
||||
|
||||
for (TouchscreenControllerAxisView axisView : mAxisViews) {
|
||||
editor.remove(getConfigKeyForXTranslation(axisView.getConfigName()));
|
||||
editor.remove(getConfigKeyForYTranslation(axisView.getConfigName()));
|
||||
axisView.setTranslationX(0.0f);
|
||||
axisView.setTranslationY(0.0f);
|
||||
}
|
||||
|
||||
editor.commit();
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
private void reloadButtonTranslation() {
|
||||
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
|
||||
for (TouchscreenControllerButtonView buttonView : mButtonViews) {
|
||||
try {
|
||||
buttonView.setTranslationX(prefs.getFloat(getConfigKeyForXTranslation(buttonView.getConfigName()), 0.0f));
|
||||
buttonView.setTranslationY(prefs.getFloat(getConfigKeyForYTranslation(buttonView.getConfigName()), 0.0f));
|
||||
//Log.i("TouchscreenController", String.format("Translation for %s %f %f", buttonView.getConfigName(),
|
||||
// buttonView.getTranslationX(), buttonView.getTranslationY()));
|
||||
} catch (ClassCastException ex) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
for (TouchscreenControllerAxisView axisView : mAxisViews) {
|
||||
try {
|
||||
axisView.setTranslationX(prefs.getFloat(getConfigKeyForXTranslation(axisView.getConfigName()), 0.0f));
|
||||
axisView.setTranslationY(prefs.getFloat(getConfigKeyForYTranslation(axisView.getConfigName()), 0.0f));
|
||||
} catch (ClassCastException ex) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getOrientationString() {
|
||||
switch (getContext().getResources().getConfiguration().orientation) {
|
||||
case Configuration.ORIENTATION_PORTRAIT:
|
||||
return "Portrait";
|
||||
case Configuration.ORIENTATION_LANDSCAPE:
|
||||
default:
|
||||
return "Landscape";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the orientation of the layout has changed, and if so, reloads button translations.
|
||||
*/
|
||||
public void updateOrientation() {
|
||||
String newOrientation = getOrientationString();
|
||||
if (mLayoutOrientation != null && mLayoutOrientation.equals(newOrientation))
|
||||
return;
|
||||
|
||||
Log.i("TouchscreenController", "New orientation: " + newOrientation);
|
||||
mLayoutOrientation = newOrientation;
|
||||
reloadButtonTranslation();
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
public void init(int controllerIndex, String controllerType, String viewType, boolean hapticFeedback) {
|
||||
mControllerIndex = controllerIndex;
|
||||
mControllerType = controllerType;
|
||||
mViewType = viewType;
|
||||
mHapticFeedback = hapticFeedback;
|
||||
mLayoutOrientation = getOrientationString();
|
||||
|
||||
if (mEditingLayout)
|
||||
endLayoutEditing();
|
||||
|
||||
mButtonViews.clear();
|
||||
mAxisViews.clear();
|
||||
removeAllViews();
|
||||
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
switch (viewType) {
|
||||
case "digital":
|
||||
mMainView = inflater.inflate(R.layout.layout_touchscreen_controller_digital, this, true);
|
||||
break;
|
||||
|
||||
case "analog_stick":
|
||||
mMainView = inflater.inflate(R.layout.layout_touchscreen_controller_analog_stick, this, true);
|
||||
break;
|
||||
|
||||
case "analog_sticks":
|
||||
mMainView = inflater.inflate(R.layout.layout_touchscreen_controller_analog_sticks, this, true);
|
||||
break;
|
||||
|
||||
case "none":
|
||||
default:
|
||||
mMainView = null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (mMainView == null)
|
||||
return;
|
||||
|
||||
mMainView.setOnTouchListener((view1, event) -> {
|
||||
if (mEditingLayout)
|
||||
return handleEditingTouchEvent(event);
|
||||
else
|
||||
return handleTouchEvent(event);
|
||||
});
|
||||
|
||||
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
|
||||
linkButton(mMainView, R.id.controller_button_up, "UpButton", "Up");
|
||||
linkButton(mMainView, R.id.controller_button_right, "RightButton", "Right");
|
||||
linkButton(mMainView, R.id.controller_button_down, "DownButton", "Down");
|
||||
linkButton(mMainView, R.id.controller_button_left, "LeftButton", "Left");
|
||||
linkButton(mMainView, R.id.controller_button_l1, "L1Button", "L1");
|
||||
linkButton(mMainView, R.id.controller_button_l2, "L2Button", "L2");
|
||||
linkButton(mMainView, R.id.controller_button_select, "SelectButton", "Select");
|
||||
linkButton(mMainView, R.id.controller_button_start, "StartButton", "Start");
|
||||
linkButton(mMainView, R.id.controller_button_triangle, "TriangleButton", "Triangle");
|
||||
linkButton(mMainView, R.id.controller_button_circle, "CircleButton", "Circle");
|
||||
linkButton(mMainView, R.id.controller_button_cross, "CrossButton", "Cross");
|
||||
linkButton(mMainView, R.id.controller_button_square, "SquareButton", "Square");
|
||||
linkButton(mMainView, R.id.controller_button_r1, "R1Button", "R1");
|
||||
linkButton(mMainView, R.id.controller_button_r2, "R2Button", "R2");
|
||||
|
||||
if (!linkAxis(mMainView, R.id.controller_axis_left, "LeftAxis", "Left"))
|
||||
linkAxisToButtons(mMainView, R.id.controller_axis_left, "LeftAxis", "");
|
||||
|
||||
linkAxis(mMainView, R.id.controller_axis_right, "RightAxis", "Right");
|
||||
reloadButtonTranslation();
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
private void linkButton(View view, int id, String configName, String buttonName) {
|
||||
TouchscreenControllerButtonView buttonView = (TouchscreenControllerButtonView) view.findViewById(id);
|
||||
if (buttonView == null)
|
||||
return;
|
||||
|
||||
int code = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonName);
|
||||
Log.i("TouchscreenController", String.format("%s -> %d", buttonName, code));
|
||||
|
||||
if (code >= 0) {
|
||||
buttonView.setConfigName(configName);
|
||||
buttonView.setButtonCode(mControllerIndex, code);
|
||||
buttonView.setHapticFeedback(mHapticFeedback);
|
||||
mButtonViews.add(buttonView);
|
||||
} else {
|
||||
Log.e("TouchscreenController", String.format("Unknown button name '%s' " +
|
||||
"for '%s'", buttonName, mControllerType));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean linkAxis(View view, int id, String configName, String axisName) {
|
||||
TouchscreenControllerAxisView axisView = (TouchscreenControllerAxisView) view.findViewById(id);
|
||||
if (axisView == null)
|
||||
return false;
|
||||
|
||||
int xCode = AndroidHostInterface.getControllerAxisCode(mControllerType, axisName + "X");
|
||||
int yCode = AndroidHostInterface.getControllerAxisCode(mControllerType, axisName + "Y");
|
||||
Log.i("TouchscreenController", String.format("%s -> %d/%d", axisName, xCode, yCode));
|
||||
if (xCode < 0 && yCode < 0)
|
||||
return false;
|
||||
|
||||
axisView.setConfigName(configName);
|
||||
axisView.setControllerAxis(mControllerIndex, xCode, yCode);
|
||||
mAxisViews.add(axisView);
|
||||
return true;
|
||||
}
|
||||
|
||||
private boolean linkAxisToButtons(View view, int id, String configName, String buttonPrefix) {
|
||||
TouchscreenControllerAxisView axisView = (TouchscreenControllerAxisView) view.findViewById(id);
|
||||
if (axisView == null)
|
||||
return false;
|
||||
|
||||
int leftCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Left");
|
||||
int rightCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Right");
|
||||
int upCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Up");
|
||||
int downCode = AndroidHostInterface.getControllerButtonCode(mControllerType, buttonPrefix + "Down");
|
||||
Log.i("TouchscreenController", String.format("%s(ButtonAxis) -> %d,%d,%d,%d", buttonPrefix, leftCode, rightCode, upCode, downCode));
|
||||
if (leftCode < 0 && rightCode < 0 && upCode < 0 && downCode < 0)
|
||||
return false;
|
||||
|
||||
axisView.setControllerButtons(mControllerIndex, leftCode, rightCode, upCode, downCode);
|
||||
mAxisViews.add(axisView);
|
||||
return true;
|
||||
}
|
||||
|
||||
private int dpToPixels(float dp) {
|
||||
return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics()));
|
||||
}
|
||||
|
||||
public void startLayoutEditing() {
|
||||
if (mEditLayout == null) {
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
mEditLayout = (ConstraintLayout) inflater.inflate(R.layout.layout_touchscreen_controller_edit, this, false);
|
||||
((Button) mEditLayout.findViewById(R.id.stop_editing)).setOnClickListener((view) -> endLayoutEditing());
|
||||
((Button) mEditLayout.findViewById(R.id.reset_layout)).setOnClickListener((view) -> clearTranslationForAllButtons());
|
||||
addView(mEditLayout);
|
||||
}
|
||||
|
||||
mEditingLayout = true;
|
||||
}
|
||||
|
||||
public void endLayoutEditing() {
|
||||
if (mEditLayout != null) {
|
||||
((ViewGroup) mMainView).removeView(mEditLayout);
|
||||
mEditLayout = null;
|
||||
}
|
||||
|
||||
mEditingLayout = false;
|
||||
mMovingView = null;
|
||||
mMovingName = null;
|
||||
mMovingLastX = 0.0f;
|
||||
mMovingLastY = 0.0f;
|
||||
}
|
||||
|
||||
private boolean handleEditingTouchEvent(MotionEvent event) {
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_UP: {
|
||||
if (mMovingView != null) {
|
||||
// save position
|
||||
saveTranslationForButton(mMovingName, mMovingView.getTranslationX(), mMovingView.getTranslationY());
|
||||
mMovingView = null;
|
||||
mMovingName = null;
|
||||
mMovingLastX = 0.0f;
|
||||
mMovingLastY = 0.0f;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_DOWN: {
|
||||
if (mMovingView != null) {
|
||||
// already moving a button
|
||||
return true;
|
||||
}
|
||||
|
||||
Rect rect = new Rect();
|
||||
final float x = event.getX();
|
||||
final float y = event.getY();
|
||||
for (TouchscreenControllerButtonView buttonView : mButtonViews) {
|
||||
buttonView.getHitRect(rect);
|
||||
if (rect.contains((int) x, (int) y)) {
|
||||
mMovingView = buttonView;
|
||||
mMovingName = buttonView.getConfigName();
|
||||
mMovingLastX = x;
|
||||
mMovingLastY = y;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
for (TouchscreenControllerAxisView axisView : mAxisViews) {
|
||||
axisView.getHitRect(rect);
|
||||
if (rect.contains((int) x, (int) y)) {
|
||||
mMovingView = axisView;
|
||||
mMovingName = axisView.getConfigName();
|
||||
mMovingLastX = x;
|
||||
mMovingLastY = y;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// nothing..
|
||||
return true;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
if (mMovingView == null)
|
||||
return true;
|
||||
|
||||
final float x = event.getX();
|
||||
final float y = event.getY();
|
||||
final float dx = x - mMovingLastX;
|
||||
final float dy = y - mMovingLastY;
|
||||
mMovingLastX = x;
|
||||
mMovingLastY = y;
|
||||
|
||||
final float posX = mMovingView.getX() + dx;
|
||||
final float posY = mMovingView.getY() + dy;
|
||||
//Log.d("Position", String.format("%f %f -> (%f %f) %f %f",
|
||||
// mMovingView.getX(), mMovingView.getY(), dx, dy, posX, posY));
|
||||
mMovingView.setX(posX);
|
||||
mMovingView.setY(posY);
|
||||
mMovingView.invalidate();
|
||||
mMainView.requestLayout();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean handleTouchEvent(MotionEvent event) {
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_UP: {
|
||||
if (!AndroidHostInterface.hasInstanceAndEmulationThreadIsRunning())
|
||||
return false;
|
||||
|
||||
for (TouchscreenControllerButtonView buttonView : mButtonViews) {
|
||||
buttonView.setPressed(false);
|
||||
}
|
||||
|
||||
for (TouchscreenControllerAxisView axisView : mAxisViews) {
|
||||
axisView.setUnpressed();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_DOWN:
|
||||
case MotionEvent.ACTION_POINTER_UP:
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
if (!AndroidHostInterface.hasInstanceAndEmulationThreadIsRunning())
|
||||
return false;
|
||||
|
||||
Rect rect = new Rect();
|
||||
final int pointerCount = event.getPointerCount();
|
||||
final int liftedPointerIndex = (event.getActionMasked() == MotionEvent.ACTION_POINTER_UP) ? event.getActionIndex() : -1;
|
||||
for (TouchscreenControllerButtonView buttonView : mButtonViews) {
|
||||
buttonView.getHitRect(rect);
|
||||
boolean pressed = false;
|
||||
for (int i = 0; i < pointerCount; i++) {
|
||||
if (i == liftedPointerIndex)
|
||||
continue;
|
||||
|
||||
final int x = (int) event.getX(i);
|
||||
final int y = (int) event.getY(i);
|
||||
if (rect.contains(x, y)) {
|
||||
buttonView.setPressed(true);
|
||||
pressed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!pressed)
|
||||
buttonView.setPressed(pressed);
|
||||
}
|
||||
|
||||
for (TouchscreenControllerAxisView axisView : mAxisViews) {
|
||||
axisView.getHitRect(rect);
|
||||
boolean pressed = false;
|
||||
for (int i = 0; i < pointerCount; i++) {
|
||||
if (i == liftedPointerIndex)
|
||||
continue;
|
||||
|
||||
final int pointerId = event.getPointerId(i);
|
||||
final int x = (int) event.getX(i);
|
||||
final int y = (int) event.getY(i);
|
||||
|
||||
if ((rect.contains(x, y) && !axisView.isPressed()) ||
|
||||
(axisView.isPressed() && axisView.getPointerId() == pointerId)) {
|
||||
axisView.setPressed(pointerId, x, y);
|
||||
pressed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!pressed)
|
||||
axisView.setUnpressed();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="108"
|
||||
android:viewportHeight="108">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="78.5885"
|
||||
android:endY="90.9159"
|
||||
android:startX="48.7653"
|
||||
android:startY="61.0927"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#44000000"
|
||||
android:offset="0.0" />
|
||||
<item
|
||||
android:color="#00000000"
|
||||
android:offset="1.0" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillType="nonZero"
|
||||
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||
android:strokeWidth="1"
|
||||
android:strokeColor="#00000000" />
|
||||
</vector>
|
||||
71
android/app/src/main/res/drawable/flag_eu.xml
Normal file
71
android/app/src/main/res/drawable/flag_eu.xml
Normal file
@@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- https://raw.githubusercontent.com/Shusshu/android-flags/master/flags/src/main/res/drawable/flag_us2.xml -->
|
||||
<vector android:height="15dp"
|
||||
android:viewportHeight="15"
|
||||
android:viewportWidth="21"
|
||||
android:width="21dp"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M0,0h21v15h-21z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="10.5"
|
||||
android:endY="15"
|
||||
android:startX="10.5"
|
||||
android:startY="0"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#FFFFFFFF"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#FFF0F0F0"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M0,0h21v15h-21z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="10.5"
|
||||
android:endY="15"
|
||||
android:startX="10.5"
|
||||
android:startY="0"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#FF043CAE"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#FF00339A"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M10.5,3L9.7929,3.2071L10,2.5L9.7929,1.7929L10.5,2L11.2071,1.7929L11,2.5L11.2071,3.2071L10.5,3ZM10.5,13L9.7929,13.2071L10,12.5L9.7929,11.7929L10.5,12L11.2071,11.7929L11,12.5L11.2071,13.2071L10.5,13ZM15.5,8L14.7929,8.2071L15,7.5L14.7929,6.7929L15.5,7L16.2071,6.7929L16,7.5L16.2071,8.2071L15.5,8ZM5.5,8L4.7929,8.2071L5,7.5L4.7929,6.7929L5.5,7L6.2071,6.7929L6,7.5L6.2071,8.2071L5.5,8ZM14.8301,5.5L14.123,5.7071L14.3301,5L14.123,4.2929L14.8301,4.5L15.5372,4.2929L15.3301,5L15.5372,5.7071L14.8301,5.5ZM6.1699,10.5L5.4628,10.7071L5.6699,10L5.4628,9.2929L6.1699,9.5L6.877,9.2929L6.6699,10L6.877,10.7071L6.1699,10.5ZM13,3.6699L12.2929,3.877L12.5,3.1699L12.2929,2.4628L13,2.6699L13.7071,2.4628L13.5,3.1699L13.7071,3.877L13,3.6699ZM8,12.3301L7.2929,12.5372L7.5,11.8301L7.2929,11.123L8,11.3301L8.7071,11.123L8.5,11.8301L8.7071,12.5372L8,12.3301ZM14.8301,10.5L14.123,10.7071L14.3301,10L14.123,9.2929L14.8301,9.5L15.5372,9.2929L15.3301,10L15.5372,10.7071L14.8301,10.5ZM6.1699,5.5L5.4628,5.7071L5.6699,5L5.4628,4.2929L6.1699,4.5L6.877,4.2929L6.6699,5L6.877,5.7071L6.1699,5.5ZM13,12.3301L12.2929,12.5372L12.5,11.8301L12.2929,11.123L13,11.3301L13.7071,11.123L13.5,11.8301L13.7071,12.5372L13,12.3301ZM8,3.6699L7.2929,3.877L7.5,3.1699L7.2929,2.4628L8,2.6699L8.7071,2.4628L8.5,3.1699L8.7071,3.877L8,3.6699Z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="10.5"
|
||||
android:endY="13.2071"
|
||||
android:startX="10.5"
|
||||
android:startY="1.7929"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#FFFFD429"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#FFFFCC00"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
||||
50
android/app/src/main/res/drawable/flag_jp.xml
Normal file
50
android/app/src/main/res/drawable/flag_jp.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- https://raw.githubusercontent.com/Shusshu/android-flags/master/flags/src/main/res/drawable/flag_hp.xml -->
|
||||
<vector android:height="15dp"
|
||||
android:viewportHeight="15"
|
||||
android:viewportWidth="21"
|
||||
android:width="21dp"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M0,0h21v15h-21z"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="10.5"
|
||||
android:endY="15"
|
||||
android:startX="10.5"
|
||||
android:startY="0"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#FFFFFFFF"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#FFF0F0F0"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M10.5,7.5m-4.5,0a4.5,4.5 0,1 1,9 0a4.5,4.5 0,1 1,-9 0"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeWidth="1">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:endX="10.5"
|
||||
android:endY="12"
|
||||
android:startX="10.5"
|
||||
android:startY="3"
|
||||
android:type="linear">
|
||||
<item
|
||||
android:color="#FFD81441"
|
||||
android:offset="0" />
|
||||
<item
|
||||
android:color="#FFBB0831"
|
||||
android:offset="1" />
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
||||
539
android/app/src/main/res/drawable/flag_us.xml
Normal file
539
android/app/src/main/res/drawable/flag_us.xml
Normal file
@@ -0,0 +1,539 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- https://raw.githubusercontent.com/Shusshu/android-flags/master/flags/src/main/res/drawable/flag_us2.xml -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="21dp"
|
||||
android:height="21dp"
|
||||
android:viewportWidth="10"
|
||||
android:viewportHeight="13">
|
||||
<path
|
||||
android:fillColor="#bd3d44"
|
||||
android:pathData="M0 0h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M0 1h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#bd3d44"
|
||||
android:pathData="M0 2h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M0 3h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#bd3d44"
|
||||
android:pathData="M0 4h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M0 5h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#bd3d44"
|
||||
android:pathData="M0 6h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M0 7h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#bd3d44"
|
||||
android:pathData="M0 8h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M0 9h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#bd3d44"
|
||||
android:pathData="M0 10h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M0 11h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#bd3d44"
|
||||
android:pathData="M0 12h13v1h-13Z" />
|
||||
<path
|
||||
android:fillColor="#192f5d"
|
||||
android:pathData="M0 0h5.2v7h-5.2Z" />
|
||||
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="4.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
|
||||
|
||||
<group android:translateY="1.4">
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="4.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
|
||||
<group android:translateY="2.9">
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="4.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
|
||||
<group android:translateY="4.3">
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="4.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
|
||||
<group android:translateY="5.6">
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="4.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<!-- Odd stars -->
|
||||
|
||||
<group
|
||||
android:translateY="0.7"
|
||||
android:translateX="0.4">
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group
|
||||
android:translateY="2.1"
|
||||
android:translateX="0.4">
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group
|
||||
android:translateY="3.6"
|
||||
android:translateX="0.4">
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
<group
|
||||
android:translateY="5.0"
|
||||
android:translateX="0.4">
|
||||
<group
|
||||
android:translateX="0.2"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.0"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="1.8"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="2.6"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
<group
|
||||
android:translateX="3.4"
|
||||
android:translateY="0.2"
|
||||
android:scaleX="0.009"
|
||||
android:scaleY="0.012">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M 48,54 L 31,42 15,54 21,35 6,23 25,23 32,4 40,23 58,23 42,35 z" />
|
||||
</group>
|
||||
</group>
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,16 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2l-5.5,9h11z" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M17.5,17.5m-4.5,0a4.5,4.5 0,1 1,9 0a4.5,4.5 0,1 1,-9 0" />
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M3,13.5h8v8H3z" />
|
||||
</vector>
|
||||
10
android/app/src/main/res/drawable/ic_baseline_delete_24.xml
Normal file
10
android/app/src/main/res/drawable/ic_baseline_delete_24.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z" />
|
||||
</vector>
|
||||
10
android/app/src/main/res/drawable/ic_baseline_folder_24.xml
Normal file
10
android/app/src/main/res/drawable/ic_baseline_folder_24.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,6h-8l-2,-2L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,8c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,8h16v10z" />
|
||||
</vector>
|
||||
10
android/app/src/main/res/drawable/ic_baseline_gamepad_24.xml
Normal file
10
android/app/src/main/res/drawable/ic_baseline_gamepad_24.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15,7.5V2H9v5.5l3,3 3,-3zM7.5,9H2v6h5.5l3,-3 -3,-3zM9,16.5V22h6v-5.5l-3,-3 -3,3zM16.5,9l-3,3 3,3H22V9h-5.5z" />
|
||||
</vector>
|
||||
10
android/app/src/main/res/drawable/ic_baseline_help_24.xml
Normal file
10
android/app/src/main/res/drawable/ic_baseline_help_24.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,19h-2v-2h2v2zM15.07,11.25l-0.9,0.92C13.45,12.9 13,13.5 13,15h-2v-0.5c0,-1.1 0.45,-2.1 1.17,-2.83l1.24,-1.26c0.37,-0.36 0.59,-0.86 0.59,-1.41 0,-1.1 -0.9,-2 -2,-2s-2,0.9 -2,2L8,9c0,-2.21 1.79,-4 4,-4s4,1.79 4,4c0,0.88 -0.36,1.68 -0.93,2.25z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,2L8,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM18,7h-3v5.5c0,1.38 -1.12,2.5 -2.5,2.5S10,13.88 10,12.5s1.12,-2.5 2.5,-2.5c0.57,0 1.08,0.19 1.5,0.51L14,5h4v2zM4,6L2,6v14c0,1.1 0.9,2 2,2h14v-2L4,20L4,6z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<vector android:height="60dp" android:tint="?attr/colorControlNormal"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="60dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8 0,-1.85 0.63,-3.55 1.69,-4.9L16.9,18.31C15.55,19.37 13.85,20 12,20zM18.31,16.9L7.1,5.69C8.45,4.63 10.15,4 12,4c4.42,0 8,3.58 8,8 0,1.85 -0.63,3.55 -1.69,4.9z"/>
|
||||
</vector>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user