Compare commits

...

169 Commits
2124 ... master

Author SHA1 Message Date
GasInfinity
3066887ff4 fix: don't crash when getaddrinfo gets a small or empty buffer in soc:U
* those are valid in hw!
2026-03-29 20:08:41 +02:00
GasInfinity
901f010913 fix: properly handle getaddrinfo/getnameinfo return values in soc:U 2026-03-29 20:08:41 +02:00
Why? You Don't Know?
5fc9732f05
android: Convert bgColor default values to Int (#1959) 2026-03-29 20:05:54 +02:00
OpenSauce04
39363cd435 ci: Merge standalone macOS CI/CD jobs into single runner 2026-03-28 15:32:24 +00:00
GasInfinity
60661c3b8b fix: correct the response of SendToOther in soc:U 2026-03-28 14:14:25 +00:00
PabloMK7
be0f096f48
core: Set boss as a online LLE module (#1952) 2026-03-28 12:43:26 +01:00
Marcin Serwin
6201256e15
cmake: Add option to use system oaknut (#1947)
Signed-off-by: Marcin Serwin <marcin@serwin.dev>
2026-03-28 12:21:42 +01:00
PabloMK7
af188bb7b7
core: kernel: Implement thread cpu time limit for core1 (#1934) 2026-03-28 12:20:33 +01:00
RedBlackAka
0862e5e98a
Qt: Remove Vulkan warning and OpenGL Mesa override (#1938) 2026-03-28 12:17:15 +01:00
PabloMK7
49b0bef17d
android: Fix visibility of hidden system titles (#1935) 2026-03-28 12:04:43 +01:00
PabloMK7
f14f095e72
core: svc: Add better logging to svc failures (#1948) 2026-03-28 12:03:16 +01:00
PabloMK7
7e58ac5bcf android: Handle surface lost during swapchain creation 2026-03-27 18:31:13 +00:00
OpenSauce04
7220bd2edd externals: Updated to boost 1.90 + LLVM 22 workaround 2026-03-27 18:30:41 +00:00
PabloMK7
d4e9daa739
android: Fix compression and decompression on vanilla build (#1939) 2026-03-24 18:58:56 +01:00
OpenSauce04
9b045bf837 libretro: Replace render_touchscreen setting with enable_touch_pointer_timeout
Touch pointer rendering is now always enabled, but unless a controller is being used to move the touchscreen cursor, it will be hidden due to the timeout which is also enabled by default.
2026-03-23 13:07:39 +00:00
PabloMK7
7a600e28d2 android: Fix icon not showing if update title fails to load 2026-03-22 22:57:32 +01:00
PabloMK7
5a07260e1b loader: Fix identifying zcci files when system files are not set up 2026-03-22 22:57:32 +01:00
OpenSauce04
2c8297c34c android: Fixed native path intent URIs not launching apps correctly 2026-03-21 22:01:00 +00:00
OpenSauce
7f9f1e90ca
Added prerelease badge to readme 2026-03-20 18:42:21 +00:00
OpenSauce04
04a543290a Added AI policy document 2026-03-20 14:15:36 +00:00
OpenSauce04
8bcb8a225a Updated translations via Transifex 2026-03-20 12:42:47 +00:00
David Griswold
64cb0b57fb nullptr check on update_surface 2026-03-20 12:38:41 +00:00
OpenSauce04
7a60160f68 Updated translations via Transifex 2026-03-19 14:50:43 +00:00
OpenSauce04
dc91e8803e Updated compatibility data 2026-03-19 14:36:28 +00:00
PabloMK7
c55435b78d
android: Fix lifecycle bugs on SetupFragment (#1902)
* android: Attempt fixing lifecycle bugs on SetupFragment

* android: Fixed setup page number being lost on recreation

* Move the registerForActivityResult to MainActivity

* Code cleanup

* ViewUtils.kt: Added missing guard clause in showView

* Fixed permission buttons appearing to duplicate during setup

* ViewUtils.kt: Updated license header

---------

Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
2026-03-19 13:48:58 +01:00
David Griswold
f721a474e4
force android emu_window to update height and width on surface change, solving aspect ratio issues on some screens (#1907) 2026-03-19 13:46:56 +01:00
PabloMK7
ab39df3ff0
android: Handle asynchronous screen disconnects (#1903) 2026-03-17 19:24:30 +01:00
OpenSauce04
2ff04dccba Removed confusing punctuation from "Failed to obtain loader" log message 2026-03-17 12:25:54 +00:00
PabloMK7
3d5ba09eb1
android: Fix launching applications through intent data in vanilla build (#1896)
* android: Fix launching applications through intent data in vanilla build

* GameHelper.kt: Use Uri.scheme where applicable

---------

Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
2026-03-17 12:15:33 +00:00
Cobalt
ae9972b6be
Qt compat fix (again) (#1895)
* fix compilation on older QT6

* fix indent

my C++ is very rusty

* fix indents again

* Fixed formatting

* fix capitalization error

---------

Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
2026-03-15 18:29:30 +00:00
OpenSauce04
e677f72bda android: Fixed onResume attempting to pause instead of unpause 2026-03-15 15:50:05 +00:00
OpenSauce04
4109bb200b android: Show Azahar version in toast when double-clicking on Applications 2026-03-15 14:51:53 +00:00
PabloMK7
6ad642a984
android: camera: Fix still image camera input (#1892) 2026-03-15 15:17:50 +01:00
Cobalt
ccd61d0134
qt: fix compilation on older QT6 (#1886)
* fix compilation on older QT6

* fix indent

my C++ is very rusty

* fix indents again

* Fixed formatting

---------

Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
2026-03-15 13:46:50 +01:00
OpenSauce04
d97da17263 android: Fixed installed app shortcut creation failing on vanilla 2026-03-14 19:47:00 +00:00
OpenSauce04
0ff2aebdf1 android: Fixed Amiibo files failing to load on vanilla 2026-03-14 17:57:03 +00:00
Lillie
100b00b3b5 Fix typo "cartidges" 2026-03-13 10:02:38 +00:00
OpenSauce04
9e162705f4 Updated translations via Transifex 2026-03-12 20:54:03 +00:00
PabloMK7
b3f82618d7 android: Use StorageManager to get removable media path 2026-03-12 20:49:20 +00:00
OpenSauce04
fc6a410dfa android: Separate package IDs for build variants 2026-03-12 20:45:19 +00:00
OpenSauce04
56a563c239 macos: Add warning dialog when launching azahar executable directly 2026-03-12 18:14:12 +00:00
OpenSauce04
6715959382 shader_jit_a64_compiler: Added missing include
Fixes a build issue on ARM64 Linux w/ GCC

Fix proposed by PabloMK7
2026-03-12 17:18:29 +00:00
OpenSauce04
75cd4ce649 ci: Add build tests for ARM Linux
No binaries yet
2026-03-12 16:14:43 +00:00
Eric Warmenhoven
463db8ffe4 Move libretro ci file to .ci 2026-03-12 15:18:44 +00:00
Eric Warmenhoven
ecaebc54ff Rename libretro ci file 2026-03-12 15:01:29 +00:00
David Griswold
1febb83942
qt: Add controller touchpad support (#777) 2026-03-12 00:21:17 +01:00
PabloMK7
a3db3be4a6
video_core: vulkan: Fix Framebuffer move behaviour (#1865) 2026-03-11 23:36:03 +01:00
PabloMK7
909e4b7e1f
core: apt: Fix GetStartupArgument operation order (#1862) 2026-03-11 18:48:15 +01:00
PabloMK7
e92272ce31
core: fs: Implement NAND archives (#1861) 2026-03-11 15:06:28 +01:00
PabloMK7
51170ea85d
core: ac: Implement GetNZoneBeaconNotFoundEvent (#1860) 2026-03-11 13:49:56 +01:00
PabloMK7
9a7cc43d81
core: kernel: Set debug thread name based on process name (#1859) 2026-03-11 13:49:39 +01:00
OpenSauce04
e351fa56ce Updated translations via Transifex 2026-03-10 19:25:21 +00:00
OpenSauce04
845fadf49e android: Fix IOFile::GetFd not functioning as expected in vanilla
This fixes the cheats menu not loading correctly
2026-03-10 19:17:37 +00:00
Eric Warmenhoven
98910fed1c default libretro "touch support" option on 2026-03-10 16:28:22 +00:00
RedBlackAka
d9f28c5b2a
Qt: Improve update checker system to prevent downgrades #1749 (#1768)
* Qt: Improve update checker system to prevent downgrades

* Code cleanup

* Return 0 as fallback

* Satisfy clang-format

---------

Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
2026-03-10 16:01:40 +00:00
OpenSauce04
784fc8cca9 Updated translations via Transifex 2026-03-08 19:45:39 +00:00
OpenSauce04
a35a619903 ci: Add version suffix to libretro archive filenames 2026-03-08 19:43:38 +00:00
Eric Warmenhoven
3a5fa35449 libretro: draw cursor in vulkan 2026-03-08 19:04:10 +00:00
OpenSauce04
0407568006 android: Fix long-press menu for games in app dir displaying no file error 2026-03-08 18:46:09 +00:00
PabloMK7
30779d35cd Fix 3DSX being treated as invalid applications 2026-03-08 18:46:09 +00:00
PabloMK7
32da5ea0ae Read media type and pass it to UninstallProgram 2026-03-08 18:46:09 +00:00
OpenSauce04
e878174df8 android: Fixed games located in an application directory not being accessible 2026-03-08 18:46:09 +00:00
OpenSauce04
7ad6621f91 android: Fixed CIA installation failure in vanilla variant
This introduces a very hacky way of telling TranslateFilePath that a path is already native and doesn't need translating. I don't like this very much, but addressing this in any other way is very much outside of the scope of this PR.
2026-03-08 18:46:09 +00:00
OpenSauce04
8e1ffc1bdc Fixed a possible app crash when calling AndroidStorage::GetUserDirectory 2026-03-08 18:46:09 +00:00
OpenSauce04
96485a22f8 android: Split path resolution logic of getUserDirectory into seperate function 2026-03-08 18:46:09 +00:00
OpenSauce04
97c9a51015 android: Re-implement title uninstallation via Service::AM::UninstallProgram 2026-03-08 18:46:09 +00:00
OpenSauce04
a8ebd0f551 DocumentsTree: Put resolvePath under a strict directory whitelist 2026-03-08 18:46:09 +00:00
OpenSauce04
fac63ce6b1 DocumentsTree: Re-implement getFilename without resolvePath 2026-03-08 18:46:09 +00:00
OpenSauce04
c71b2dc822 Move AndroidCanUseRawFS and AndroidTranslateFilename into AndroidStorage namespace 2026-03-08 18:46:09 +00:00
PabloMK7
e87635095a android: Fully use raw FS access on vanilla builds 2026-03-08 18:46:09 +00:00
PabloMK7
0c624f16a7
core: Stub AC::CancelConnectAsync (#1846) 2026-03-08 19:26:39 +01:00
PabloMK7
d1d14cef79
core: Disable BOSS for enabled LLE online services (#1847) 2026-03-08 19:24:52 +01:00
PabloMK7
7d5da9eaeb
core: Fix application jump parameters (#1845) 2026-03-08 18:41:31 +01:00
lannoene
abc1980418
Add DLP:SRVR + misc bug fixes (#1828)
* Add DLP:SRVR + add friend code seed hack for LM1 + add multiple filters in IPC debugger + fix cia_container smdh offset not being applied, possible IF statement underflowing + default initialize boss variables + fix IPC header asserts in AM functions + add extra debug info to IPC param assert

* Make server & client more resistant to high ping conditions

* Remove DLP from list of online recommended modules

* Fix license headers + fix clang formatting + fix server create network assert

* Fix recorder.cpp license header
2026-03-08 15:48:09 +01:00
Fausto Núñez Alberro
70c9e18eea
libretro: enable VK_EXT_custom_border_color extension (#1825)
Fixes crash on startup with Vulkan renderer. The extension and its
features must be enabled during device creation for samplers using
custom border colors to work.

Fixes #1824
2026-03-07 21:56:44 +01:00
PabloMK7
46ca83cc36
core: Enable LLE CECD and BOSS when online LLE modules are enabled (#1842) 2026-03-07 21:11:45 +01:00
PabloMK7
1e0df67cc4
file_util: Fix file behaviour on Windows (#1841) 2026-03-07 20:33:29 +01:00
PabloMK7
ced1ec0112
core: nwm: Implement NWM_SOC::GetMACAddress (#1840) 2026-03-07 17:53:10 +01:00
Fausto Núñez Alberro
1b41c78afc
libretro: expose large_screen_proportion as a core option (#1833)
Adds the large_screen_proportion setting to libretro core options,
allowing users to adjust the ratio between the large and small screens
in the "Large Screen, Small Screen" layout.

This is useful on devices like the Steam Deck where the default 4x ratio
makes the small screen too tiny to be practical.

Values range from 1.00x to 6.00x in 0.25 increments.

Closes #1832
2026-03-07 16:44:27 +01:00
PabloMK7
748b97ac5e
core: ndm: Implement suspend daemons count (#1839) 2026-03-07 14:26:40 +01:00
PabloMK7
2207be30a9
core: Add "toggle unique data console type" option (#1826) 2026-03-06 01:23:35 +01:00
PabloMK7
8d284aeccf
video_core: Fix a few vulkan validation issues (#1818) 2026-03-04 21:05:22 +01:00
jbm11208
d49aa070fd
qt: Always receive camera data from UI thread (#1812)
* Make camera functions thread-safe

* Revert redundant changes to qt_multimedia_camera.h and add comments to qt_camera_base.cpp
2026-03-04 16:58:20 +01:00
OpenSauce04
92cd488754 Move version numbers to end of release file filenames 2026-03-04 14:48:04 +00:00
OpenSauce04
efccedbbd2 cmake: Version info generation improvements
- Allow GIT-COMMIT and GIT-TAG files to override real git info (useful for testing version-related functionality such as update checks)
- Always re-configure scm_rev.cpp when configuring with CMake (fixes an issue where the version number would just not update in incremental builds)
2026-03-04 11:45:55 +00:00
OpenSauce04
93e831decb android: Fixed invalid default config content, resulting in a crash 2026-03-03 18:34:53 +00:00
OpenSauce04
d6fadff3ee Updated translations via Transifex 2026-03-03 16:30:03 +00:00
OpenSauce04
af980b4117 Remove _android suffix from Android libretro core filename 2026-03-03 16:26:08 +00:00
OpenSauce04
f23e296802 ci: Libretro artifacts are now packed within properly named archives 2026-03-03 16:04:12 +00:00
OpenSauce04
a0ab331928 ci: Use macOS 26 runner for Intel build 2026-03-02 23:27:32 +00:00
OpenSauce
068d6598bc
Configuration backend improvements Pt. 1 (#1762)
* Convert all setting keys in settings.h into hana strings

* Derive libretro setting keys from common/settings.h hana strings

* settings.h: Reduce code repetition in key definitions via macro

* Implemented mechanism to pass our C++ setting keys to Android/Kotlin

None of the Android keys have been moved over as of this commit, this is just prep work

* jni_settings_keys.cpp.in: Removed redundant code

* Migrate (almost) all Android setting string keys over to SettingKeys

Also some slight cleanup

* Updated license headers

* Fixed top custom width erroneously being used in place of top custom height

* Migrate (probably) all setting string keys to Settings::QKeys

* Migrated several previously missed string keys to generated keys

* SettingKeys.kt: Visually seperate shared and Android-exclusive keys, similar to GenerateSettingKeys.cmake

* android: sdl2_config --> android_config

Not sure why these values are named this way. Hold-over from SDL2 frontend?

* android: Generate and validate default config.ini dynamically

* Settings: Assume C-style string keys by default

Relative to the previous commit, the following names have changed:
- Settings::Keys --> Settings::HKeys
- Settings::CKeys --> Settings::Keys

* default_ini.h: Fixed formatting warning

* Comment cleanup

* android: Fixed compilation failure due to incorrect namespace

* config.cpp: Use ASSERT_MSG instead of LOG_ERROR and ASSERT(false)
2026-03-02 23:26:43 +00:00
TeamPuzel
fe2f637467 Fix UI freeze on macOS game-list population
it seems like the initial call to `GameList::PopulateAsync` in the window constructor is too early in the app life-cycle and doesn't run asynchronously, or Qt is using macOS API in an esoteric way, or maybe it's something else entirely.

Moving the list population to happen before the window is shown instead appears to fix the issue.
2026-03-02 22:55:40 +00:00
PabloMK7
0f9d5f29f3
video_core: Move shader and pipeline compilation into separate workers (#1802) 2026-03-01 23:09:13 +01:00
Eric Warmenhoven
cb09d1e064
libretro: add portability_subset extension if required (#1791) 2026-02-28 15:23:03 +01:00
Richard
526d9d4cea
android: Add auto-map controller button with long-press to clear all bindings (#1769) 2026-02-27 19:57:41 +01:00
David Griswold
b477ba09c1
Ability to select which layouts to cycle with the cycle layout hotkey (#1430) 2026-02-27 13:21:53 +01:00
David Griswold
6b2ac400eb
Android: Hotkey Enable Button (#1464) 2026-02-26 18:40:42 +01:00
David Griswold
5ac0ef8fde
hide portrait layout menu on landscape and vice versa (#1473) 2026-02-26 14:27:18 +01:00
David Griswold
3255620934
Implement integer scaling (#1400) 2026-02-26 13:33:13 +01:00
keynote
03d62efe13
artic_base_client: Fix high cpu usage (#1789)
Fixes high CPU usage by adding a small sleep to the Client::Read and Client::Write methods
2026-02-25 21:34:52 +01:00
PabloMK7
7d19679cc5
video_core: vk_texture_runtime: Refactor and fix resource leak (#1790) 2026-02-25 19:53:03 +01:00
PabloMK7
b3fd0b6c89
video_core: Apply texture filter to color surfaces (#1784) 2026-02-25 13:08:18 +01:00
RedBlackAka
15bdd27b9c
citra-meta: Use dedicated GPU by default on AMD (#1783) 2026-02-24 18:06:29 +01:00
OpenSauce04
d721cbe29b cmake: Only add catch2 library if ENABLE_TESTS is enabled 2026-02-23 23:13:21 +00:00
Eric Warmenhoven
17f4c52e56 libretro: default system type to New 3DS 2026-02-23 22:49:02 +00:00
Eric Warmenhoven
27c3e0e5c3 libretro: better safety on vkDevice feature checks 2026-02-23 22:49:02 +00:00
Eric Warmenhoven
8fac24d2a4 libretro: better load failure check 2026-02-23 22:49:02 +00:00
Eric Warmenhoven
fe59958b63 older tvos hardware does not support layered rendering 2026-02-23 22:49:02 +00:00
RedBlackAka
13e0fdeac1
libretro core: Add some ifdefs (#1765) 2026-02-23 21:35:02 +01:00
OpenSauce04
76db4b08f6 .gitlab-ci.yml: Fixed ARM64 macOS not having minimum OS correctly set 2026-02-23 18:25:05 +00:00
OpenSauce04
5d583a8a41 .gitlab-ci.yml: Bump macOS libretro core minimum OS version to 11.0 2026-02-23 15:57:48 +00:00
PabloMK7
8b72dcb235
video_core: Do not spam file IO when reading vulkan shader disk cache (#1774) 2026-02-23 15:45:26 +01:00
PabloMK7
fcb345e273
logging: Check filter before log format (#1773) 2026-02-23 14:26:20 +01:00
PabloMK7
4c054ff2e7
video_core: Fix transferability issue in vulkan shader disk cache (#1770) 2026-02-22 22:41:24 +01:00
lannoene
43cecd1692
Update File Core and Add HLE DLP Client (#1741) 2026-02-22 17:07:24 +01:00
OpenSauce04
ac0ec5edea Updated translations via Transifex 2026-02-21 17:17:12 +00:00
Chase Harkcom
c55165e19b
Fix segfault when resetting default settings (#1751) 2026-02-20 23:40:39 +01:00
jbm11208
1092295f2a
Fix Shadow Rendering / Texture Filtering (#1675)
* video_core/renderer_vulkan: Add texture filtering

* Fix Shadow Rendering (again...)

* Make individual image views per res scale

* Refactor texture runtime

* Fix some magic numbers

* More fixes and filter pipeline cache.

* Refactor Surface and Handle move and destructor

---------

Co-authored-by: PabloMK7 <hackyglitch2@gmail.com>
2026-02-20 23:39:04 +01:00
RedBlackAka
9628300ff5
citra_meta: Use integrated SSE4.2 detection method (#1753) 2026-02-20 21:34:21 +01:00
RedBlackAka
4010f4bc1f
common/cpu_detect: Remove SSE/SSE2 detection (#1754) 2026-02-20 21:34:03 +01:00
OpenSauce04
f3fb0b729e Kill SDL2 frontend
Good riddance
2026-02-20 16:02:41 +00:00
Alexandre Bouvier
7bcbf8aba4 cmake: fix import name
Some checks failed
citra-build / source (push) Has been cancelled
citra-build / linux (appimage) (push) Has been cancelled
citra-build / linux (appimage-wayland) (push) Has been cancelled
citra-build / linux (fresh) (push) Has been cancelled
citra-build / macos (arm64) (push) Has been cancelled
citra-build / macos (x86_64) (push) Has been cancelled
citra-build / windows (msvc) (push) Has been cancelled
citra-build / windows (msys2) (push) Has been cancelled
citra-build / android (googleplay) (push) Has been cancelled
citra-build / android (vanilla) (push) Has been cancelled
citra-build / docker (push) Has been cancelled
citra-format / clang-format (push) Has been cancelled
citra-libretro / android (push) Has been cancelled
citra-libretro / linux (push) Has been cancelled
citra-libretro / windows (push) Has been cancelled
citra-libretro / macos (arm64) (push) Has been cancelled
citra-libretro / macos (x86_64) (push) Has been cancelled
citra-libretro / ios (push) Has been cancelled
citra-libretro / tvos (push) Has been cancelled
citra-transifex / transifex (push) Has been cancelled
citra-build / macos-universal (push) Has been cancelled
azahar-stale / stale-issues (push) Has been cancelled
2026-02-20 14:41:31 +00:00
Eric Warmenhoven
d9b77cc21e
Implement libretro core (#1215)
Some checks are pending
citra-build / source (push) Waiting to run
citra-build / linux (appimage) (push) Waiting to run
citra-build / linux (appimage-wayland) (push) Waiting to run
citra-build / linux (fresh) (push) Waiting to run
citra-build / macos (arm64) (push) Waiting to run
citra-build / macos (x86_64) (push) Waiting to run
citra-build / macos-universal (push) Blocked by required conditions
citra-build / windows (msvc) (push) Waiting to run
citra-build / windows (msys2) (push) Waiting to run
citra-build / android (googleplay) (push) Waiting to run
citra-build / android (vanilla) (push) Waiting to run
citra-build / docker (push) Waiting to run
citra-format / clang-format (push) Waiting to run
citra-libretro / android (push) Waiting to run
citra-libretro / linux (push) Waiting to run
citra-libretro / windows (push) Waiting to run
citra-libretro / macos (arm64) (push) Waiting to run
citra-libretro / macos (x86_64) (push) Waiting to run
citra-libretro / ios (push) Waiting to run
citra-libretro / tvos (push) Waiting to run
citra-transifex / transifex (push) Waiting to run
* libretro core

* Bringing citra libretro implementation over
* libretro: hook up vulkan renderer
* libretro: github actions
* libretro: gyro
* libretro: core options v2
* libretro: on ios turn off shader jit if unavailable
* moltenvk 1.3.0 introduces 8-bit indexes but allocates 16-bit for metal; this ends up allocating stream buffer * 2 = 132MiB. Instead, just use 16-bit indexes. (This will be necessary for standalone when bumping moltenvk version.)

* libretro core: address review feedback

* libretro: microphone support

* cmake: Add ENABLE_ROOM_STANDALONE to list of incompatible libretro flags

* libretro: proper initial geometry

* libretro: fix software renderer

* libretro: address review feedback

* .github/libretro.yml: Pin macOS runners at macOS 26

* ci: Remove explicit selection of Xcode 16.0

* .github/libretro.yml: remove unnecessary windows builder apt commands

* .github/libretro.yml: bump min macos version to 11.0

* ci: Re-enable CI jobs for all libretro cores

This is under the condition that we don't introduce build cache for these builds

---------

Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
Co-authored-by: PabloMK7 <hackyglitch2@gmail.com>
2026-02-19 22:30:25 +00:00
OpenSauce04
d0eaf07a40 cmake: Added missing newline to missing submodule message 2026-02-17 15:25:54 +00:00
OpenSauce04
354f5d698f Updated translations via Transifex 2026-02-17 13:36:36 +00:00
OpenSauce04
5c6b23c64d tools: Added enter-docker-dev-container.sh script 2026-02-17 13:32:01 +00:00
PabloMK7
c43f24e489
video_core: Fixes to vulkan disk cache (#1748) 2026-02-17 14:22:48 +01:00
RedBlackAka
5d4aef81fe
common/cpu_detect: Remove FMA4 detection (#1746) 2026-02-17 13:21:21 +01:00
RedBlackAka
6c6dd68780
Windows: Fix game shortcut character corruption (#1745) 2026-02-17 10:32:01 +01:00
PabloMK7
304db9173b
video_core: vulkan: Add disk shader cache (#1725)
Some checks failed
citra-build / source (push) Has been cancelled
citra-build / linux (appimage) (push) Has been cancelled
citra-build / linux (appimage-wayland) (push) Has been cancelled
citra-build / linux (fresh) (push) Has been cancelled
citra-build / macos (arm64) (push) Has been cancelled
citra-build / macos (x86_64) (push) Has been cancelled
citra-build / windows (msvc) (push) Has been cancelled
citra-build / windows (msys2) (push) Has been cancelled
citra-build / android (googleplay) (push) Has been cancelled
citra-build / android (vanilla) (push) Has been cancelled
citra-build / docker (push) Has been cancelled
citra-format / clang-format (push) Has been cancelled
citra-transifex / transifex (push) Has been cancelled
citra-build / macos-universal (push) Has been cancelled
2026-02-16 15:59:22 +01:00
PabloMK7
91abe7f7d0
common: Add NATVIS to BitField class for better VS debugging (#1731)
Some checks failed
citra-build / source (push) Has been cancelled
citra-build / linux (appimage) (push) Has been cancelled
citra-build / linux (appimage-wayland) (push) Has been cancelled
citra-build / linux (fresh) (push) Has been cancelled
citra-build / macos (arm64) (push) Has been cancelled
citra-build / macos (x86_64) (push) Has been cancelled
citra-build / windows (msvc) (push) Has been cancelled
citra-build / windows (msys2) (push) Has been cancelled
citra-build / android (googleplay) (push) Has been cancelled
citra-build / android (vanilla) (push) Has been cancelled
citra-build / docker (push) Has been cancelled
citra-format / clang-format (push) Has been cancelled
citra-transifex / transifex (push) Has been cancelled
citra-build / macos-universal (push) Has been cancelled
azahar-stale / stale-issues (push) Has been cancelled
2026-02-13 14:30:04 +01:00
PabloMK7
3e27010c7b
Fix regex in PR verification
Some checks are pending
citra-build / source (push) Waiting to run
citra-build / linux (appimage) (push) Waiting to run
citra-build / linux (appimage-wayland) (push) Waiting to run
citra-build / linux (fresh) (push) Waiting to run
citra-build / macos (arm64) (push) Waiting to run
citra-build / macos (x86_64) (push) Waiting to run
citra-build / macos-universal (push) Blocked by required conditions
citra-build / windows (msvc) (push) Waiting to run
citra-build / windows (msys2) (push) Waiting to run
citra-build / android (googleplay) (push) Waiting to run
citra-build / android (vanilla) (push) Waiting to run
citra-build / docker (push) Waiting to run
citra-format / clang-format (push) Waiting to run
citra-transifex / transifex (push) Waiting to run
2026-02-09 23:30:15 +01:00
OpenSauce04
0c478d7614 Add "Enable display refresh rate detection" setting on desktop 2026-02-09 20:08:34 +00:00
OpenSauce04
37e688f82d qt: Fixed some setting tooltips not having automatic line breaks
Some checks failed
citra-build / source (push) Has been cancelled
citra-build / linux (appimage) (push) Has been cancelled
citra-build / linux (appimage-wayland) (push) Has been cancelled
citra-build / linux (fresh) (push) Has been cancelled
citra-build / macos (arm64) (push) Has been cancelled
citra-build / macos (x86_64) (push) Has been cancelled
citra-build / windows (msvc) (push) Has been cancelled
citra-build / windows (msys2) (push) Has been cancelled
citra-build / android (googleplay) (push) Has been cancelled
citra-build / android (vanilla) (push) Has been cancelled
citra-build / docker (push) Has been cancelled
citra-format / clang-format (push) Has been cancelled
citra-transifex / transifex (push) Has been cancelled
citra-build / macos-universal (push) Has been cancelled
2026-02-06 20:00:38 +00:00
PabloMK7
f010863ece
video_core: vulkan: Only store hashes in shader cache maps (#1710)
Some checks failed
citra-build / source (push) Has been cancelled
citra-build / linux (appimage) (push) Has been cancelled
citra-build / linux (appimage-wayland) (push) Has been cancelled
citra-build / linux (fresh) (push) Has been cancelled
citra-build / macos (arm64) (push) Has been cancelled
citra-build / macos (x86_64) (push) Has been cancelled
citra-build / windows (msvc) (push) Has been cancelled
citra-build / windows (msys2) (push) Has been cancelled
citra-build / android (googleplay) (push) Has been cancelled
citra-build / android (vanilla) (push) Has been cancelled
citra-build / docker (push) Has been cancelled
citra-format / clang-format (push) Has been cancelled
citra-transifex / transifex (push) Has been cancelled
citra-build / macos-universal (push) Has been cancelled
2026-02-02 18:25:03 +01:00
OpenSauce04
dd65ef4749 Updated translations via Transifex
Some checks failed
citra-build / source (push) Has been cancelled
citra-build / linux (appimage) (push) Has been cancelled
citra-build / linux (appimage-wayland) (push) Has been cancelled
citra-build / linux (fresh) (push) Has been cancelled
citra-build / macos (arm64) (push) Has been cancelled
citra-build / macos (x86_64) (push) Has been cancelled
citra-build / windows (msvc) (push) Has been cancelled
citra-build / windows (msys2) (push) Has been cancelled
citra-build / android (googleplay) (push) Has been cancelled
citra-build / android (vanilla) (push) Has been cancelled
citra-build / docker (push) Has been cancelled
citra-format / clang-format (push) Has been cancelled
citra-transifex / transifex (push) Has been cancelled
citra-build / macos-universal (push) Has been cancelled
2026-01-29 12:17:35 +00:00
PabloMK7
fc137b0229
frontend: Revert removal of .3ds support (#1701)
* frontend: Revert removal of .3ds support

* Added 3ds extension to Info.plist

* Added .3ds extension to org.azahar_emu.Azahar.xml

* game_list.h: Removed leftover definitions

---------

Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
2026-01-29 12:10:02 +00:00
lannoene
a9923b6844
core: small fixes to local play (#1690)
Some checks failed
citra-build / source (push) Has been cancelled
citra-build / linux (appimage) (push) Has been cancelled
citra-build / linux (appimage-wayland) (push) Has been cancelled
citra-build / linux (fresh) (push) Has been cancelled
citra-build / macos (arm64) (push) Has been cancelled
citra-build / macos (x86_64) (push) Has been cancelled
citra-build / windows (msvc) (push) Has been cancelled
citra-build / windows (msys2) (push) Has been cancelled
citra-build / android (googleplay) (push) Has been cancelled
citra-build / android (vanilla) (push) Has been cancelled
citra-build / docker (push) Has been cancelled
citra-format / clang-format (push) Has been cancelled
citra-transifex / transifex (push) Has been cancelled
citra-build / macos-universal (push) Has been cancelled
2026-01-27 10:30:26 +01:00
OpenSauce04
0a705b7449 Updated translations via Transifex
Some checks failed
citra-build / source (push) Has been cancelled
citra-build / linux (appimage) (push) Has been cancelled
citra-build / linux (appimage-wayland) (push) Has been cancelled
citra-build / linux (fresh) (push) Has been cancelled
citra-build / macos (arm64) (push) Has been cancelled
citra-build / macos (x86_64) (push) Has been cancelled
citra-build / windows (msvc) (push) Has been cancelled
citra-build / windows (msys2) (push) Has been cancelled
citra-build / android (googleplay) (push) Has been cancelled
citra-build / android (vanilla) (push) Has been cancelled
citra-build / docker (push) Has been cancelled
citra-format / clang-format (push) Has been cancelled
citra-transifex / transifex (push) Has been cancelled
citra-build / macos-universal (push) Has been cancelled
2026-01-25 00:58:03 +00:00
OpenSauce04
7c4c77becf externals: Updated SDL to 2.32.10 2026-01-25 00:43:32 +00:00
OpenSauce04
951b556a2c ci: Only build appimage-wayland for tagged jobs
This is to reduce stress on the CI/CD build queue and cache
2026-01-25 00:41:25 +00:00
David Griswold
a6688abcf5 update language and use UP instead of DOWN on axis mapping 2026-01-24 23:31:01 +00:00
OpenSauce04
6d72a6f447 plgldr: Fixed plugins that should load for all titles failing to load due to a missing check 2026-01-24 22:59:59 +00:00
PabloMK7
f1fa564733
Revert: video_core/renderer_vulkan: Add texture filtering (#1678) 2026-01-24 23:48:54 +01:00
Immersion95
ad9ab71301
qt: Mention in the tooltip that async presentation adds 1 frame of input lag (#1669)
Some checks failed
citra-build / source (push) Has been cancelled
citra-build / linux (appimage) (push) Has been cancelled
citra-build / linux (appimage-wayland) (push) Has been cancelled
citra-build / linux (fresh) (push) Has been cancelled
citra-build / macos (arm64) (push) Has been cancelled
citra-build / macos (x86_64) (push) Has been cancelled
citra-build / windows (msvc) (push) Has been cancelled
citra-build / windows (msys2) (push) Has been cancelled
citra-build / android (googleplay) (push) Has been cancelled
citra-build / android (vanilla) (push) Has been cancelled
citra-build / docker (push) Has been cancelled
citra-format / clang-format (push) Has been cancelled
citra-transifex / transifex (push) Has been cancelled
citra-build / macos-universal (push) Has been cancelled
Async Presentation (Vulkan) adds 1 frame of input lag - Mention in the tooltip
2026-01-23 15:35:59 +01:00
PabloMK7
93f54be3f9 video_core: Fix custom textures when loading a savestate
Some checks are pending
citra-build / source (push) Waiting to run
citra-build / linux (appimage) (push) Waiting to run
citra-build / linux (appimage-wayland) (push) Waiting to run
citra-build / linux (fresh) (push) Waiting to run
citra-build / macos (arm64) (push) Waiting to run
citra-build / macos (x86_64) (push) Waiting to run
citra-build / macos-universal (push) Blocked by required conditions
citra-build / windows (msvc) (push) Waiting to run
citra-build / windows (msys2) (push) Waiting to run
citra-build / android (googleplay) (push) Waiting to run
citra-build / android (vanilla) (push) Waiting to run
citra-build / docker (push) Waiting to run
citra-format / clang-format (push) Waiting to run
citra-transifex / transifex (push) Waiting to run
2026-01-22 19:42:21 +01:00
PabloMK7
95a6814752 video_core: Make all state dirty after loading a savestate 2026-01-22 19:42:21 +01:00
PabloMK7
b0fea112e8 core: Fix accurate multiplication loading (home menu and savestate)
Some checks are pending
citra-build / source (push) Waiting to run
citra-build / linux (appimage) (push) Waiting to run
citra-build / linux (appimage-wayland) (push) Waiting to run
citra-build / linux (fresh) (push) Waiting to run
citra-build / macos (arm64) (push) Waiting to run
citra-build / macos (x86_64) (push) Waiting to run
citra-build / macos-universal (push) Blocked by required conditions
citra-build / windows (msvc) (push) Waiting to run
citra-build / windows (msys2) (push) Waiting to run
citra-build / android (googleplay) (push) Waiting to run
citra-build / android (vanilla) (push) Waiting to run
citra-build / docker (push) Waiting to run
citra-format / clang-format (push) Waiting to run
citra-transifex / transifex (push) Waiting to run
2026-01-22 15:11:38 +01:00
PabloMK7
94b558d3f1 hacks: Force accurate multiplication for M&L Paper Jam 2026-01-22 15:11:38 +01:00
PabloMK7
6e666a1831
log: Fix compilation with gcc backtrace (#1668) 2026-01-22 14:45:30 +01:00
PabloMK7
b54911a52e
github: Ignore punctuation marks in PR verification 2026-01-22 13:47:25 +01:00
David Griswold
102f7d24fc
android: prioritize non-built-in displays (#1667) 2026-01-22 12:55:58 +01:00
OpenSauce
a5ac24adc5
qt: Workaround for Qt directoryChanged event spam on macOS (#1665)
Some checks are pending
citra-build / source (push) Waiting to run
citra-build / linux (appimage) (push) Waiting to run
citra-build / linux (appimage-wayland) (push) Waiting to run
citra-build / linux (fresh) (push) Waiting to run
citra-build / macos (arm64) (push) Waiting to run
citra-build / macos (x86_64) (push) Waiting to run
citra-build / macos-universal (push) Blocked by required conditions
citra-build / windows (msvc) (push) Waiting to run
citra-build / windows (msys2) (push) Waiting to run
citra-build / android (googleplay) (push) Waiting to run
citra-build / android (vanilla) (push) Waiting to run
citra-build / docker (push) Waiting to run
citra-format / clang-format (push) Waiting to run
citra-transifex / transifex (push) Waiting to run
* qt: Workaround for Qt directoryChanged event spam on macOS

* Use steady_clock instead of system_clock
2026-01-21 21:15:49 +00:00
OpenSauce04
3fdcd6b7dc qt: Implement Update Channel setting 2026-01-21 19:49:10 +00:00
PabloMK7
46429f9c02
Update PR template to use master branch 2026-01-21 18:02:11 +01:00
PabloMK7
a4c3135bf3
github: Add first time contributor verification (#1663) 2026-01-21 18:00:37 +01:00
OpenSauce04
d48d51828e android: Fix desynced default value for use_vsync setting
Some checks are pending
citra-build / source (push) Waiting to run
citra-build / linux (appimage) (push) Waiting to run
citra-build / linux (appimage-wayland) (push) Waiting to run
citra-build / linux (fresh) (push) Waiting to run
citra-build / macos (arm64) (push) Waiting to run
citra-build / macos (x86_64) (push) Waiting to run
citra-build / macos-universal (push) Blocked by required conditions
citra-build / windows (msvc) (push) Waiting to run
citra-build / windows (msys2) (push) Waiting to run
citra-build / android (googleplay) (push) Waiting to run
citra-build / android (vanilla) (push) Waiting to run
citra-build / docker (push) Waiting to run
citra-format / clang-format (push) Waiting to run
citra-transifex / transifex (push) Waiting to run
2026-01-20 22:51:03 +00:00
Marcin Serwin
79d73bbcb9 citra_meta: search for Qt6::GuiPrivate before using it
Some checks are pending
citra-build / source (push) Waiting to run
citra-build / linux (appimage) (push) Waiting to run
citra-build / linux (appimage-wayland) (push) Waiting to run
citra-build / linux (fresh) (push) Waiting to run
citra-build / macos (arm64) (push) Waiting to run
citra-build / macos (x86_64) (push) Waiting to run
citra-build / macos-universal (push) Blocked by required conditions
citra-build / windows (msvc) (push) Waiting to run
citra-build / windows (msys2) (push) Waiting to run
citra-build / android (googleplay) (push) Waiting to run
citra-build / android (vanilla) (push) Waiting to run
citra-build / docker (push) Waiting to run
citra-format / clang-format (push) Waiting to run
citra-transifex / transifex (push) Waiting to run
Otherwise, the configuration on darwin fails for me with the following
error:

```
CMake Error at src/citra_meta/CMakeLists.txt:64 (target_link_libraries):
  Target "citra_meta" links to:

    Qt6::GuiPrivate

  but the target was not found.
```

Signed-off-by: Marcin Serwin <marcin@serwin.dev>
2026-01-20 21:33:41 +00:00
RedBlackAka
e9846de5be
qt: Exclude some features logic if disabled at compile time (#1630)
* Qt: Exclude more logic if disabled

* Fix license

* Fix

* Exclude stress test logic

* Fix RequestStop
2026-01-20 18:31:56 +01:00
Cool Guy
671faf8dca
qt: Do not preload textures when custom textures are off (#1629)
Some checks are pending
citra-build / source (push) Waiting to run
citra-build / linux (appimage) (push) Waiting to run
citra-build / linux (appimage-wayland) (push) Waiting to run
citra-build / linux (fresh) (push) Waiting to run
citra-build / macos (arm64) (push) Waiting to run
citra-build / macos (x86_64) (push) Waiting to run
citra-build / macos-universal (push) Blocked by required conditions
citra-build / windows (msvc) (push) Waiting to run
citra-build / windows (msys2) (push) Waiting to run
citra-build / android (googleplay) (push) Waiting to run
citra-build / android (vanilla) (push) Waiting to run
citra-build / docker (push) Waiting to run
citra-format / clang-format (push) Waiting to run
citra-transifex / transifex (push) Waiting to run
2026-01-20 15:07:54 +01:00
Francesco Saltori
c7e0364342
cheats: Fix previous cheats not being cleaned up when a game has no cheats file (#1640) 2026-01-20 14:38:44 +01:00
OpenSauce04
0805711cba Updated translations via Transifex
Some checks failed
citra-transifex / transifex (push) Waiting to run
citra-build / source (push) Has been cancelled
citra-build / linux (appimage) (push) Has been cancelled
citra-build / linux (appimage-wayland) (push) Has been cancelled
citra-build / linux (fresh) (push) Has been cancelled
citra-build / macos (arm64) (push) Has been cancelled
citra-build / macos (x86_64) (push) Has been cancelled
citra-build / windows (msvc) (push) Has been cancelled
citra-build / windows (msys2) (push) Has been cancelled
citra-build / android (googleplay) (push) Has been cancelled
citra-build / android (vanilla) (push) Has been cancelled
citra-build / docker (push) Has been cancelled
citra-format / clang-format (push) Has been cancelled
citra-build / macos-universal (push) Has been cancelled
2026-01-19 21:17:42 +00:00
OpenSauce04
54f2221ec6 cmake: Fix build info sometimes not generating correctly 2026-01-19 21:13:18 +00:00
PabloMK7
f71844b87f log: Close file after stop and fallback to stderr 2026-01-19 21:01:55 +00:00
PabloMK7
6634b8c9d9 citra_meta: Move DetachedTasks construction to main.cpp 2026-01-19 21:01:55 +00:00
OpenSauce04
64516e6420 cmake: Removed redundant BuildInstaller.cmake 2026-01-19 18:48:13 +00:00
OpenSauce04
7869f1c618 cmake: Upgrade bundled Qt to 6.9.3 2026-01-19 17:51:38 +00:00
jbm11208
0571187bd3
video_core/renderer_vulkan: fix shadow rendering (#1634)
Some checks are pending
citra-build / source (push) Waiting to run
citra-build / linux (appimage) (push) Waiting to run
citra-build / linux (appimage-wayland) (push) Waiting to run
citra-build / linux (fresh) (push) Waiting to run
citra-build / macos (arm64) (push) Waiting to run
citra-build / macos (x86_64) (push) Waiting to run
citra-build / macos-universal (push) Blocked by required conditions
citra-build / windows (msvc) (push) Waiting to run
citra-build / windows (msys2) (push) Waiting to run
citra-build / android (googleplay) (push) Waiting to run
citra-build / android (vanilla) (push) Waiting to run
citra-build / docker (push) Waiting to run
citra-format / clang-format (push) Waiting to run
citra-transifex / transifex (push) Waiting to run
2026-01-18 23:56:35 +01:00
OpenSauce
4deb7e63b5
DirectoryInitialization.kt: Switch to getFilesDir for internalUserPath (#1646)
Some checks are pending
citra-build / source (push) Waiting to run
citra-build / linux (appimage) (push) Waiting to run
citra-build / linux (appimage-wayland) (push) Waiting to run
citra-build / linux (fresh) (push) Waiting to run
citra-build / macos (arm64) (push) Waiting to run
citra-build / macos (x86_64) (push) Waiting to run
citra-build / macos-universal (push) Blocked by required conditions
citra-build / windows (msvc) (push) Waiting to run
citra-build / windows (msys2) (push) Waiting to run
citra-build / android (googleplay) (push) Waiting to run
citra-build / android (vanilla) (push) Waiting to run
citra-build / docker (push) Waiting to run
citra-format / clang-format (push) Waiting to run
citra-transifex / transifex (push) Waiting to run
* DirectoryInitialization.kt: Switch to getFilesDir for internalUserPath

Was previously getExternalFilesDir

filesDir is used here, which is just recommended shorthand for getFilesDir

* DirectoryInitialization.kt: Updated license header
2026-01-18 18:41:54 +00:00
382 changed files with 36799 additions and 18113 deletions

133
.ci/libretro-ci.yml Normal file
View File

@ -0,0 +1,133 @@
.core-defs:
variables:
JNI_PATH: .
CORENAME: azahar
API_LEVEL: 21
BASE_CORE_ARGS: -DENABLE_LIBRETRO=ON -DENABLE_TESTS=OFF
CORE_ARGS: ${BASE_CORE_ARGS}
EXTRA_PATH: bin/Release
variables:
STATIC_RETROARCH_BRANCH: master
GIT_SUBMODULE_STRATEGY: recursive
# Inclusion templates, required for the build to work
include:
################################## DESKTOPS ############################## ##
# Windows 64-bit
- project: 'libretro-infrastructure/ci-templates'
file: '/windows-cmake-mingw.yml'
# Linux 64-bit
- project: 'libretro-infrastructure/ci-templates'
file: '/linux-cmake.yml'
# MacOS x86_64
- project: 'libretro-infrastructure/ci-templates'
file: '/osx-cmake-x86.yml'
# MacOS ARM64
- project: 'libretro-infrastructure/ci-templates'
file: '/osx-cmake-arm64.yml'
################################## CELLULAR ############################## ##
# Android
- project: 'libretro-infrastructure/ci-templates'
file: '/android-cmake.yml'
# iOS
- project: 'libretro-infrastructure/ci-templates'
file: '/ios-cmake.yml'
# tvOS
- project: 'libretro-infrastructure/ci-templates'
file: '/tvos-cmake.yml'
################################## CONSOLES ############################## ##
# Stages for building
stages:
- build-prepare
- build-shared
- build-static
##############################################################################
#################################### STAGES ##################################
##############################################################################
#
################################### DESKTOPS #################################
# Windows 64-bit
libretro-build-windows-x64:
extends:
- .core-defs
- .libretro-windows-cmake-x86_64
image: $CI_SERVER_HOST:5050/libretro-infrastructure/libretro-build-mxe-win-cross-cores:mingw12
variables:
CORE_ARGS: ${BASE_CORE_ARGS} -DENABLE_LTO=OFF -G Ninja
# Linux 64-bit
libretro-build-linux-x64:
extends:
- .core-defs
- .libretro-linux-cmake-x86_64
image: $CI_SERVER_HOST:5050/libretro-infrastructure/libretro-build-amd64-ubuntu:backports
variables:
CORE_ARGS: ${BASE_CORE_ARGS} -DENABLE_LTO=OFF
CC: /usr/bin/gcc-12
CXX: /usr/bin/g++-12
# MacOS x86_64
libretro-build-osx-x64:
tags:
- mac-apple-silicon
variables:
CORE_ARGS: ${BASE_CORE_ARGS} -DCMAKE_OSX_ARCHITECTURES=x86_64
MACOSX_DEPLOYMENT_TARGET: "11.0"
extends:
- .core-defs
- .libretro-osx-cmake-x86_64
# MacOS ARM64
libretro-build-osx-arm64:
extends:
- .core-defs
- .libretro-osx-cmake-arm64
variables:
MACOSX_DEPLOYMENT_TARGET: "11.0"
################################### CELLULAR #################################
# Android ARMv8a
android-arm64-v8a:
extends:
- .libretro-android-cmake-arm64-v8a
- .core-defs
variables:
ANDROID_NDK_VERSION: 26.2.11394342
NDK_ROOT: /android-sdk-linux/ndk/$ANDROID_NDK_VERSION
LIBNAME: ${CORENAME}_libretro.so
artifacts:
paths:
- $LIBNAME
# iOS arm64
libretro-build-ios-arm64:
extends:
- .libretro-ios-cmake-arm64
- .core-defs
variables:
CORE_ARGS: ${BASE_CORE_ARGS} -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF
IOS_MINVER: "14.0"
EXTRA_PATH: bin/RelWithDebInfo
# tvOS arm64
libretro-build-tvos-arm64:
extends:
- .libretro-tvos-cmake-arm64
- .core-defs
variables:
CORE_ARGS: ${BASE_CORE_ARGS} -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DIOS=ON -DCMAKE_SYSTEM_NAME=tvOS -DCMAKE_OSX_SYSROOT=appletvos -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF
MINVER: "14.0"
EXTRA_PATH: bin/RelWithDebInfo
################################### CONSOLES #################################

13
.ci/libretro-pack.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash -ex
# Determine the full revision name.
GITDATE="`git show -s --date=short --format='%ad' | sed 's/-//g'`"
GITREV="`git show -s --format='%h'`"
REV_NAME="azahar-libretro-$OS-$TARGET-$GITDATE-$GITREV"
if [ "$GITHUB_REF_TYPE" = "tag" ]; then
REV_NAME="azahar-libretro-$OS-$TARGET-$GITHUB_REF_NAME"
fi
# Create .zip
zip -j -9 $REV_NAME.zip $BUILD_DIR/$EXTRA_PATH/azahar_libretro.*

View File

@ -1,6 +1,6 @@
#!/bin/bash -ex
if [[ "$TARGET" == "appimage"* ]]; then
if [[ "$TARGET" == "appimage"* ]] || [[ "$TARGET" == "clang"* ]]; then
# Compile the AppImage we distribute with Clang.
export EXTRA_CMAKE_FLAGS=(-DCMAKE_CXX_COMPILER=clang++
-DCMAKE_C_COMPILER=clang

View File

@ -2,13 +2,14 @@
ARTIFACTS_LIST=($ARTIFACTS)
BUNDLE_DIR=build/bundle
mkdir build
BUILD_DIR=build
UNIVERSAL_DIR=$BUILD_DIR/universal
BUNDLE_DIR=$UNIVERSAL_DIR/bundle
OTHER_BUNDLE_DIR=$BUILD_DIR/x86_64/bundle
# Set up the base artifact to combine into.
BASE_ARTIFACT=${ARTIFACTS_LIST[0]}
BASE_ARTIFACT_ARCH="${BASE_ARTIFACT##*-}"
mv $BASE_ARTIFACT $BUNDLE_DIR
# Set up the base bundle to combine into.
mkdir $UNIVERSAL_DIR
cp -a $BUILD_DIR/arm64/bundle $UNIVERSAL_DIR
# Executable binary paths that need to be combined.
BIN_PATHS=(Azahar.app/Contents/MacOS/azahar)
@ -19,21 +20,18 @@ DYLIB_PATHS=($(cd $BUNDLE_DIR && find . -name '*.dylib'))
unset IFS
# Combine all of the executable binaries and dylibs.
for OTHER_ARTIFACT in "${ARTIFACTS_LIST[@]:1}"; do
OTHER_ARTIFACT_ARCH="${OTHER_ARTIFACT##*-}"
for BIN_PATH in "${BIN_PATHS[@]}"; do
lipo -create -output $BUNDLE_DIR/$BIN_PATH $BUNDLE_DIR/$BIN_PATH $OTHER_BUNDLE_DIR/$BIN_PATH
done
for BIN_PATH in "${BIN_PATHS[@]}"; do
lipo -create -output $BUNDLE_DIR/$BIN_PATH $BUNDLE_DIR/$BIN_PATH $OTHER_ARTIFACT/$BIN_PATH
done
for DYLIB_PATH in "${DYLIB_PATHS[@]}"; do
# Only merge if the libraries do not have conflicting arches, otherwise it will fail.
DYLIB_INFO=`file $BUNDLE_DIR/$DYLIB_PATH`
for DYLIB_PATH in "${DYLIB_PATHS[@]}"; do
# Only merge if the libraries do not have conflicting arches, otherwise it will fail.
DYLIB_INFO=`file $BUNDLE_DIR/$DYLIB_PATH`
OTHER_DYLIB_INFO=`file $OTHER_ARTIFACT/$DYLIB_PATH`
if ! [[ "$DYLIB_INFO" =~ "$OTHER_ARTIFACT_ARCH" ]] && ! [[ "$OTHER_DYLIB_INFO" =~ "$BASE_ARTIFACT_ARCH" ]]; then
lipo -create -output $BUNDLE_DIR/$DYLIB_PATH $BUNDLE_DIR/$DYLIB_PATH $OTHER_ARTIFACT/$DYLIB_PATH
fi
done
OTHER_DYLIB_INFO=`file $OTHER_BUNDLE_DIR/$DYLIB_PATH`
if ! [[ "$DYLIB_INFO" =~ "x86_64" ]] && ! [[ "$OTHER_DYLIB_INFO" =~ "arm64" ]]; then
lipo -create -output $BUNDLE_DIR/$DYLIB_PATH $BUNDLE_DIR/$DYLIB_PATH $OTHER_BUNDLE_DIR/$DYLIB_PATH
fi
done
# Remove leftover libs so that they aren't distributed

View File

@ -4,12 +4,10 @@ if [ "$GITHUB_REF_TYPE" == "tag" ]; then
export EXTRA_CMAKE_FLAGS=(-DENABLE_QT_UPDATE_CHECKER=ON)
fi
mkdir build && cd build
cmake .. -GNinja \
mkdir -p build/$BUILD_ARCH && cd build/$BUILD_ARCH
cmake ../.. -GNinja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_OSX_ARCHITECTURES="$TARGET" \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_OSX_ARCHITECTURES="$BUILD_ARCH" \
-DENABLE_QT_TRANSLATION=ON \
-DENABLE_ROOM_STANDALONE=OFF \
-DUSE_DISCORD_PRESENCE=ON \
@ -18,9 +16,8 @@ ninja
ninja bundle
mv ./bundle/azahar.app ./bundle/Azahar.app # TODO: Can this be done in CMake?
ccache -s -v
CURRENT_ARCH=`arch`
if [ "$TARGET" = "$CURRENT_ARCH" ]; then
if [ "$BUILD_ARCH" = "$CURRENT_ARCH" ]; then
ctest -VV -C Release
fi

View File

@ -3,20 +3,21 @@
# Determine the full revision name.
GITDATE="`git show -s --date=short --format='%ad' | sed 's/-//g'`"
GITREV="`git show -s --format='%h'`"
REV_NAME="azahar-$OS-$TARGET-$GITDATE-$GITREV"
# Determine the name of the release being built.
if [ "$GITHUB_REF_TYPE" = "tag" ]; then
RELEASE_NAME=azahar-$GITHUB_REF_NAME
REV_NAME="azahar-$GITHUB_REF_NAME-$OS-$TARGET"
else
RELEASE_NAME=azahar-head
fi
# Archive and upload the artifacts.
mkdir -p artifacts
function pack_artifacts() {
REV_NAME="azahar-$OS-$TARGET-$GITDATE-$GITREV"
# Determine the name of the release being built.
if [ "$GITHUB_REF_TYPE" = "tag" ]; then
RELEASE_NAME=azahar-$GITHUB_REF_NAME
REV_NAME="azahar-$OS-$TARGET-$GITHUB_REF_NAME"
else
RELEASE_NAME=azahar-head
fi
ARTIFACTS_PATH="$1"
# Set up root directory for archive.
@ -56,11 +57,23 @@ if [ -n "$UNPACKED" ]; then
FILENAME=$(basename "$ARTIFACT")
EXTENSION="${FILENAME##*.}"
# TODO: Deduplicate
REV_NAME="azahar-$OS-$TARGET-$GITDATE-$GITREV"
# Determine the name of the release being built.
if [ "$GITHUB_REF_TYPE" = "tag" ]; then
RELEASE_NAME=azahar-$GITHUB_REF_NAME
REV_NAME="azahar-$OS-$TARGET-$GITHUB_REF_NAME"
else
RELEASE_NAME=azahar-head
fi
mv "$ARTIFACT" "artifacts/$REV_NAME.$EXTENSION"
done
elif [ -n "$PACK_INDIVIDUALLY" ]; then
# Pack and upload the artifacts one-by-one.
for ARTIFACT in build/bundle/*; do
TARGET=$(basename "$ARTIFACT")
pack_artifacts "$ARTIFACT"
done
else

15
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,15 @@
- [ ] I have read the [Azahar AI Policy document](https://github.com/azahar-emu/azahar/blob/master/AI-POLICY.md) and have disclosed any use of AI if applicable under those terms.
---
<!--
If you are contributing to Azahar for the first time please
keep the block of text between `---` and write your
PR description below it. Do not write anything inside
or change this block of text!
If you are a recurrent contributor, remove this entire
block of text and proceed as normal.
-->
![Ignore Until Your PR has been created!](../blob/master/.github/ignore_unless_human.png?raw=true)
---

BIN
.github/ignore_unless_human.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -23,12 +23,60 @@ jobs:
name: source
path: artifacts/
linux:
linux-x86_64:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
target: ["appimage", "appimage-wayland", "fresh"]
target: ["appimage", "appimage-wayland", "gcc-nopch"]
container:
image: opensauce04/azahar-build-environment:latest
options: -u 1001
env:
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_COMPILERCHECK: content
CCACHE_SLOPPINESS: time_macros
OS: linux
TARGET: ${{ matrix.target }}
SHOULD_RUN: ${{ (matrix.target != 'appimage-wayland' || github.ref_type == 'tag') }}
steps:
- uses: actions/checkout@v4
if: ${{ env.SHOULD_RUN == 'true' }}
with:
submodules: recursive
- name: Set up cache
if: ${{ env.SHOULD_RUN == 'true' }}
uses: actions/cache@v4
with:
path: ${{ env.CCACHE_DIR }}
key: ${{ github.job }}-${{ matrix.target }}-${{ github.sha }}
restore-keys: |
${{ github.job }}-${{ matrix.target }}-
- name: Build
if: ${{ env.SHOULD_RUN == 'true' }}
run: ./.ci/linux.sh
- name: Move AppImage to artifacts directory
if: ${{ contains(matrix.target, 'appimage') && env.SHOULD_RUN == 'true' }}
run: |
mkdir -p artifacts
mv build/bundle/*.AppImage artifacts/
- name: Rename AppImage
if: ${{ matrix.target == 'appimage-wayland' && env.SHOULD_RUN == 'true' }}
run: |
mv artifacts/azahar.AppImage artifacts/azahar-wayland.AppImage
- name: Upload
if: ${{ contains(matrix.target, 'appimage') && env.SHOULD_RUN == 'true' }}
uses: actions/upload-artifact@v4
with:
name: ${{ github.job }}-${{ matrix.target }}
path: artifacts/
linux-arm64:
runs-on: ubuntu-24.04-arm
strategy:
fail-fast: false
matrix:
target: ["clang", "gcc-nopch"]
container:
image: opensauce04/azahar-build-environment:latest
options: -u 1001
@ -46,39 +94,19 @@ jobs:
uses: actions/cache@v4
with:
path: ${{ env.CCACHE_DIR }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
key: ${{ github.job }}-${{ matrix.target }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-${{ matrix.target }}-
${{ github.job }}-${{ matrix.target }}-
- name: Build
run: ./.ci/linux.sh
- name: Move AppImage to artifacts directory
if: ${{ contains(matrix.target, 'appimage') }}
run: |
mkdir -p artifacts
mv build/bundle/*.AppImage artifacts/
- name: Rename AppImage
if: ${{ matrix.target == 'appimage-wayland' }}
run: |
mv artifacts/azahar.AppImage artifacts/azahar-wayland.AppImage
- name: Upload
if: ${{ contains(matrix.target, 'appimage') }}
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: artifacts/
macos:
runs-on: ${{ (matrix.target == 'x86_64' && 'macos-15-intel') || 'macos-26' }}
strategy:
fail-fast: false
matrix:
target: ["x86_64", "arm64"]
runs-on: 'macos-26'
env:
CCACHE_DIR: ${{ github.workspace }}/.ccache
CCACHE_COMPILERCHECK: content
CCACHE_SLOPPINESS: time_macros
OS: macos
TARGET: ${{ matrix.target }}
steps:
- uses: actions/checkout@v4
with:
@ -87,58 +115,31 @@ jobs:
uses: actions/cache@v4
with:
path: ${{ env.CCACHE_DIR }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
key: ${{ runner.os }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-${{ matrix.target }}-
${{ runner.os }}-
- name: Install tools
run: brew install ccache ninja spirv-tools
- name: Build
run: ./.ci/macos.sh
- name: Prepare outputs for caching
run: cp -R build/bundle $OS-$TARGET
- name: Cache outputs for universal build
uses: actions/cache/save@v4
with:
path: ${{ env.OS }}-${{ env.TARGET }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
- name: Pack
run: ./.ci/pack.sh
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: artifacts/
macos-universal:
runs-on: macos-26
needs: macos
env:
OS: macos
TARGET: universal
steps:
- uses: actions/checkout@v4
- name: Download x86_64 build from cache
uses: actions/cache/restore@v4
with:
path: ${{ env.OS }}-x86_64
key: ${{ runner.os }}-x86_64-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
fail-on-cache-miss: true
- name: Download ARM64 build from cache
uses: actions/cache/restore@v4
with:
path: ${{ env.OS }}-arm64
key: ${{ runner.os }}-arm64-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
fail-on-cache-miss: true
- name: Build (x86_64)
run: BUILD_ARCH=x86_64 ./.ci/macos.sh
- name: Build (arm64)
run: BUILD_ARCH=arm64 ./.ci/macos.sh
- name: Create universal app
run: ./.ci/macos-universal.sh
env:
ARTIFACTS: ${{ env.OS }}-x86_64 ${{ env.OS }}-arm64
- name: Prepare for packing
run: |
mkdir build/bundle
cp -r build/x86_64/bundle build/bundle/x86_64
cp -r build/arm64/bundle build/bundle/arm64
cp -r build/universal/bundle build/bundle/universal
- name: Pack
env:
PACK_INDIVIDUALLY: 1
run: ./.ci/pack.sh
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
name: ${{ env.OS }}
path: artifacts/
windows:
@ -306,4 +307,4 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: docker
path: artifacts/
path: artifacts/

View File

@ -0,0 +1,51 @@
name: Detect first-time contributors
on:
pull_request_target:
types: [opened]
permissions:
pull-requests: write
issues: write
jobs:
detect:
runs-on: ubuntu-latest
if: >-
(github.repository == 'azahar-emu/azahar') &&
(github.event.pull_request.author_association != 'COLLABORATOR') &&
(github.event.pull_request.author_association != 'CONTRIBUTOR') &&
(github.event.pull_request.author_association != 'MANNEQUIN') &&
(github.event.pull_request.author_association != 'MEMBER') &&
(github.event.pull_request.author_association != 'OWNER')
steps:
- name: Detect PR if author is first-time contributor
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const pr = context.payload.pull_request;
// Add needs verification label so that the reopen action runs on comment.
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pr.number,
labels: ['needs verification'],
});
// Close the pull request and wait for verification.
await github.rest.pulls.update({
owner,
repo,
pull_number: pr.number,
state: 'closed',
});
// Show the new contributor how to verify (they need to write a short poem about the Wii and 3DS being lovers)
await github.rest.issues.createComment({
owner,
repo,
issue_number: pr.number,
body: 'Welcome to the Azahar Emulator repository! Due to the surge of AI bots we have decided to add an extra verification step to new contributors. Please follow the exact instructions in your own written Pull Request description to reopen it.',
});

View File

@ -0,0 +1,79 @@
name: Verify first-time contributors
on:
issue_comment:
types: [created]
permissions:
pull-requests: write
issues: write
jobs:
verify:
runs-on: ubuntu-latest
if: github.event.issue.pull_request && contains(github.event.issue.labels.*.name, 'needs verification')
steps:
- name: Verify and reopen PR
uses: actions/github-script@v7
with:
script: |
const { owner, repo } = context.repo;
const issue = context.payload.issue;
const comment = context.payload.comment;
const { data: pr } = await github.rest.pulls.get({
owner,
repo,
pull_number: issue.number,
});
// Only allow verification of the comment user is the author
if (comment.user.login !== pr.user.login) {
return;
}
// Fetch user display and login names (lowercase)
const { data: user } = await github.rest.users.getByUsername({
username: pr.user.login,
});
const username = pr.user.login.toLowerCase();
const displayName = (user.name || '').toLowerCase();
// Make comment body lowercase and split words
const body = comment.body.toLowerCase().trim().replace(/[^a-z0-9_\-\s]/g, '').split(/\s+/);
// Check that the user verified themselves by writing a song about the NES and the SNES.
const verified =
(body.includes(username) ||
(displayName && body.includes(displayName))) &&
body.includes('azahar');
// Only reopen the PR and remove the label if verification succeeded
if (verified) {
await github.rest.pulls.update({
owner,
repo,
pull_number: issue.number,
state: 'open',
});
await github.rest.issues.createComment({
owner,
repo,
issue_number: issue.number,
body: 'Verification successful! Pull request has been reopened. Please also edit your PR description to remove the block of text between `---` to make the description easier to read.',
});
try {
await github.rest.issues.removeLabel({
owner,
repo,
issue_number: issue.number,
name: 'needs verification',
});
} catch {}
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: issue.number,
body: 'Verification failed! Pull request will remain closed.',
});
}

178
.github/workflows/libretro.yml vendored Normal file
View File

@ -0,0 +1,178 @@
name: citra-libretro
on:
push:
branches: [ "*" ]
tags: [ "*" ]
pull_request:
branches: [ master ]
workflow_dispatch:
env:
CORE_ARGS: -DENABLE_LIBRETRO=ON
jobs:
android:
runs-on: ubuntu-22.04
env:
OS: android
TARGET: arm64-v8a
API_LEVEL: 21
ANDROID_NDK_VERSION: 26.2.11394342
ANDROID_ABI: arm64-v8a
BUILD_DIR: build/android-arm64-v8a
EXTRA_PATH: bin/Release
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set tag name
run: |
if [[ "$GITHUB_REF_TYPE" == "tag" ]]; then
echo "GIT_TAG_NAME=$GITHUB_REF_NAME" >> $GITHUB_ENV
fi
echo $GIT_TAG_NAME
- name: Update Android SDK CMake version
run: |
echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "ndk;$ANDROID_NDK_VERSION"
echo "y" | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager "cmake;3.30.3"
- name: Build
run: |
export NDK_ROOT=${ANDROID_SDK_ROOT}/ndk/$ANDROID_NDK_VERSION
${ANDROID_SDK_ROOT}/cmake/3.30.3/bin/cmake $CORE_ARGS -DANDROID_PLATFORM=android-$API_LEVEL -DCMAKE_TOOLCHAIN_FILE=$NDK_ROOT/build/cmake/android.toolchain.cmake -DANDROID_STL=c++_static -DANDROID_ABI=$ANDROID_ABI . -B $BUILD_DIR
${ANDROID_SDK_ROOT}/cmake/3.30.3/bin/cmake --build $BUILD_DIR --target azahar_libretro --config Release -j $(nproc)
- name: Pack
run: ./.ci/libretro-pack.sh
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ./*.zip
linux:
runs-on: ubuntu-22.04
env:
OS: linux
TARGET: x86_64
BUILD_DIR: build/linux-x86_64
EXTRA_PATH: bin/Release
EXTRA_CORE_ARGS: -DCMAKE_C_COMPILER=gcc-12 -DCMAKE_CXX_COMPILER=g++-12 -DENABLE_LTO=OFF
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Build
run: |
cmake $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR
cmake --build $BUILD_DIR --target azahar_libretro --config Release -j $(nproc)
- name: Pack
run: ./.ci/libretro-pack.sh
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ./*.zip
windows:
runs-on: ubuntu-latest
env:
OS: windows
TARGET: x86_64
BUILD_DIR: build/windows-x86_64
EXTRA_CORE_ARGS: -DENABLE_LTO=OFF -G Ninja
CMAKE: x86_64-w64-mingw32.static-cmake
IMAGE: git.libretro.com:5050/libretro-infrastructure/libretro-build-mxe-win-cross-cores:mingw12
EXTRA_PATH: bin/Release
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Build in cross-container
run: |
docker pull $IMAGE
docker run --rm --user root \
-v "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}" \
-w "${GITHUB_WORKSPACE}" \
$IMAGE \
bash -lc "\
${CMAKE} $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR && \
${CMAKE} --build $BUILD_DIR --target azahar_libretro --config Release -j $(nproc)"
- name: Pack
run: ./.ci/libretro-pack.sh
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ./*.zip
macos:
runs-on: macos-26
strategy:
matrix:
target: ["x86_64", "arm64"]
env:
OS: macos
TARGET: ${{ matrix.target }}
MACOSX_DEPLOYMENT_TARGET: 11.0
BUILD_DIR: build/osx-${{ matrix.target }}
EXTRA_PATH: bin/Release
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Install tools
run: brew install spirv-tools
- name: Build
run: |
cmake $CORE_ARGS -DCMAKE_OSX_ARCHITECTURES=$TARGET . -B $BUILD_DIR
cmake --build $BUILD_DIR --target azahar_libretro --config Release
- name: Pack
run: ./.ci/libretro-pack.sh
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ./*.zip
ios:
runs-on: macos-26
env:
OS: ios
TARGET: arm64
BUILD_DIR: build/ios-arm64
EXTRA_PATH: bin/Release
EXTRA_CORE_ARGS: -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_C_FLAGS=-DIOS -DCMAKE_CXX_FLAGS=-DIOS -DIOS=ON -DCMAKE_SYSTEM_NAME=iOS -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Build
run: |
cmake $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR
cmake --build $BUILD_DIR --target azahar_libretro --config Release
- name: Pack
run: ./.ci/libretro-pack.sh
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ./*.zip
tvos:
runs-on: macos-26
env:
OS: tvos
TARGET: arm64
BUILD_DIR: build/tvos-arm64
EXTRA_PATH: bin/Release
EXTRA_CORE_ARGS: -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_C_FLAGS=-DIOS -DCMAKE_CXX_FLAGS=-DIOS -DIOS=ON -DCMAKE_SYSTEM_NAME=tvOS -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0 -DCITRA_USE_PRECOMPILED_HEADERS=OFF -DCMAKE_OSX_SYSROOT=appletvos -DCMAKE_OSX_ARCHITECTURES=arm64 -DENABLE_OPT=OFF
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Build
run: |
cmake $CORE_ARGS $EXTRA_CORE_ARGS . -B $BUILD_DIR
cmake --build $BUILD_DIR --target azahar_libretro --config Release
- name: Pack
run: ./.ci/libretro-pack.sh
- name: Upload
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: ./*.zip

4
.gitignore vendored
View File

@ -56,3 +56,7 @@ repo/
.ccache/
node_modules/
VULKAN_SDK/
# Version info files
GIT-COMMIT
GIT-TAG

3
.gitmodules vendored
View File

@ -103,3 +103,6 @@
[submodule "externals/xxHash"]
path = externals/xxHash
url = https://github.com/Cyan4973/xxHash.git
[submodule "externals/libretro-common"]
path = externals/libretro-common/libretro-common
url = https://github.com/libretro/libretro-common.git

20
AI-POLICY.md Normal file
View File

@ -0,0 +1,20 @@
# Azahar Emulator AI Use Policy
The following document outlines the acceptable and unacceptable uses of AI within the Azahar codebase.
It describes whether or not submissions which were exposed to large language models (LLMs) such as ChatGPT, Claude, DeepSeek, and similar models would be capable of being merged in a pull request or otherwise utilized.
- ✅ Use of AI to help developers discover or understand problems in the codebase is acceptable **under the condition that any discovered issue is independently verified by a human**.
- ✅ Use of AI to write code snippets of a sufficiently small size that they aren't reasonably copyrightable **with disclosure in the PR description** is acceptable.
- This will be handled on a case-by-case basis and is up to the interpretation of the maintainer, but generic algorithm snippets up to a maximum of approximately 5 lines of code are acceptable.
- ❌ Use of AI to write code for submission without disclosure is prohibited.
- ❌ Use of AI to write the entirety/ a significant portion of a contribution is prohibited.
- ❌ Use of AI to write snippets of code which are of a size such that they could reasonably be copyrightable is prohibited.
- ❌ Use of AI to rewrite incompatibly-licensed code for submission to Azahar is prohibited.
- ❌ Use of AI to autonomously submit pull requests or issues is prohibited.
Pull requests which violate these rules will be closed. Previously accepted submissions which are found to violate these rules will be retroactively removed from the codebase.
This document may be updated in the future if further clarification is required.
This policy is effective for code submitted on or after the 20th of March 2026.

View File

@ -17,20 +17,23 @@ list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/CMakeModules")
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/externals/cmake-modules")
include(DownloadExternals)
include(CMakeDependentOption)
include(FindPkgConfig)
project(citra LANGUAGES C CXX ASM)
# must be invoked after project() command when using CMAKE_TOOLCHAIN_FILE
include(FindPkgConfig)
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin" OR CMAKE_SYSTEM_NAME STREQUAL "iOS")
enable_language(OBJC OBJCXX)
endif()
option(ENABLE_LIBRETRO "Build as a LibRetro core" OFF)
# Some submodules like to pick their own default build type if not specified.
# Make sure we default to Release build type always, unless the generator has custom types.
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Choose the type of build." FORCE)
endif()
if (APPLE)
if (APPLE AND NOT ENABLE_LIBRETRO)
# Silence warnings on empty objects, for example when platform-specific code is #ifdef'd out.
set(CMAKE_C_ARCHIVE_CREATE "<CMAKE_AR> Scr <TARGET> <LINK_FLAGS> <OBJECTS>")
set(CMAKE_CXX_ARCHIVE_CREATE "<CMAKE_AR> Scr <TARGET> <LINK_FLAGS> <OBJECTS>")
@ -90,8 +93,18 @@ else()
set(DEFAULT_ENABLE_OPENGL ON)
endif()
# Track which options were explicitly set by the user (for libretro conflict detection)
set(_LIBRETRO_INCOMPATIBLE_OPTIONS
ENABLE_SDL2 ENABLE_QT ENABLE_WEB_SERVICE ENABLE_SCRIPTING
ENABLE_OPENAL ENABLE_ROOM ENABLE_ROOM_STANDALONE ENABLE_CUBEB ENABLE_LIBUSB)
set(_USER_SET_OPTIONS "")
foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS)
if(DEFINED ${_opt})
list(APPEND _USER_SET_OPTIONS ${_opt})
endif()
endforeach()
option(ENABLE_SDL2 "Enable using SDL2" ON)
CMAKE_DEPENDENT_OPTION(ENABLE_SDL2_FRONTEND "Enable the SDL2 frontend" OFF "ENABLE_SDL2;NOT ANDROID AND NOT IOS" OFF)
option(USE_SYSTEM_SDL2 "Use the system SDL2 lib (instead of the bundled one)" OFF)
# Set bundled qt as dependent options.
@ -130,6 +143,31 @@ option(ENABLE_NATIVE_OPTIMIZATION "Enables processor-specific optimizations via
option(CITRA_USE_PRECOMPILED_HEADERS "Use precompiled headers" ON)
option(CITRA_WARNINGS_AS_ERRORS "Enable warnings as errors" ON)
# Handle incompatible options for libretro builds
if(ENABLE_LIBRETRO)
# Check for explicitly-set conflicting options
set(_CONFLICTS "")
foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS)
list(FIND _USER_SET_OPTIONS ${_opt} _idx)
if(NOT _idx EQUAL -1 AND ${_opt})
list(APPEND _CONFLICTS ${_opt})
endif()
endforeach()
if(_CONFLICTS)
string(REPLACE ";" ", " _CONFLICTS_STR "${_CONFLICTS}")
message(FATAL_ERROR
"ENABLE_LIBRETRO is incompatible with: ${_CONFLICTS_STR}\n"
"These options were explicitly enabled but are not supported for libretro builds.\n"
"Remove these options or set them to OFF.")
endif()
# Force disable incompatible options (handles defaulted-on options)
foreach(_opt IN LISTS _LIBRETRO_INCOMPATIBLE_OPTIONS)
set(${_opt} OFF CACHE BOOL "Disabled for libretro" FORCE)
endforeach()
endif()
# Pass the following values to C++ land
if (ENABLE_QT)
add_definitions(-DENABLE_QT)
@ -143,9 +181,6 @@ endif()
if (ENABLE_SDL2)
add_definitions(-DENABLE_SDL2)
endif()
if (ENABLE_SDL2_FRONTEND)
add_definitions(-DENABLE_SDL2_FRONTEND)
endif()
if(ENABLE_SSE42 AND (CMAKE_SYSTEM_PROCESSOR MATCHES "x86_64|AMD64"))
message(STATUS "SSE4.2 enabled for x86_64")
@ -223,7 +258,7 @@ function(check_submodules_present)
foreach(module ${gitmodules})
string(REGEX REPLACE "path *= *" "" module ${module})
if (NOT EXISTS "${PROJECT_SOURCE_DIR}/${module}/.git")
message(SEND_ERROR "Git submodule ${module} not found."
message(SEND_ERROR "Git submodule ${module} not found.\n"
"Please run: git submodule update --init --recursive")
endif()
endforeach()
@ -300,6 +335,9 @@ set(CMAKE_VISIBILITY_INLINES_HIDDEN NO)
# set up output paths for executable binaries
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/bin/$<CONFIG>)
if (ENABLE_LIBRETRO)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
endif()
# System imported libraries
# ======================
@ -310,7 +348,7 @@ find_package(Threads REQUIRED)
if (ENABLE_QT)
if (NOT USE_SYSTEM_QT)
download_qt(6.9.2)
download_qt(6.9.3)
endif()
find_package(Qt6 REQUIRED COMPONENTS Widgets Multimedia Concurrent)
@ -359,7 +397,7 @@ if (APPLE)
find_library(IOSURFACE_LIBRARY IOSurface REQUIRED)
set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${AVFOUNDATION_LIBRARY} ${IOSURFACE_LIBRARY} ${MOLTENVK_LIBRARY})
if (ENABLE_VULKAN)
if (ENABLE_VULKAN AND NOT ENABLE_LIBRETRO)
if (NOT USE_SYSTEM_MOLTENVK)
download_moltenvk()
endif()
@ -513,8 +551,6 @@ if (NOT ANDROID AND NOT IOS)
include(BundleTarget)
if (ENABLE_QT)
qt_bundle_target(citra_meta)
elseif (ENABLE_SDL2_FRONTEND)
bundle_target(citra_meta)
endif()
if (ENABLE_ROOM_STANDALONE)
bundle_target(citra_room_standalone)

View File

@ -1,26 +0,0 @@
# To use this as a script, make sure you pass in the variables BASE_DIR, SRC_DIR, BUILD_DIR, and TARGET_FILE
cmake_minimum_required(VERSION 3.15)
if(WIN32)
set(PLATFORM "windows")
elseif(APPLE)
set(PLATFORM "mac")
elseif(UNIX)
set(PLATFORM "linux")
else()
message(FATAL_ERROR "Cannot build installer for this unsupported platform")
endif()
list(APPEND CMAKE_MODULE_PATH "${BASE_DIR}/CMakeModules")
include(DownloadExternals)
download_qt(tools_ifw)
get_external_prefix(qt QT_PREFIX)
file(GLOB_RECURSE INSTALLER_BASE "${QT_PREFIX}/**/installerbase*")
file(GLOB_RECURSE BINARY_CREATOR "${QT_PREFIX}/**/binarycreator*")
set(CONFIG_FILE "${SRC_DIR}/config/config_${PLATFORM}.xml")
set(PACKAGES_DIR "${BUILD_DIR}/packages")
file(MAKE_DIRECTORY ${PACKAGES_DIR})
execute_process(COMMAND ${BINARY_CREATOR} -t ${INSTALLER_BASE} -n -c ${CONFIG_FILE} -p ${PACKAGES_DIR} ${TARGET_FILE})

View File

@ -1,49 +1,52 @@
# Gets a UTC timstamp and sets the provided variable to it
function(get_timestamp _var)
string(TIMESTAMP timestamp UTC)
set(${_var} "${timestamp}" PARENT_SCOPE)
endfunction()
get_timestamp(BUILD_DATE)
macro(generate_build_info)
find_package(Git QUIET)
list(APPEND CMAKE_MODULE_PATH "${SRC_DIR}/externals/cmake-modules")
# Gets a UTC timstamp and sets the provided variable to it
function(get_timestamp _var)
string(TIMESTAMP timestamp UTC)
set(${_var} "${timestamp}" PARENT_SCOPE)
endfunction()
get_timestamp(BUILD_DATE)
if (EXISTS "${SRC_DIR}/.git/objects")
# Find the package here with the known path so that the GetGit commands can find it as well
find_package(Git QUIET PATHS "${GIT_EXECUTABLE}")
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/externals/cmake-modules")
# only use Git to check revision info when source is obtained via Git
include(GetGitRevisionDescription)
get_git_head_revision(GIT_REF_SPEC GIT_REV)
git_describe(GIT_DESC --always --long --dirty)
git_branch_name(GIT_BRANCH)
elseif (EXISTS "${SRC_DIR}/GIT-COMMIT" AND EXISTS "${SRC_DIR}/GIT-TAG")
# unified source archive
file(READ "${SRC_DIR}/GIT-COMMIT" GIT_REV_RAW LIMIT 64)
string(STRIP "${GIT_REV_RAW}" GIT_REV)
string(SUBSTRING "${GIT_REV_RAW}" 0 9 GIT_DESC)
set(GIT_BRANCH "HEAD")
else()
# self-packed archive?
set(GIT_REV "UNKNOWN")
set(GIT_DESC "UNKNOWN")
set(GIT_BRANCH "UNKNOWN")
endif()
string(SUBSTRING "${GIT_REV}" 0 7 GIT_SHORT_REV)
if (EXISTS "${CMAKE_SOURCE_DIR}/GIT-COMMIT" AND EXISTS "${CMAKE_SOURCE_DIR}/GIT-TAG")
file(READ "${CMAKE_SOURCE_DIR}/GIT-COMMIT" GIT_REV_RAW LIMIT 64)
string(STRIP "${GIT_REV_RAW}" GIT_REV)
string(SUBSTRING "${GIT_REV_RAW}" 0 9 GIT_DESC)
set(GIT_BRANCH "HEAD")
elseif (EXISTS "${CMAKE_SOURCE_DIR}/.git/objects")
# Find the package here with the known path so that the GetGit commands can find it as well
find_package(Git QUIET PATHS "${GIT_EXECUTABLE}")
# Set build version
set(REPO_NAME "")
set(BUILD_VERSION "0")
set(BUILD_FULLNAME "${GIT_SHORT_REV}")
if (DEFINED ENV{CI} AND DEFINED ENV{GITHUB_ACTIONS})
if ($ENV{GITHUB_REF_TYPE} STREQUAL "tag")
set(GIT_TAG $ENV{GITHUB_REF_NAME})
# only use Git to check revision info when source is obtained via Git
include(GetGitRevisionDescription)
get_git_head_revision(GIT_REF_SPEC GIT_REV)
git_describe(GIT_DESC --always --long --dirty)
git_branch_name(GIT_BRANCH)
else()
# self-packed archive?
set(GIT_REV "UNKNOWN")
set(GIT_DESC "UNKNOWN")
set(GIT_BRANCH "UNKNOWN")
endif()
elseif (EXISTS "${SRC_DIR}/GIT-COMMIT" AND EXISTS "${SRC_DIR}/GIT-TAG")
file(READ "${SRC_DIR}/GIT-TAG" GIT_TAG)
string(STRIP ${GIT_TAG} GIT_TAG)
endif()
string(SUBSTRING "${GIT_REV}" 0 7 GIT_SHORT_REV)
if (DEFINED GIT_TAG AND NOT "${GIT_TAG}" STREQUAL "unknown")
set(BUILD_VERSION "${GIT_TAG}")
set(BUILD_FULLNAME "${BUILD_VERSION}")
endif()
# Set build version
set(REPO_NAME "")
set(BUILD_VERSION "0")
set(BUILD_FULLNAME "${GIT_SHORT_REV}")
if (DEFINED ENV{CI} AND DEFINED ENV{GITHUB_ACTIONS})
if ($ENV{GITHUB_REF_TYPE} STREQUAL "tag")
set(GIT_TAG $ENV{GITHUB_REF_NAME})
endif()
elseif (EXISTS "${CMAKE_SOURCE_DIR}/GIT-COMMIT" AND EXISTS "${CMAKE_SOURCE_DIR}/GIT-TAG")
file(READ "${CMAKE_SOURCE_DIR}/GIT-TAG" GIT_TAG)
string(STRIP ${GIT_TAG} GIT_TAG)
endif()
if (DEFINED GIT_TAG AND NOT "${GIT_TAG}" STREQUAL "unknown")
set(BUILD_VERSION "${GIT_TAG}")
set(BUILD_FULLNAME "${BUILD_VERSION}")
endif()
endmacro()

View File

@ -1,8 +1,10 @@
list(APPEND CMAKE_MODULE_PATH "${SRC_DIR}/CMakeModules")
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/CMakeModules")
include(GenerateBuildInfo)
generate_build_info()
# The variable SRC_DIR must be passed into the script (since it uses the current build directory for all values of CMAKE_*_DIR)
set(VIDEO_CORE "${SRC_DIR}/src/video_core")
set(VIDEO_CORE "${CMAKE_SOURCE_DIR}/src/video_core")
set(HASH_FILES
"${VIDEO_CORE}/renderer_opengl/gl_shader_disk_cache.cpp"
"${VIDEO_CORE}/renderer_opengl/gl_shader_disk_cache.h"
@ -10,6 +12,10 @@ set(HASH_FILES
"${VIDEO_CORE}/renderer_opengl/gl_shader_util.h"
"${VIDEO_CORE}/renderer_vulkan/vk_shader_util.cpp"
"${VIDEO_CORE}/renderer_vulkan/vk_shader_util.h"
"${VIDEO_CORE}/renderer_vulkan/vk_shader_disk_cache.cpp"
"${VIDEO_CORE}/renderer_vulkan/vk_shader_disk_cache.h"
"${VIDEO_CORE}/renderer_vulkan/vk_pipeline_cache.cpp"
"${VIDEO_CORE}/renderer_vulkan/vk_pipeline_cache.h"
"${VIDEO_CORE}/shader/generator/glsl_fs_shader_gen.cpp"
"${VIDEO_CORE}/shader/generator/glsl_fs_shader_gen.h"
"${VIDEO_CORE}/shader/generator/glsl_shader_decompiler.cpp"
@ -18,6 +24,7 @@ set(HASH_FILES
"${VIDEO_CORE}/shader/generator/glsl_shader_gen.h"
"${VIDEO_CORE}/shader/generator/pica_fs_config.cpp"
"${VIDEO_CORE}/shader/generator/pica_fs_config.h"
"${VIDEO_CORE}/shader/generator/profile.h"
"${VIDEO_CORE}/shader/generator/shader_gen.cpp"
"${VIDEO_CORE}/shader/generator/shader_gen.h"
"${VIDEO_CORE}/shader/generator/shader_uniforms.cpp"
@ -41,4 +48,4 @@ foreach (F IN LISTS HASH_FILES)
set(COMBINED "${COMBINED}${TMP}")
endforeach()
string(MD5 SHADER_CACHE_VERSION "${COMBINED}")
configure_file("${SRC_DIR}/src/common/scm_rev.cpp.in" "scm_rev.cpp" @ONLY)
configure_file("${CMAKE_SOURCE_DIR}/src/common/scm_rev.cpp.in" "scm_rev.cpp" @ONLY)

View File

@ -0,0 +1,278 @@
## This file should be the *only place* where setting keys exist as strings.
# All references to setting strings should be derived from the
# `setting_keys.h` and `jni_setting_keys.cpp` files generated here.
# !!! Changes made here should be mirrored to SettingKeys.kt if used on Android
# Shared setting keys (multi-platform)
foreach(KEY IN ITEMS
"use_artic_base_controller"
"enable_gamemode"
"use_cpu_jit"
"cpu_clock_percentage"
"is_new_3ds"
"lle_applets"
"deterministic_async_operations"
"enable_required_online_lle_modules"
"use_virtual_sd"
"use_custom_storage"
"compress_cia_installs"
"region_value"
"init_clock"
"init_time"
"init_time_offset"
"init_ticks_type"
"init_ticks_override"
"plugin_loader"
"allow_plugin_loader"
"steps_per_hour"
"apply_region_free_patch"
"graphics_api"
"physical_device"
"use_gles"
"renderer_debug"
"dump_command_buffers"
"spirv_shader_gen"
"disable_spirv_optimizer"
"async_shader_compilation"
"async_presentation"
"use_hw_shader"
"use_disk_shader_cache"
"shaders_accurate_mul"
"use_vsync"
"use_display_refresh_rate_detection"
"use_shader_jit"
"resolution_factor"
"frame_limit"
"turbo_limit"
"texture_filter"
"texture_sampling"
"delay_game_render_thread_us"
"layout_option"
"swap_screen"
"upright_screen"
"secondary_display_layout"
"large_screen_proportion"
"screen_gap"
"small_screen_position"
"custom_top_x"
"custom_top_y"
"custom_top_width"
"custom_top_height"
"custom_bottom_x"
"custom_bottom_y"
"custom_bottom_width"
"custom_bottom_height"
"custom_second_layer_opacity"
"aspect_ratio"
"screen_top_stretch"
"screen_top_leftright_padding"
"screen_top_topbottom_padding"
"screen_bottom_stretch"
"screen_bottom_leftright_padding"
"screen_bottom_topbottom_padding"
"portrait_layout_option"
"custom_portrait_top_x"
"custom_portrait_top_y"
"custom_portrait_top_width"
"custom_portrait_top_height"
"custom_portrait_bottom_x"
"custom_portrait_bottom_y"
"custom_portrait_bottom_width"
"custom_portrait_bottom_height"
"bg_red"
"bg_green"
"bg_blue"
"render_3d"
"factor_3d"
"swap_eyes_3d"
"render_3d_which_display"
"mono_render_option"
"cardboard_screen_size"
"cardboard_x_shift"
"cardboard_y_shift"
"filter_mode"
"pp_shader_name"
"anaglyph_shader_name"
"dump_textures"
"custom_textures"
"preload_textures"
"async_custom_loading"
"disable_right_eye_render"
"audio_emulation"
"enable_audio_stretching"
"enable_realtime_audio"
"volume"
"output_type"
"output_device"
"input_type"
"input_device"
"delay_start_for_lle_modules"
"use_gdbstub"
"gdbstub_port"
"instant_debug_log"
"enable_rpc_server"
"log_filter"
"log_regex_filter"
"toggle_unique_data_console_type"
"use_integer_scaling"
"layouts_to_cycle"
"camera_inner_flip"
"camera_outer_left_flip"
"camera_outer_right_flip"
"camera_inner_name"
"camera_inner_config"
"camera_outer_left_name"
"camera_outer_left_config"
"camera_outer_right_name"
"camera_outer_right_config"
"video_encoder"
"video_encoder_options"
"video_bitrate"
"audio_encoder"
"audio_encoder_options"
"audio_bitrate"
"last_artic_base_addr"
"motion_device"
"touch_device"
"udp_input_address"
"udp_input_port"
"udp_pad_index"
"record_frame_times"
"language" # FIXME: DUPLICATE KEY (libretro equivalent: language_value)
"web_api_url"
"citra_username"
"citra_token"
)
set(SETTING_KEY_LIST "${SETTING_KEY_LIST}\n\"${KEY}\",")
set(SETTING_KEY_DEFINITIONS "${SETTING_KEY_DEFINITIONS}\nDEFINE_KEY(${KEY})")
if (ANDROID)
string(REPLACE "_" "_1" KEY_JNI_ESCAPED ${KEY})
set(JNI_SETTING_KEY_DEFINITIONS "${JNI_SETTING_KEY_DEFINITIONS}
JNI_DEFINE_KEY(${KEY}, ${KEY_JNI_ESCAPED})")
endif()
endforeach()
# Qt exclusive setting keys
# Note: A lot of these are very generic because our Qt settings are currently put under groups:
# E.g. UILayout\geometry
# TODO: We should probably get rid of these groups and use complete keys at some point. -OS
# FIXME: Some of these settings don't use the standard snake_case. When we can migrate, address that. -OS
if (ENABLE_QT)
foreach(KEY IN ITEMS
"nickname"
"ip"
"port"
"room_nickname"
"room_name"
"room_port"
"host_type"
"max_player"
"room_description"
"multiplayer_filter_text"
"multiplayer_filter_games_owned"
"multiplayer_filter_hide_empty"
"multiplayer_filter_hide_full"
"username_ban_list"
"username"
"ip_ban_list"
"romsPath"
"symbolsPath"
"movieRecordPath"
"moviePlaybackPath"
"videoDumpingPath"
"gameListRootDir"
"gameListDeepScan"
"path"
"deep_scan"
"expanded"
"recentFiles"
"output_format"
"format_options"
"theme"
"program_id"
"geometry"
"state"
"geometryRenderWindow"
"gameListHeaderState"
"microProfileDialogGeometry"
"name"
"bind"
"profile"
"use_touchpad"
"controller_touch_device"
"use_touch_from_button"
"touch_from_button_map"
"touch_from_button_maps" # Why are these two so similar? Basically typo bait
"nand_directory"
"sdmc_directory"
"game_id"
"KeySeq"
"gamedirs"
"libvorbis"
"Context"
"favorites"
)
set(SETTING_KEY_LIST "${SETTING_KEY_LIST}\n\"${KEY}\",")
set(SETTING_KEY_DEFINITIONS "${SETTING_KEY_DEFINITIONS}\nDEFINE_KEY(${KEY})")
endforeach()
endif()
# Android exclusive setting keys (standalone app only, not Android libretro)
if (ANDROID)
foreach(KEY IN ITEMS
"expand_to_cutout_area"
"performance_overlay_enable"
"performance_overlay_show_fps"
"performance_overlay_show_frame_time"
"performance_overlay_show_speed"
"performance_overlay_show_app_ram_usage"
"performance_overlay_show_available_ram"
"performance_overlay_show_battery_temp"
"performance_overlay_background"
"use_frame_limit" # FIXME: DUPLICATE KEY (shared equivalent: frame_limit)
"android_hide_images"
"screen_orientation"
"performance_overlay_position"
)
string(REPLACE "_" "_1" KEY_JNI_ESCAPED ${KEY})
set(SETTING_KEY_LIST "${SETTING_KEY_LIST}\n\"${KEY}\",")
set(SETTING_KEY_DEFINITIONS "${SETTING_KEY_DEFINITIONS}\nDEFINE_KEY(${KEY})")
set(JNI_SETTING_KEY_DEFINITIONS "${JNI_SETTING_KEY_DEFINITIONS}
JNI_DEFINE_KEY(${KEY}, ${KEY_JNI_ESCAPED})")
endforeach()
endif()
# Libretro exclusive setting keys
if (ENABLE_LIBRETRO)
foreach(KEY IN ITEMS
"language_value"
"swap_screen_mode"
"use_libretro_save_path"
"analog_function"
"analog_deadzone"
"enable_mouse_touchscreen"
"enable_touch_touchscreen"
"enable_touch_pointer_timeout"
"enable_motion"
"motion_sensitivity"
)
string(REPLACE "_" "_1" KEY_JNI_ESCAPED ${KEY})
set(SETTING_KEY_LIST "${SETTING_KEY_LIST}\n\"${KEY}\",")
set(SETTING_KEY_DEFINITIONS "${SETTING_KEY_DEFINITIONS}\nDEFINE_KEY(${KEY})")
endforeach()
endif()
# Trim trailing comma and newline from SETTING_KEY_LIST
string(LENGTH "${SETTING_KEY_LIST}" SETTING_KEY_LIST_LENGTH)
math(EXPR SETTING_KEY_LIST_NEW_LENGTH "${SETTING_KEY_LIST_LENGTH} - 1")
string(SUBSTRING "${SETTING_KEY_LIST}" 0 ${SETTING_KEY_LIST_NEW_LENGTH} SETTING_KEY_LIST)
# Configure files
configure_file("common/setting_keys.h.in" "common/setting_keys.h" @ONLY)
if (ENABLE_QT)
configure_file("citra_qt/setting_qkeys.h.in" "citra_qt/setting_qkeys.h" @ONLY)
endif()
if (ANDROID AND NOT ENABLE_LIBRETRO)
configure_file("android/app/src/main/jni/jni_setting_keys.cpp.in" "android/app/src/main/jni/jni_setting_keys.cpp" @ONLY)
endif()

View File

@ -1,6 +1,8 @@
![Azahar Emulator](https://azahar-emu.org/resources/images/logo/azahar-name-and-logo.svg)
![GitHub Release](https://img.shields.io/github/v/release/azahar-emu/azahar?label=Current%20Release)
![Current Release](https://img.shields.io/github/v/release/azahar-emu/azahar?label=Current%20Release)
![Current Prerelease](https://img.shields.io/github/v/release/azahar-emu/azahar?include_prereleases&label=Current%20Prerelease)
![GitHub Downloads](https://img.shields.io/github/downloads/azahar-emu/azahar/total?logo=github&label=GitHub%20Downloads)
![Google Play Downloads](https://playbadges.pavi2410.com/badge/downloads?id=io.github.lime3ds.android&pretty&label=Play%20Store%20Downloads)
![Flathub Downloads](https://img.shields.io/flathub/downloads/org.azahar_emu.Azahar?logo=flathub&label=Flathub%20Downloads)

View File

@ -35,6 +35,7 @@
<string>cci</string>
<string>cxi</string>
<string>cia</string>
<string>3ds</string>
</array>
<key>CFBundleTypeName</key>
<string>Nintendo 3DS File</string>

@ -1 +1 @@
Subproject commit eadcdfb84b6f3b95734e867d99fe16a9e8db717f
Subproject commit d9f1126e42b606d02ecc89b10cb9a336a3b2f5a3

File diff suppressed because it is too large Load Diff

1058
dist/languages/da_DK.ts vendored

File diff suppressed because it is too large Load Diff

1064
dist/languages/de.ts vendored

File diff suppressed because it is too large Load Diff

2153
dist/languages/el.ts vendored

File diff suppressed because it is too large Load Diff

1087
dist/languages/es_ES.ts vendored

File diff suppressed because it is too large Load Diff

1056
dist/languages/fi.ts vendored

File diff suppressed because it is too large Load Diff

1188
dist/languages/fr.ts vendored

File diff suppressed because it is too large Load Diff

1060
dist/languages/hu_HU.ts vendored

File diff suppressed because it is too large Load Diff

1058
dist/languages/id.ts vendored

File diff suppressed because it is too large Load Diff

1137
dist/languages/it.ts vendored

File diff suppressed because it is too large Load Diff

1064
dist/languages/ja_JP.ts vendored

File diff suppressed because it is too large Load Diff

1060
dist/languages/ko_KR.ts vendored

File diff suppressed because it is too large Load Diff

1056
dist/languages/lt_LT.ts vendored

File diff suppressed because it is too large Load Diff

1060
dist/languages/nb.ts vendored

File diff suppressed because it is too large Load Diff

1060
dist/languages/nl.ts vendored

File diff suppressed because it is too large Load Diff

1073
dist/languages/pl_PL.ts vendored

File diff suppressed because it is too large Load Diff

1079
dist/languages/pt_BR.ts vendored

File diff suppressed because it is too large Load Diff

1060
dist/languages/ro_RO.ts vendored

File diff suppressed because it is too large Load Diff

1064
dist/languages/ru_RU.ts vendored

File diff suppressed because it is too large Load Diff

1073
dist/languages/sv.ts vendored

File diff suppressed because it is too large Load Diff

1064
dist/languages/tr_TR.ts vendored

File diff suppressed because it is too large Load Diff

1058
dist/languages/vi_VN.ts vendored

File diff suppressed because it is too large Load Diff

1068
dist/languages/zh_CN.ts vendored

File diff suppressed because it is too large Load Diff

1058
dist/languages/zh_TW.ts vendored

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@
<expanded-acronym>CTR Cart Image</expanded-acronym>
<icon name="azahar"/>
<glob pattern="*.cci"/>
<glob pattern="*.3ds"/>
<magic><match value="NCSD" type="string" offset="256"/></magic>
</mime-type>

View File

@ -50,15 +50,17 @@ else()
endif()
# Catch2
add_library(catch2 INTERFACE)
if(USE_SYSTEM_CATCH2)
find_package(Catch2 3.0.0 REQUIRED)
else()
set(CATCH_INSTALL_DOCS OFF CACHE BOOL "")
set(CATCH_INSTALL_EXTRAS OFF CACHE BOOL "")
add_subdirectory(catch2)
if (ENABLE_TESTS)
add_library(catch2 INTERFACE)
if(USE_SYSTEM_CATCH2)
find_package(Catch2 3.0.0 REQUIRED)
else()
set(CATCH_INSTALL_DOCS OFF CACHE BOOL "")
set(CATCH_INSTALL_EXTRAS OFF CACHE BOOL "")
add_subdirectory(catch2)
endif()
target_link_libraries(catch2 INTERFACE Catch2::Catch2WithMain)
endif()
target_link_libraries(catch2 INTERFACE Catch2::Catch2WithMain)
# Crypto++
if(USE_SYSTEM_CRYPTOPP)
@ -109,7 +111,13 @@ endif()
# Oaknut
if ("arm64" IN_LIST ARCHITECTURE)
add_subdirectory(oaknut EXCLUDE_FROM_ALL)
if(USE_SYSTEM_OAKNUT)
find_package(oaknut REQUIRED)
add_library(oaknut INTERFACE)
target_link_libraries(oaknut INTERFACE merry::oaknut)
else()
add_subdirectory(oaknut EXCLUDE_FROM_ALL)
endif()
endif()
# Dynarmic
@ -292,6 +300,15 @@ if (USE_DISCORD_PRESENCE)
target_include_directories(discord-rpc INTERFACE ./discord-rpc/include)
endif()
# LibRetro
if (ENABLE_LIBRETRO)
add_library(libretro INTERFACE)
target_include_directories(libretro INTERFACE ./libretro-common/libretro-common/include)
if (ANDROID)
add_subdirectory(libretro-common EXCLUDE_FROM_ALL)
endif()
endif()
# JSON
add_library(json-headers INTERFACE)
if (USE_SYSTEM_JSON)

2
externals/boost vendored

@ -1 +1 @@
Subproject commit 2c82bd787302398bcae990e3c9ab2b451284f4ca
Subproject commit 6a85c3100499e886e11c87a5c2109eedacea0a61

View File

@ -14,6 +14,7 @@ option(USE_SYSTEM_JSON "Use the system JSON (nlohmann-json3) package (instead of
option(USE_SYSTEM_DYNARMIC "Use the system dynarmic (instead of the bundled one)" OFF)
option(USE_SYSTEM_FMT "Use the system fmt (instead of the bundled one)" OFF)
option(USE_SYSTEM_XBYAK "Use the system xbyak (instead of the bundled one)" OFF)
option(USE_SYSTEM_OAKNUT "Use the system oaknut (instead of the bundled one)" OFF)
option(USE_SYSTEM_INIH "Use the system inih (instead of the bundled one)" OFF)
option(USE_SYSTEM_FFMPEG_HEADERS "Use the system FFmpeg headers (instead of the bundled one)" OFF)
option(USE_SYSTEM_GLSLANG "Use the system glslang and SPIR-V libraries (instead of the bundled ones)" OFF)
@ -40,6 +41,7 @@ CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_JSON "Disable system JSON" OFF "USE_SYSTEM
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_DYNARMIC "Disable system Dynarmic" OFF "USE_SYSTEM_LIBS" OFF)
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_FMT "Disable system fmt" OFF "USE_SYSTEM_LIBS" OFF)
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_XBYAK "Disable system xbyak" OFF "USE_SYSTEM_LIBS" OFF)
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_OAKNUT "Disable system oaknut" OFF "USE_SYSTEM_LIBS" OFF)
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_INIH "Disable system inih" OFF "USE_SYSTEM_LIBS" OFF)
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_FFMPEG_HEADERS "Disable system ffmpeg" OFF "USE_SYSTEM_LIBS" OFF)
CMAKE_DEPENDENT_OPTION(DISABLE_SYSTEM_GLSLANG "Disable system glslang" OFF "USE_SYSTEM_LIBS" OFF)
@ -66,6 +68,7 @@ set(LIB_VAR_LIST
DYNARMIC
FMT
XBYAK
OAKNUT
INIH
FFMPEG_HEADERS
GLSLANG

View File

@ -0,0 +1,16 @@
add_library(libretro_common STATIC
libretro-common/compat/compat_posix_string.c
libretro-common/compat/fopen_utf8.c
libretro-common/encodings/encoding_utf.c
libretro-common/compat/compat_strl.c
libretro-common/file/file_path.c
libretro-common/streams/file_stream.c
libretro-common/streams/file_stream_transforms.c
libretro-common/string/stdstring.c
libretro-common/time/rtime.c
libretro-common/vfs/vfs_implementation.c
)
target_include_directories(libretro_common PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/libretro-common
${CMAKE_CURRENT_SOURCE_DIR}/libretro-common/include
)

@ -0,0 +1 @@
Subproject commit 7fc7feeddca391be65c94e6541381467684b814d

2
externals/sdl2/SDL vendored

@ -1 +1 @@
Subproject commit 2359383fc187386204c3bb22de89655a494cd128
Subproject commit 5d249570393f7a37e037abf22cd6012a4cc56a71

View File

@ -1,6 +1,8 @@
# Enable modules to include each other's files
include_directories(.)
include(GenerateSettingKeys)
# CMake seems to only define _DEBUG on Windows
set_property(DIRECTORY APPEND PROPERTY
COMPILE_DEFINITIONS $<$<CONFIG:Debug>:_DEBUG> $<$<NOT:$<CONFIG:Debug>>:NDEBUG>)
@ -110,10 +112,14 @@ else()
# In case a flag isn't supported on e.g. a certain architecture, don't error.
-Wno-unused-command-line-argument
# Build fortification options
-Wp,-D_GLIBCXX_ASSERTIONS
-fstack-protector-strong
-fstack-clash-protection
)
if (NOT ENABLE_LIBRETRO)
add_compile_options(
-Wp,-D_GLIBCXX_ASSERTIONS
-fstack-clash-protection
)
endif()
# If we define _FORTIFY_SOURCE when it is already defined, compilation will fail
string(FIND "-D_FORTIFY_SOURCE" "${CMAKE_CXX_FLAGS} " FORTIFY_SOURCE_DEFINED)
@ -189,18 +195,18 @@ if (ENABLE_TESTS)
add_subdirectory(tests)
endif()
if (ENABLE_SDL2_FRONTEND)
add_subdirectory(citra_sdl)
endif()
if (ENABLE_QT)
add_subdirectory(citra_qt)
endif()
if (ENABLE_QT OR ENABLE_SDL2_FRONTEND)
if (ENABLE_QT) # Or any other hypothetical future frontends
add_subdirectory(citra_meta)
endif()
if (ENABLE_LIBRETRO)
add_subdirectory(citra_libretro)
endif()
if (ENABLE_ROOM)
add_subdirectory(citra_room)
endif()
@ -209,7 +215,7 @@ if (ENABLE_ROOM_STANDALONE)
add_subdirectory(citra_room_standalone)
endif()
if (ANDROID)
if (ANDROID AND NOT ENABLE_LIBRETRO)
add_subdirectory(android/app/src/main/jni)
target_include_directories(citra-android PRIVATE android/app/src/main)
endif()

View File

@ -63,7 +63,7 @@ android {
defaultConfig {
// The application ID refers to Lime3DS to allow for
// the Play Store listing, which was originally set up for Lime3DS, to still be used.
applicationId = "io.github.lime3ds.android"
applicationId = "org.azahar_emu.azahar"
minSdk = 29
targetSdk = 35
versionCode = autoVersion
@ -173,6 +173,7 @@ android {
register("googlePlay") {
dimension = "version"
versionNameSuffix = "-googleplay"
applicationId = "io.github.lime3ds.android"
}
}

View File

@ -25,6 +25,7 @@ import androidx.fragment.app.DialogFragment
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.citra.citra_emu.activities.EmulationActivity
import org.citra.citra_emu.model.Game
import org.citra.citra_emu.utils.BuildUtil
import org.citra.citra_emu.utils.FileUtil
import org.citra.citra_emu.utils.Log
@ -132,7 +133,27 @@ object NativeLibrary {
* If not set, it auto-detects a location
*/
external fun setUserDirectory(directory: String)
external fun getInstalledGamePaths(): Array<String?>
data class InstalledGame(
val path: String,
val mediaType: Game.MediaType
)
fun getInstalledGamePaths(): Array<InstalledGame> {
val games = getInstalledGamePathsImpl()
return games.mapNotNull { entry ->
entry?.let {
val sep = it.lastIndexOf('|')
if (sep == -1) return@mapNotNull null
val path = it.substring(0, sep)
val mediaType = Game.MediaType.fromInt(it.substring(sep + 1).toInt())
InstalledGame(path, mediaType!!)
}
}.toTypedArray()
}
private external fun getInstalledGamePathsImpl(): Array<String?>
// Create the config.ini file.
external fun createConfigFile()
@ -230,6 +251,13 @@ object NativeLibrary {
external fun playTimeManagerGetPlayTime(titleId: Long): Long
external fun playTimeManagerGetCurrentTitleId(): Long
private external fun uninstallTitle(titleId: Long, mediaType: Int): Boolean
fun uninstallTitle(titleId: Long, mediaType: Game.MediaType): Boolean {
return uninstallTitle(titleId, mediaType.value)
}
external fun nativeFileExists(path: String): Boolean
private var coreErrorAlertResult = false
private val coreErrorAlertLock = Object()
@ -691,34 +719,47 @@ object NativeLibrary {
@Keep
@JvmStatic
fun getUserDirectory(uriOverride: Uri? = null): String {
fun getNativePath(uri: Uri): String {
BuildUtil.assertNotGooglePlay()
val preferences: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
val dirSep = "/"
val udUri = uriOverride ?:
preferences.getString("CITRA_DIRECTORY", "")!!.toUri()
val udPathSegment = udUri.lastPathSegment!!
val udVirtualPath = udPathSegment.substringAfter(":")
if (udPathSegment.startsWith("primary:")) { // User directory is located in primary storage
val uriString = uri.toString()
if (!uriString.contains(":")) { // These raw URIs happen when generating the game list. Why?
return uriString
}
if (uri.scheme == "file") {
return uri.path!!
}
val pathSegment = uri.lastPathSegment ?: return ""
val virtualPath = pathSegment.substringAfter(":")
if (pathSegment.startsWith("primary:")) { // User directory is located in primary storage
val primaryStoragePath = Environment.getExternalStorageDirectory().absolutePath
return primaryStoragePath + dirSep + udVirtualPath + dirSep
return primaryStoragePath + dirSep + virtualPath
} else { // User directory probably located on a removable storage device
val storageIdString = udPathSegment.substringBefore(":")
val udRemovablePath = RemovableStorageHelper.getRemovableStoragePath(storageIdString)
val storageIdString = pathSegment.substringBefore(":")
val removablePath = RemovableStorageHelper.getRemovableStoragePath(CitraApplication.appContext, storageIdString)
if (udRemovablePath == null) {
if (removablePath == null) {
android.util.Log.e("NativeLibrary",
"Unknown mount location for storage device '$storageIdString' (URI: $udUri)"
"Unknown mount location for storage device '$storageIdString' (URI: $uri)"
)
return ""
}
return udRemovablePath + dirSep + udVirtualPath + dirSep
return removablePath + dirSep + virtualPath
}
}
@Keep
@JvmStatic
fun getUserDirectory(): String {
val preferences: SharedPreferences =
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
val userDirectoryUri = preferences.getString("CITRA_DIRECTORY", "")!!.toUri()
return getNativePath(userDirectoryUri)
}
@Keep

View File

@ -10,7 +10,6 @@ import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
@ -21,6 +20,7 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.net.toUri
import androidx.core.os.BundleCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
@ -43,6 +43,7 @@ import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
import org.citra.citra_emu.fragments.EmulationFragment
import org.citra.citra_emu.fragments.MessageDialogFragment
import org.citra.citra_emu.model.Game
import org.citra.citra_emu.utils.BuildUtil
import org.citra.citra_emu.utils.ControllerMappingHelper
import org.citra.citra_emu.utils.FileBrowserHelper
import org.citra.citra_emu.utils.EmulationLifecycleUtil
@ -267,41 +268,34 @@ class EmulationActivity : AppCompatActivity() {
return super.dispatchKeyEvent(event)
}
val button =
preferences.getInt(InputBindingSetting.getInputButtonKey(event.keyCode), event.keyCode)
val action: Int = when (event.action) {
when (event.action) {
KeyEvent.ACTION_DOWN -> {
hotkeyUtility.handleHotkey(button)
// On some devices, the back gesture / button press is not intercepted by androidx
// and fails to open the emulation menu. So we're stuck running deprecated code to
// cover for either a fault on androidx's side or in OEM skins (MIUI at least)
if (event.keyCode == KeyEvent.KEYCODE_BACK) {
// If the hotkey is pressed, we don't want to open the drawer
if (!hotkeyUtility.HotkeyIsPressed) {
if (!hotkeyUtility.hotkeyIsPressed) {
onBackPressed()
return true
}
}
// Normal key events.
NativeLibrary.ButtonState.PRESSED
return hotkeyUtility.handleKeyPress(event)
}
KeyEvent.ACTION_UP -> {
hotkeyUtility.HotkeyIsPressed = false
NativeLibrary.ButtonState.RELEASED
return hotkeyUtility.handleKeyRelease(event)
}
else -> {
return false;
}
else -> return false
}
val input = event.device
?: // Controller was disconnected
return false
return NativeLibrary.onGamePadEvent(input.descriptor, button, action)
}
private fun onAmiiboSelected(selectedFile: String) {
val success = NativeLibrary.loadAmiibo(selectedFile)
if (!success) {
Log.error("[EmulationActivity] Failed to load Amiibo file: $selectedFile")
MessageDialogFragment.newInstance(
R.string.amiibo_load_error,
R.string.amiibo_load_error_message
@ -524,13 +518,19 @@ class EmulationActivity : AppCompatActivity() {
return true
}
val openFileLauncher =
val openAmiiboFileLauncher =
registerForActivityResult(OpenFileResultContract()) { result: Intent? ->
if (result == null) return@registerForActivityResult
val selectedFiles = FileBrowserHelper.getSelectedFiles(
result, applicationContext, listOf<String>("bin")
) ?: return@registerForActivityResult
onAmiiboSelected(selectedFiles[0])
if (BuildUtil.isGooglePlayBuild) {
onAmiiboSelected(selectedFiles[0])
} else {
val fileUri = selectedFiles[0].toUri()
val nativePath = "!" + NativeLibrary.getNativePath(fileUri)
onAmiiboSelected(nativePath)
}
}
val openImageLauncher =

View File

@ -54,8 +54,10 @@ import org.citra.citra_emu.databinding.DialogShortcutBinding
import org.citra.citra_emu.features.cheats.ui.CheatsFragmentDirections
import org.citra.citra_emu.fragments.IndeterminateProgressDialogFragment
import org.citra.citra_emu.model.Game
import org.citra.citra_emu.utils.BuildUtil
import org.citra.citra_emu.utils.FileUtil
import org.citra.citra_emu.utils.GameIconUtils
import org.citra.citra_emu.utils.Log
import org.citra.citra_emu.viewmodel.GamesViewModel
class GameAdapter(
@ -136,7 +138,7 @@ class GameAdapter(
val holder = view.tag as GameViewHolder
gameExists(holder)
if (holder.game.titleId == 0L) {
if (!holder.game.valid) {
MaterialAlertDialogBuilder(context)
.setTitle(R.string.properties)
.setMessage(R.string.properties_not_loaded)
@ -153,12 +155,21 @@ class GameAdapter(
if (holder.game.isInstalled) {
return true
}
val gameExists = DocumentFile.fromSingleUri(
CitraApplication.appContext,
Uri.parse(holder.game.path)
)?.exists() == true
val path = holder.game.path
val pathUri = path.toUri()
var gameExists: Boolean
if (BuildUtil.isGooglePlayBuild || FileUtil.isNativePath(path)) {
gameExists =
DocumentFile.fromSingleUri(
CitraApplication.appContext,
pathUri
)?.exists() == true
} else {
val nativePath = NativeLibrary.getNativePath(pathUri)
gameExists = NativeLibrary.nativeFileExists(nativePath)
}
return if (!gameExists) {
Log.error("[GameAdapter] ROM file does not exist: $path")
Toast.makeText(
CitraApplication.appContext,
R.string.loader_error_file_not_found,
@ -323,14 +334,16 @@ class GameAdapter(
}
}
val titleId = game.titleId
val dlcTitleId = titleId or 0x8C00000000L
val updateTitleId = titleId or 0xE00000000L
popup.setOnMenuItemClickListener { menuItem ->
val uninstallAction: () -> Unit = {
when (menuItem.itemId) {
R.id.game_context_uninstall -> CitraApplication.documentsTree.deleteDocument(dirs.gameDir)
R.id.game_context_uninstall_dlc -> FileUtil.deleteDocument(CitraApplication.documentsTree.folderUriHelper(dirs.dlcDir)
.toString())
R.id.game_context_uninstall_updates -> FileUtil.deleteDocument(CitraApplication.documentsTree.folderUriHelper(dirs.updatesDir)
.toString())
R.id.game_context_uninstall -> NativeLibrary.uninstallTitle(titleId, game.mediaType)
R.id.game_context_uninstall_dlc -> NativeLibrary.uninstallTitle(dlcTitleId, Game.MediaType.SDMC)
R.id.game_context_uninstall_updates -> NativeLibrary.uninstallTitle(updateTitleId, Game.MediaType.SDMC)
}
ViewModelProvider(activity)[GamesViewModel::class.java].reloadGames(true)
bottomSheetDialog.dismiss()
@ -556,7 +569,9 @@ class GameAdapter(
private class DiffCallback : DiffUtil.ItemCallback<Game>() {
override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
return oldItem.titleId == newItem.titleId
// The title is taken into account to support 3DSX, which all have the titleID 0.
// This only works now because we always return the English title, adjust if that changes.
return oldItem.titleId == newItem.titleId && oldItem.title == newItem.title
}
override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -57,6 +57,7 @@ object StillImageCameraHelper {
val request = ImageRequest.Builder(context)
.data(uri)
.size(width, height)
.allowHardware(false)
.build()
return context.imageLoader.executeBlocking(request).drawable?.toBitmap(
width,

View File

@ -12,6 +12,7 @@ import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
import org.citra.citra_emu.features.settings.model.BooleanSetting
import org.citra.citra_emu.features.settings.model.IntSetting
import org.citra.citra_emu.features.settings.model.IntListSetting
import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.features.settings.utils.SettingsFile
import org.citra.citra_emu.utils.EmulationMenuSettings
@ -31,8 +32,16 @@ class ScreenAdjustmentUtil(
BooleanSetting.SWAP_SCREEN.boolean = isEnabled
settings.saveSetting(BooleanSetting.SWAP_SCREEN, SettingsFile.FILE_NAME_CONFIG)
}
fun cycleLayouts() {
val landscapeValues = context.resources.getIntArray(R.array.landscapeValues)
val landscapeLayoutsToCycle = IntListSetting.LAYOUTS_TO_CYCLE.list;
val landscapeValues =
if (landscapeLayoutsToCycle.isNotEmpty())
landscapeLayoutsToCycle.toIntArray()
else context.resources.getIntArray(
R.array.landscapeValues
)
val portraitValues = context.resources.getIntArray(R.array.portraitValues)
if (NativeLibrary.isPortraitMode) {

View File

@ -6,18 +6,15 @@ package org.citra.citra_emu.display
import android.app.Presentation
import android.content.Context
import android.graphics.SurfaceTexture
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.os.Bundle
import android.view.Display
import android.view.MotionEvent
import android.view.Surface
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.WindowManager
import org.citra.citra_emu.features.settings.model.IntSetting
import org.citra.citra_emu.display.SecondaryDisplayLayout
import org.citra.citra_emu.NativeLibrary
class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener {
@ -50,7 +47,7 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener {
val currentDisplayId = context.display.displayId
val displays = dm.displays
val presDisplays = dm.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
return displays.firstOrNull {
val extDisplays = displays.filter {
val isPresentable = presDisplays.any { pd -> pd.displayId == it.displayId }
val isNotDefaultOrPresentable = it.displayId != Display.DEFAULT_DISPLAY || isPresentable
isNotDefaultOrPresentable &&
@ -59,9 +56,18 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener {
it.state != Display.STATE_OFF &&
it.isValid
}
// if there is a display called Built-In Display or Built-In Screen, prioritize the OTHER screen
val selected = extDisplays.firstOrNull { ! it.name.contains("Built",true) }
?: extDisplays.firstOrNull()
return selected
}
fun updateDisplay() {
// return early if the parent context is dead or dying
if (context is android.app.Activity && (context.isFinishing || context.isDestroyed)) {
return
}
// decide if we are going to the external display or the internal one
var display = getExternalDisplay(context)
if (display == null ||
@ -74,12 +80,25 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener {
// otherwise, make a new presentation
releasePresentation()
pres = SecondaryDisplayPresentation(context, display!!, this)
pres?.show()
try {
pres = SecondaryDisplayPresentation(context, display!!, this)
pres?.show()
}
// catch BadTokenException and InvalidDisplayException,
// the display became invalid asynchronously, so we can assign to null
// until onDisplayAdded/Removed/Changed is called and logic retriggered
catch (_: WindowManager.BadTokenException) {
pres = null
} catch (_: WindowManager.InvalidDisplayException) {
pres = null
}
}
fun releasePresentation() {
pres?.dismiss()
try {
pres?.dismiss()
} catch (_: Exception) { }
pres = null
}

View File

@ -11,5 +11,6 @@ enum class Hotkey(val button: Int) {
PAUSE_OR_RESUME(10004),
QUICKSAVE(10005),
QUICKLOAD(10006),
TURBO_LIMIT(10007);
TURBO_LIMIT(10007),
ENABLE(10008);
}

View File

@ -5,50 +5,140 @@
package org.citra.citra_emu.features.hotkeys
import android.content.Context
import android.view.KeyEvent
import android.widget.Toast
import androidx.preference.PreferenceManager
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
import org.citra.citra_emu.utils.EmulationLifecycleUtil
import org.citra.citra_emu.utils.TurboHelper
import org.citra.citra_emu.display.ScreenAdjustmentUtil
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
import org.citra.citra_emu.features.settings.model.Settings
class HotkeyUtility(
private val screenAdjustmentUtil: ScreenAdjustmentUtil,
private val context: Context) {
private val context: Context
) {
private val hotkeyButtons = Hotkey.entries.map { it.button }
var HotkeyIsPressed = false
private var hotkeyIsEnabled = false
var hotkeyIsPressed = false
private val currentlyPressedButtons = mutableSetOf<Int>()
fun handleKeyPress(keyEvent: KeyEvent): Boolean {
var handled = false
val buttonSet = InputBindingSetting.getButtonSet(keyEvent)
val enableButton =
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
.getString(Settings.HOTKEY_ENABLE, "")
val thisKeyIsEnableButton = buttonSet.contains(Hotkey.ENABLE.button)
val thisKeyIsHotkey =
!thisKeyIsEnableButton && Hotkey.entries.any { buttonSet.contains(it.button) }
hotkeyIsEnabled = hotkeyIsEnabled || enableButton == "" || thisKeyIsEnableButton
// Now process all internal buttons associated with this keypress
for (button in buttonSet) {
currentlyPressedButtons.add(button)
//option 1 - this is the enable command, which was already handled
if (button == Hotkey.ENABLE.button) {
handled = true
}
// option 2 - this is a different hotkey command
else if (hotkeyButtons.contains(button)) {
if (hotkeyIsEnabled) {
handled = handleHotkey(button) || handled
}
}
// option 3 - this is a normal key
else {
// if this key press is ALSO associated with a hotkey that will process, skip
// the normal key event.
if (!thisKeyIsHotkey || !hotkeyIsEnabled) {
handled = NativeLibrary.onGamePadEvent(
keyEvent.device.descriptor,
button,
NativeLibrary.ButtonState.PRESSED
) || handled
}
}
}
return handled
}
fun handleKeyRelease(keyEvent: KeyEvent): Boolean {
var handled = false
val buttonSet = InputBindingSetting.getButtonSet(keyEvent)
val thisKeyIsEnableButton = buttonSet.contains(Hotkey.ENABLE.button)
val thisKeyIsHotkey =
!thisKeyIsEnableButton && Hotkey.entries.any { buttonSet.contains(it.button) }
if (thisKeyIsEnableButton) {
handled = true; hotkeyIsEnabled = false
}
for (button in buttonSet) {
// this is a hotkey button
if (hotkeyButtons.contains(button)) {
currentlyPressedButtons.remove(button)
if (!currentlyPressedButtons.any { hotkeyButtons.contains(it) }) {
// all hotkeys are no longer pressed
hotkeyIsPressed = false
}
} else {
// if this key ALSO sends a hotkey command that we already/will handle,
// or if we did not register the press of this button, e.g. if this key
// was also a hotkey pressed after enable, but released after enable button release, then
// skip the normal key event
if ((!thisKeyIsHotkey || !hotkeyIsEnabled) && currentlyPressedButtons.contains(
button
)
) {
handled = NativeLibrary.onGamePadEvent(
keyEvent.device.descriptor,
button,
NativeLibrary.ButtonState.RELEASED
) || handled
currentlyPressedButtons.remove(button)
}
}
}
return handled
}
fun handleHotkey(bindedButton: Int): Boolean {
if(hotkeyButtons.contains(bindedButton)) {
when (bindedButton) {
Hotkey.SWAP_SCREEN.button -> screenAdjustmentUtil.swapScreen()
Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts()
Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame()
Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume()
Hotkey.TURBO_LIMIT.button -> TurboHelper.toggleTurbo(true)
Hotkey.QUICKSAVE.button -> {
NativeLibrary.saveState(NativeLibrary.QUICKSAVE_SLOT)
Toast.makeText(context,
context.getString(R.string.saving),
Toast.LENGTH_SHORT).show()
}
Hotkey.QUICKLOAD.button -> {
val wasLoaded = NativeLibrary.loadStateIfAvailable(NativeLibrary.QUICKSAVE_SLOT)
val stringRes = if(wasLoaded) {
R.string.loading
} else {
R.string.quickload_not_found
}
Toast.makeText(context,
context.getString(stringRes),
Toast.LENGTH_SHORT).show()
}
else -> {}
when (bindedButton) {
Hotkey.SWAP_SCREEN.button -> screenAdjustmentUtil.swapScreen()
Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts()
Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame()
Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume()
Hotkey.TURBO_LIMIT.button -> TurboHelper.toggleTurbo(true)
Hotkey.QUICKSAVE.button -> {
NativeLibrary.saveState(NativeLibrary.QUICKSAVE_SLOT)
Toast.makeText(
context,
context.getString(R.string.saving),
Toast.LENGTH_SHORT
).show()
}
HotkeyIsPressed = true
return true
Hotkey.QUICKLOAD.button -> {
val wasLoaded = NativeLibrary.loadStateIfAvailable(NativeLibrary.QUICKSAVE_SLOT)
val stringRes = if (wasLoaded) {
R.string.loading
} else {
R.string.quickload_not_found
}
Toast.makeText(
context,
context.getString(stringRes),
Toast.LENGTH_SHORT
).show()
}
else -> {}
}
return false
hotkeyIsPressed = true
return true
}
}

View File

@ -0,0 +1,141 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings
// This list should mirror the list in GenerateSettingKeys.cmake,
// specifically the Shared and Android setting keys.
@Suppress("KotlinJniMissingFunction", "FunctionName")
object SettingKeys {
// Shared
external fun use_artic_base_controller(): String
external fun use_cpu_jit(): String
external fun cpu_clock_percentage(): String
external fun is_new_3ds(): String
external fun lle_applets(): String
external fun deterministic_async_operations(): String
external fun enable_required_online_lle_modules(): String
external fun use_virtual_sd(): String
external fun compress_cia_installs(): String
external fun region_value(): String
external fun init_clock(): String
external fun init_time(): String
external fun init_ticks_type(): String
external fun init_ticks_override(): String
external fun plugin_loader(): String
external fun allow_plugin_loader(): String
external fun steps_per_hour(): String
external fun apply_region_free_patch(): String
external fun graphics_api(): String
external fun use_gles(): String
external fun renderer_debug(): String
external fun spirv_shader_gen(): String
external fun disable_spirv_optimizer(): String
external fun async_shader_compilation(): String
external fun async_presentation(): String
external fun use_hw_shader(): String
external fun use_disk_shader_cache(): String
external fun shaders_accurate_mul(): String
external fun use_vsync(): String
external fun use_shader_jit(): String
external fun resolution_factor(): String
external fun frame_limit(): String
external fun turbo_limit(): String
external fun texture_filter(): String
external fun texture_sampling(): String
external fun delay_game_render_thread_us(): String
external fun layout_option(): String
external fun swap_screen(): String
external fun upright_screen(): String
external fun secondary_display_layout(): String
external fun large_screen_proportion(): String
external fun screen_gap(): String
external fun small_screen_position(): String
external fun custom_top_x(): String
external fun custom_top_y(): String
external fun custom_top_width(): String
external fun custom_top_height(): String
external fun custom_bottom_x(): String
external fun custom_bottom_y(): String
external fun custom_bottom_width(): String
external fun custom_bottom_height(): String
external fun custom_second_layer_opacity(): String
external fun aspect_ratio(): String
external fun portrait_layout_option(): String
external fun custom_portrait_top_x(): String
external fun custom_portrait_top_y(): String
external fun custom_portrait_top_width(): String
external fun custom_portrait_top_height(): String
external fun custom_portrait_bottom_x(): String
external fun custom_portrait_bottom_y(): String
external fun custom_portrait_bottom_width(): String
external fun custom_portrait_bottom_height(): String
external fun bg_red(): String
external fun bg_green(): String
external fun bg_blue(): String
external fun render_3d(): String
external fun factor_3d(): String
external fun swap_eyes_3d(): String
external fun render_3d_which_display(): String
external fun cardboard_screen_size(): String
external fun cardboard_x_shift(): String
external fun cardboard_y_shift(): String
external fun filter_mode(): String
external fun pp_shader_name(): String
external fun anaglyph_shader_name(): String
external fun dump_textures(): String
external fun custom_textures(): String
external fun preload_textures(): String
external fun async_custom_loading(): String
external fun disable_right_eye_render(): String
external fun audio_emulation(): String
external fun enable_audio_stretching(): String
external fun enable_realtime_audio(): String
external fun volume(): String
external fun output_type(): String
external fun output_device(): String
external fun input_type(): String
external fun input_device(): String
external fun delay_start_for_lle_modules(): String
external fun use_gdbstub(): String
external fun gdbstub_port(): String
external fun instant_debug_log(): String
external fun enable_rpc_server(): String
external fun toggle_unique_data_console_type(): String
external fun log_filter(): String
external fun log_regex_filter(): String
external fun use_integer_scaling(): String
external fun layouts_to_cycle(): String
external fun camera_inner_flip(): String
external fun camera_outer_left_flip(): String
external fun camera_outer_right_flip(): String
external fun camera_inner_name(): String
external fun camera_inner_config(): String
external fun camera_outer_left_name(): String
external fun camera_outer_left_config(): String
external fun camera_outer_right_name(): String
external fun camera_outer_right_config(): String
external fun last_artic_base_addr(): String
external fun motion_device(): String
external fun touch_device(): String
external fun udp_input_address(): String
external fun udp_input_port(): String
external fun udp_pad_index(): String
external fun record_frame_times(): String
// Android
external fun expand_to_cutout_area(): String
external fun performance_overlay_enable(): String
external fun performance_overlay_show_fps(): String
external fun performance_overlay_show_frame_time(): String
external fun performance_overlay_show_speed(): String
external fun performance_overlay_show_app_ram_usage(): String
external fun performance_overlay_show_available_ram(): String
external fun performance_overlay_show_battery_temp(): String
external fun performance_overlay_background(): String
external fun use_frame_limit(): String
external fun android_hide_images(): String
external fun screen_orientation(): String
external fun performance_overlay_position(): String
}

View File

@ -0,0 +1,9 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.model
interface AbstractListSetting<E> : AbstractSetting {
var list: List<E>
}

View File

@ -4,56 +4,59 @@
package org.citra.citra_emu.features.settings.model
import org.citra.citra_emu.features.settings.SettingKeys
enum class BooleanSetting(
override val key: String,
override val section: String,
override val defaultValue: Boolean
) : AbstractBooleanSetting {
EXPAND_TO_CUTOUT_AREA("expand_to_cutout_area", Settings.SECTION_LAYOUT, false),
SPIRV_SHADER_GEN("spirv_shader_gen", Settings.SECTION_RENDERER, true),
ASYNC_SHADERS("async_shader_compilation", Settings.SECTION_RENDERER, false),
DISABLE_SPIRV_OPTIMIZER("disable_spirv_optimizer", Settings.SECTION_RENDERER, true),
PLUGIN_LOADER("plugin_loader", Settings.SECTION_SYSTEM, false),
ALLOW_PLUGIN_LOADER("allow_plugin_loader", Settings.SECTION_SYSTEM, true),
SWAP_SCREEN("swap_screen", Settings.SECTION_LAYOUT, false),
INSTANT_DEBUG_LOG("instant_debug_log", Settings.SECTION_DEBUG, false),
ENABLE_RPC_SERVER("enable_rpc_server", Settings.SECTION_DEBUG, false),
CUSTOM_LAYOUT("custom_layout",Settings.SECTION_LAYOUT,false),
SWAP_EYES_3D("swap_eyes_3d",Settings.SECTION_RENDERER,false),
PERF_OVERLAY_ENABLE("performance_overlay_enable", Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_FPS("performance_overlay_show_fps", Settings.SECTION_LAYOUT, true),
PERF_OVERLAY_SHOW_FRAMETIME("performance_overlay_show_frame_time", Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_SPEED("performance_overlay_show_speed", Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_APP_RAM_USAGE("performance_overlay_show_app_ram_usage", Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_AVAILABLE_RAM("performance_overlay_show_available_ram", Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_BATTERY_TEMP("performance_overlay_show_battery_temp", Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_BACKGROUND("performance_overlay_background", Settings.SECTION_LAYOUT, false),
DELAY_START_LLE_MODULES("delay_start_for_lle_modules", Settings.SECTION_DEBUG, true),
DETERMINISTIC_ASYNC_OPERATIONS("deterministic_async_operations", Settings.SECTION_DEBUG, false),
REQUIRED_ONLINE_LLE_MODULES("enable_required_online_lle_modules", Settings.SECTION_SYSTEM, false),
LLE_APPLETS("lle_applets", Settings.SECTION_SYSTEM, false),
NEW_3DS("is_new_3ds", Settings.SECTION_SYSTEM, true),
LINEAR_FILTERING("filter_mode", Settings.SECTION_RENDERER, true),
SHADERS_ACCURATE_MUL("shaders_accurate_mul", Settings.SECTION_RENDERER, false),
DISK_SHADER_CACHE("use_disk_shader_cache", Settings.SECTION_RENDERER, true),
DUMP_TEXTURES("dump_textures", Settings.SECTION_UTILITY, false),
CUSTOM_TEXTURES("custom_textures", Settings.SECTION_UTILITY, false),
ASYNC_CUSTOM_LOADING("async_custom_loading", Settings.SECTION_UTILITY, true),
PRELOAD_TEXTURES("preload_textures", Settings.SECTION_UTILITY, false),
ENABLE_AUDIO_STRETCHING("enable_audio_stretching", Settings.SECTION_AUDIO, true),
ENABLE_REALTIME_AUDIO("enable_realtime_audio", Settings.SECTION_AUDIO, false),
CPU_JIT("use_cpu_jit", Settings.SECTION_CORE, true),
HW_SHADER("use_hw_shader", Settings.SECTION_RENDERER, true),
SHADER_JIT("use_shader_jit", Settings.SECTION_RENDERER, true),
VSYNC("use_vsync", Settings.SECTION_RENDERER, false),
USE_FRAME_LIMIT("use_frame_limit", Settings.SECTION_RENDERER, true),
DEBUG_RENDERER("renderer_debug", Settings.SECTION_DEBUG, false),
DISABLE_RIGHT_EYE_RENDER("disable_right_eye_render", Settings.SECTION_RENDERER, false),
USE_ARTIC_BASE_CONTROLLER("use_artic_base_controller", Settings.SECTION_CONTROLS, false),
UPRIGHT_SCREEN("upright_screen", Settings.SECTION_LAYOUT, false),
COMPRESS_INSTALLED_CIA_CONTENT("compress_cia_installs", Settings.SECTION_STORAGE, false),
ANDROID_HIDE_IMAGES("android_hide_images", Settings.SECTION_CORE, false),
APPLY_REGION_FREE_PATCH("apply_region_free_patch", Settings.SECTION_SYSTEM, true);
EXPAND_TO_CUTOUT_AREA(SettingKeys.expand_to_cutout_area(), Settings.SECTION_LAYOUT, false),
SPIRV_SHADER_GEN(SettingKeys.spirv_shader_gen(), Settings.SECTION_RENDERER, true),
ASYNC_SHADERS(SettingKeys.async_shader_compilation(), Settings.SECTION_RENDERER, false),
DISABLE_SPIRV_OPTIMIZER(SettingKeys.disable_spirv_optimizer(), Settings.SECTION_RENDERER, true),
PLUGIN_LOADER(SettingKeys.plugin_loader(), Settings.SECTION_SYSTEM, false),
ALLOW_PLUGIN_LOADER(SettingKeys.allow_plugin_loader(), Settings.SECTION_SYSTEM, true),
SWAP_SCREEN(SettingKeys.swap_screen(), Settings.SECTION_LAYOUT, false),
INSTANT_DEBUG_LOG(SettingKeys.instant_debug_log(), Settings.SECTION_DEBUG, false),
ENABLE_RPC_SERVER(SettingKeys.enable_rpc_server(), Settings.SECTION_DEBUG, false),
TOGGLE_UNIQUE_DATA_CONSOLE_TYPE(SettingKeys.toggle_unique_data_console_type(), Settings.SECTION_DEBUG, false),
SWAP_EYES_3D(SettingKeys.swap_eyes_3d(),Settings.SECTION_RENDERER, false),
PERF_OVERLAY_ENABLE(SettingKeys.performance_overlay_enable(), Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_FPS(SettingKeys.performance_overlay_show_fps(), Settings.SECTION_LAYOUT, true),
PERF_OVERLAY_SHOW_FRAMETIME(SettingKeys.performance_overlay_show_frame_time(), Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_SPEED(SettingKeys.performance_overlay_show_speed(), Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_APP_RAM_USAGE(SettingKeys.performance_overlay_show_app_ram_usage(), Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_AVAILABLE_RAM(SettingKeys.performance_overlay_show_available_ram(), Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_SHOW_BATTERY_TEMP(SettingKeys.performance_overlay_show_battery_temp(), Settings.SECTION_LAYOUT, false),
PERF_OVERLAY_BACKGROUND(SettingKeys.performance_overlay_background(), Settings.SECTION_LAYOUT, false),
DELAY_START_LLE_MODULES(SettingKeys.delay_start_for_lle_modules(), Settings.SECTION_DEBUG, true),
DETERMINISTIC_ASYNC_OPERATIONS(SettingKeys.deterministic_async_operations(), Settings.SECTION_DEBUG, false),
REQUIRED_ONLINE_LLE_MODULES(SettingKeys.enable_required_online_lle_modules(), Settings.SECTION_SYSTEM, false),
LLE_APPLETS(SettingKeys.lle_applets(), Settings.SECTION_SYSTEM, false),
NEW_3DS(SettingKeys.is_new_3ds(), Settings.SECTION_SYSTEM, true),
LINEAR_FILTERING(SettingKeys.filter_mode(), Settings.SECTION_RENDERER, true),
SHADERS_ACCURATE_MUL(SettingKeys.shaders_accurate_mul(), Settings.SECTION_RENDERER, false),
DISK_SHADER_CACHE(SettingKeys.use_disk_shader_cache(), Settings.SECTION_RENDERER, true),
DUMP_TEXTURES(SettingKeys.dump_textures(), Settings.SECTION_UTILITY, false),
CUSTOM_TEXTURES(SettingKeys.custom_textures(), Settings.SECTION_UTILITY, false),
ASYNC_CUSTOM_LOADING(SettingKeys.async_custom_loading(), Settings.SECTION_UTILITY, true),
PRELOAD_TEXTURES(SettingKeys.preload_textures(), Settings.SECTION_UTILITY, false),
ENABLE_AUDIO_STRETCHING(SettingKeys.enable_audio_stretching(), Settings.SECTION_AUDIO, true),
ENABLE_REALTIME_AUDIO(SettingKeys.enable_realtime_audio(), Settings.SECTION_AUDIO, false),
CPU_JIT(SettingKeys.use_cpu_jit(), Settings.SECTION_CORE, true),
HW_SHADER(SettingKeys.use_hw_shader(), Settings.SECTION_RENDERER, true),
SHADER_JIT(SettingKeys.use_shader_jit(), Settings.SECTION_RENDERER, true),
VSYNC(SettingKeys.use_vsync(), Settings.SECTION_RENDERER, false),
USE_FRAME_LIMIT(SettingKeys.use_frame_limit(), Settings.SECTION_RENDERER, true),
DEBUG_RENDERER(SettingKeys.renderer_debug(), Settings.SECTION_DEBUG, false),
DISABLE_RIGHT_EYE_RENDER(SettingKeys.disable_right_eye_render(), Settings.SECTION_RENDERER, false),
USE_ARTIC_BASE_CONTROLLER(SettingKeys.use_artic_base_controller(), Settings.SECTION_CONTROLS, false),
UPRIGHT_SCREEN(SettingKeys.upright_screen(), Settings.SECTION_LAYOUT, false),
COMPRESS_INSTALLED_CIA_CONTENT(SettingKeys.compress_cia_installs(), Settings.SECTION_STORAGE, false),
ANDROID_HIDE_IMAGES(SettingKeys.android_hide_images(), Settings.SECTION_MISC, false),
APPLY_REGION_FREE_PATCH(SettingKeys.apply_region_free_patch(), Settings.SECTION_SYSTEM, true),
USE_INTEGER_SCALING(SettingKeys.use_integer_scaling(), Settings.SECTION_RENDERER, false);
override var boolean: Boolean = defaultValue
@ -80,6 +83,7 @@ enum class BooleanSetting(
REQUIRED_ONLINE_LLE_MODULES,
NEW_3DS,
LLE_APPLETS,
TOGGLE_UNIQUE_DATA_CONSOLE_TYPE,
VSYNC,
DEBUG_RENDERER,
CPU_JIT,

View File

@ -4,17 +4,18 @@
package org.citra.citra_emu.features.settings.model
import org.citra.citra_emu.features.settings.SettingKeys
enum class FloatSetting(
override val key: String,
override val section: String,
override val defaultValue: Float
) : AbstractFloatSetting {
LARGE_SCREEN_PROPORTION("large_screen_proportion",Settings.SECTION_LAYOUT,2.25f),
SECOND_SCREEN_OPACITY("custom_second_layer_opacity", Settings.SECTION_RENDERER, 100f),
BACKGROUND_RED("bg_red", Settings.SECTION_RENDERER, 0f),
BACKGROUND_BLUE("bg_blue", Settings.SECTION_RENDERER, 0f),
BACKGROUND_GREEN("bg_green", Settings.SECTION_RENDERER, 0f),
EMPTY_SETTING("", "", 0.0f);
LARGE_SCREEN_PROPORTION(SettingKeys.large_screen_proportion(),Settings.SECTION_LAYOUT,2.25f),
SECOND_SCREEN_OPACITY(SettingKeys.custom_second_layer_opacity(), Settings.SECTION_RENDERER, 100f),
BACKGROUND_RED(SettingKeys.bg_red(), Settings.SECTION_RENDERER, 0f),
BACKGROUND_BLUE(SettingKeys.bg_blue(), Settings.SECTION_RENDERER, 0f),
BACKGROUND_GREEN(SettingKeys.bg_green(), Settings.SECTION_RENDERER, 0f);
override var float: Float = defaultValue

View File

@ -0,0 +1,52 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.model
enum class IntListSetting(
override val key: String,
override val section: String,
override val defaultValue: List<Int>,
val canBeEmpty: Boolean = true
) : AbstractListSetting<Int> {
LAYOUTS_TO_CYCLE("layouts_to_cycle", Settings.SECTION_LAYOUT, listOf(0, 1, 2, 3, 4, 5), canBeEmpty = false);
private var backingList: List<Int> = defaultValue
private var lastValidList : List<Int> = defaultValue
override var list: List<Int>
get() = backingList
set(value) {
if (!canBeEmpty && value.isEmpty()) {
backingList = lastValidList
} else {
backingList = value
lastValidList = value
}
}
override val valueAsString: String
get() = list.joinToString()
override val isRuntimeEditable: Boolean
get() {
for (setting in NOT_RUNTIME_EDITABLE) {
if (setting == this) {
return false
}
}
return true
}
companion object {
private val NOT_RUNTIME_EDITABLE: List<IntListSetting> = emptyList()
fun from(key: String): IntListSetting? =
values().firstOrNull { it.key == key }
fun clear() = values().forEach { it.list = it.defaultValue }
}
}

View File

@ -4,57 +4,59 @@
package org.citra.citra_emu.features.settings.model
import org.citra.citra_emu.features.settings.SettingKeys
enum class IntSetting(
override val key: String,
override val section: String,
override val defaultValue: Int
) : AbstractIntSetting {
FRAME_LIMIT("frame_limit", Settings.SECTION_RENDERER, 100),
EMULATED_REGION("region_value", Settings.SECTION_SYSTEM, -1),
INIT_CLOCK("init_clock", Settings.SECTION_SYSTEM, 0),
CAMERA_INNER_FLIP("camera_inner_flip", Settings.SECTION_CAMERA, 0),
CAMERA_OUTER_LEFT_FLIP("camera_outer_left_flip", Settings.SECTION_CAMERA, 0),
CAMERA_OUTER_RIGHT_FLIP("camera_outer_right_flip", Settings.SECTION_CAMERA, 0),
GRAPHICS_API("graphics_api", Settings.SECTION_RENDERER, 1),
RESOLUTION_FACTOR("resolution_factor", Settings.SECTION_RENDERER, 1),
STEREOSCOPIC_3D_MODE("render_3d", Settings.SECTION_RENDERER, 2),
STEREOSCOPIC_3D_DEPTH("factor_3d", Settings.SECTION_RENDERER, 0),
STEPS_PER_HOUR("steps_per_hour", Settings.SECTION_SYSTEM, 0),
CARDBOARD_SCREEN_SIZE("cardboard_screen_size", Settings.SECTION_LAYOUT, 85),
CARDBOARD_X_SHIFT("cardboard_x_shift", Settings.SECTION_LAYOUT, 0),
CARDBOARD_Y_SHIFT("cardboard_y_shift", Settings.SECTION_LAYOUT, 0),
SCREEN_LAYOUT("layout_option", Settings.SECTION_LAYOUT, 0),
SMALL_SCREEN_POSITION("small_screen_position",Settings.SECTION_LAYOUT,0),
LANDSCAPE_TOP_X("custom_top_x",Settings.SECTION_LAYOUT,0),
LANDSCAPE_TOP_Y("custom_top_y",Settings.SECTION_LAYOUT,0),
LANDSCAPE_TOP_WIDTH("custom_top_width",Settings.SECTION_LAYOUT,800),
LANDSCAPE_TOP_HEIGHT("custom_top_height",Settings.SECTION_LAYOUT,480),
LANDSCAPE_BOTTOM_X("custom_bottom_x",Settings.SECTION_LAYOUT,80),
LANDSCAPE_BOTTOM_Y("custom_bottom_y",Settings.SECTION_LAYOUT,480),
LANDSCAPE_BOTTOM_WIDTH("custom_bottom_width",Settings.SECTION_LAYOUT,640),
LANDSCAPE_BOTTOM_HEIGHT("custom_bottom_height",Settings.SECTION_LAYOUT,480),
SCREEN_GAP("screen_gap",Settings.SECTION_LAYOUT,0),
PORTRAIT_SCREEN_LAYOUT("portrait_layout_option",Settings.SECTION_LAYOUT,0),
SECONDARY_DISPLAY_LAYOUT("secondary_display_layout",Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_X("custom_portrait_top_x",Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_Y("custom_portrait_top_y",Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_WIDTH("custom_portrait_top_width",Settings.SECTION_LAYOUT,800),
PORTRAIT_TOP_HEIGHT("custom_portrait_top_height",Settings.SECTION_LAYOUT,480),
PORTRAIT_BOTTOM_X("custom_portrait_bottom_x",Settings.SECTION_LAYOUT,80),
PORTRAIT_BOTTOM_Y("custom_portrait_bottom_y",Settings.SECTION_LAYOUT,480),
PORTRAIT_BOTTOM_WIDTH("custom_portrait_bottom_width",Settings.SECTION_LAYOUT,640),
PORTRAIT_BOTTOM_HEIGHT("custom_portrait_bottom_height",Settings.SECTION_LAYOUT,480),
AUDIO_INPUT_TYPE("input_type", Settings.SECTION_AUDIO, 0),
CPU_CLOCK_SPEED("cpu_clock_percentage", Settings.SECTION_CORE, 100),
TEXTURE_FILTER("texture_filter", Settings.SECTION_RENDERER, 0),
TEXTURE_SAMPLING("texture_sampling", Settings.SECTION_RENDERER, 0),
USE_FRAME_LIMIT("use_frame_limit", Settings.SECTION_RENDERER, 1),
DELAY_RENDER_THREAD_US("delay_game_render_thread_us", Settings.SECTION_RENDERER, 0),
ORIENTATION_OPTION("screen_orientation", Settings.SECTION_LAYOUT, 2),
TURBO_LIMIT("turbo_limit", Settings.SECTION_CORE, 200),
PERFORMANCE_OVERLAY_POSITION("performance_overlay_position", Settings.SECTION_LAYOUT, 0),
RENDER_3D_WHICH_DISPLAY("render_3d_which_display",Settings.SECTION_RENDERER,0),
ASPECT_RATIO("aspect_ratio", Settings.SECTION_LAYOUT, 0);
FRAME_LIMIT(SettingKeys.frame_limit(), Settings.SECTION_RENDERER, 100),
EMULATED_REGION(SettingKeys.region_value(), Settings.SECTION_SYSTEM, -1),
INIT_CLOCK(SettingKeys.init_clock(), Settings.SECTION_SYSTEM, 0),
CAMERA_INNER_FLIP(SettingKeys.camera_inner_flip(), Settings.SECTION_CAMERA, 0),
CAMERA_OUTER_LEFT_FLIP(SettingKeys.camera_outer_left_flip(), Settings.SECTION_CAMERA, 0),
CAMERA_OUTER_RIGHT_FLIP(SettingKeys.camera_outer_right_flip(), Settings.SECTION_CAMERA, 0),
GRAPHICS_API(SettingKeys.graphics_api(), Settings.SECTION_RENDERER, 1),
RESOLUTION_FACTOR(SettingKeys.resolution_factor(), Settings.SECTION_RENDERER, 1),
STEREOSCOPIC_3D_MODE(SettingKeys.render_3d(), Settings.SECTION_RENDERER, 2),
STEREOSCOPIC_3D_DEPTH(SettingKeys.factor_3d(), Settings.SECTION_RENDERER, 0),
STEPS_PER_HOUR(SettingKeys.steps_per_hour(), Settings.SECTION_SYSTEM, 0),
CARDBOARD_SCREEN_SIZE(SettingKeys.cardboard_screen_size(), Settings.SECTION_LAYOUT, 85),
CARDBOARD_X_SHIFT(SettingKeys.cardboard_x_shift(), Settings.SECTION_LAYOUT, 0),
CARDBOARD_Y_SHIFT(SettingKeys.cardboard_y_shift(), Settings.SECTION_LAYOUT, 0),
SCREEN_LAYOUT(SettingKeys.layout_option(), Settings.SECTION_LAYOUT, 0),
SMALL_SCREEN_POSITION(SettingKeys.small_screen_position(),Settings.SECTION_LAYOUT,0),
LANDSCAPE_TOP_X(SettingKeys.custom_top_x(),Settings.SECTION_LAYOUT,0),
LANDSCAPE_TOP_Y(SettingKeys.custom_top_y(),Settings.SECTION_LAYOUT,0),
LANDSCAPE_TOP_WIDTH(SettingKeys.custom_top_width(),Settings.SECTION_LAYOUT,800),
LANDSCAPE_TOP_HEIGHT(SettingKeys.custom_top_height(),Settings.SECTION_LAYOUT,480),
LANDSCAPE_BOTTOM_X(SettingKeys.custom_bottom_x(),Settings.SECTION_LAYOUT,80),
LANDSCAPE_BOTTOM_Y(SettingKeys.custom_bottom_y(),Settings.SECTION_LAYOUT,480),
LANDSCAPE_BOTTOM_WIDTH(SettingKeys.custom_bottom_width(),Settings.SECTION_LAYOUT,640),
LANDSCAPE_BOTTOM_HEIGHT(SettingKeys.custom_bottom_height(),Settings.SECTION_LAYOUT,480),
SCREEN_GAP(SettingKeys.screen_gap(),Settings.SECTION_LAYOUT,0),
PORTRAIT_SCREEN_LAYOUT(SettingKeys.portrait_layout_option(),Settings.SECTION_LAYOUT,0),
SECONDARY_DISPLAY_LAYOUT(SettingKeys.secondary_display_layout(),Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_X(SettingKeys.custom_portrait_top_x(),Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_Y(SettingKeys.custom_portrait_top_y(),Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_WIDTH(SettingKeys.custom_portrait_top_width(),Settings.SECTION_LAYOUT,800),
PORTRAIT_TOP_HEIGHT(SettingKeys.custom_portrait_top_height(),Settings.SECTION_LAYOUT,480),
PORTRAIT_BOTTOM_X(SettingKeys.custom_portrait_bottom_x(),Settings.SECTION_LAYOUT,80),
PORTRAIT_BOTTOM_Y(SettingKeys.custom_portrait_bottom_y(),Settings.SECTION_LAYOUT,480),
PORTRAIT_BOTTOM_WIDTH(SettingKeys.custom_portrait_bottom_width(),Settings.SECTION_LAYOUT,640),
PORTRAIT_BOTTOM_HEIGHT(SettingKeys.custom_portrait_bottom_height(),Settings.SECTION_LAYOUT,480),
AUDIO_INPUT_TYPE(SettingKeys.input_type(), Settings.SECTION_AUDIO, 0),
CPU_CLOCK_SPEED(SettingKeys.cpu_clock_percentage(), Settings.SECTION_CORE, 100),
TEXTURE_FILTER(SettingKeys.texture_filter(), Settings.SECTION_RENDERER, 0),
TEXTURE_SAMPLING(SettingKeys.texture_sampling(), Settings.SECTION_RENDERER, 0),
USE_FRAME_LIMIT(SettingKeys.use_frame_limit(), Settings.SECTION_RENDERER, 1),
DELAY_RENDER_THREAD_US(SettingKeys.delay_game_render_thread_us(), Settings.SECTION_RENDERER, 0),
ORIENTATION_OPTION(SettingKeys.screen_orientation(), Settings.SECTION_LAYOUT, 2),
TURBO_LIMIT(SettingKeys.turbo_limit(), Settings.SECTION_CORE, 200),
PERFORMANCE_OVERLAY_POSITION(SettingKeys.performance_overlay_position(), Settings.SECTION_LAYOUT, 0),
RENDER_3D_WHICH_DISPLAY(SettingKeys.render_3d_which_display(),Settings.SECTION_RENDERER,0),
ASPECT_RATIO(SettingKeys.aspect_ratio(), Settings.SECTION_LAYOUT, 0);
override var int: Int = defaultValue

View File

@ -1,16 +1,18 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.model
import org.citra.citra_emu.features.settings.SettingKeys
enum class ScaledFloatSetting(
override val key: String,
override val section: String,
override val defaultValue: Float,
val scale: Int
) : AbstractFloatSetting {
AUDIO_VOLUME("volume", Settings.SECTION_AUDIO, 1.0f, 100);
AUDIO_VOLUME(SettingKeys.volume(), Settings.SECTION_AUDIO, 1.0f, 100);
override var float: Float = defaultValue
get() = field * scale

View File

@ -113,6 +113,7 @@ class Settings {
const val SECTION_CUSTOM_PORTRAIT = "Custom Portrait Layout"
const val SECTION_PERFORMANCE_OVERLAY = "Performance Overlay"
const val SECTION_STORAGE = "Storage"
const val SECTION_MISC = "Miscellaneous"
const val KEY_BUTTON_A = "button_a"
const val KEY_BUTTON_B = "button_b"
@ -135,6 +136,7 @@ class Settings {
const val KEY_CSTICK_AXIS_HORIZONTAL = "cstick_axis_horizontal"
const val KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical"
const val KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal"
const val HOTKEY_ENABLE = "hotkey_enable"
const val HOTKEY_SCREEN_SWAP = "hotkey_screen_swap"
const val HOTKEY_CYCLE_LAYOUT = "hotkey_toggle_layout"
const val HOTKEY_CLOSE_GAME = "hotkey_close_game"
@ -202,6 +204,7 @@ class Settings {
R.string.button_zr
)
val hotKeys = listOf(
HOTKEY_ENABLE,
HOTKEY_SCREEN_SWAP,
HOTKEY_CYCLE_LAYOUT,
HOTKEY_CLOSE_GAME,
@ -211,6 +214,7 @@ class Settings {
HOTKEY_TURBO_LIMIT
)
val hotkeyTitles = listOf(
R.string.controller_hotkey_enable_button,
R.string.emulation_swap_screens,
R.string.emulation_cycle_landscape_layouts,
R.string.emulation_close_game,
@ -220,6 +224,7 @@ class Settings {
R.string.turbo_limit_hotkey
)
// TODO: Move these in with the other setting keys in GenerateSettingKeys.cmake
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
const val PREF_MATERIAL_YOU = "MaterialYouTheme"
const val PREF_THEME_MODE = "ThemeMode"

View File

@ -1,21 +1,23 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.model
import org.citra.citra_emu.features.settings.SettingKeys
enum class StringSetting(
override val key: String,
override val section: String,
override val defaultValue: String
) : AbstractStringSetting {
INIT_TIME("init_time", Settings.SECTION_SYSTEM, "946731601"),
CAMERA_INNER_NAME("camera_inner_name", Settings.SECTION_CAMERA, "ndk"),
CAMERA_INNER_CONFIG("camera_inner_config", Settings.SECTION_CAMERA, "_front"),
CAMERA_OUTER_LEFT_NAME("camera_outer_left_name", Settings.SECTION_CAMERA, "ndk"),
CAMERA_OUTER_LEFT_CONFIG("camera_outer_left_config", Settings.SECTION_CAMERA, "_back"),
CAMERA_OUTER_RIGHT_NAME("camera_outer_right_name", Settings.SECTION_CAMERA, "ndk"),
CAMERA_OUTER_RIGHT_CONFIG("camera_outer_right_config", Settings.SECTION_CAMERA, "_back");
INIT_TIME(SettingKeys.init_time(), Settings.SECTION_SYSTEM, "946731601"),
CAMERA_INNER_NAME(SettingKeys.camera_inner_name(), Settings.SECTION_CAMERA, "ndk"),
CAMERA_INNER_CONFIG(SettingKeys.camera_inner_config(), Settings.SECTION_CAMERA, "_front"),
CAMERA_OUTER_LEFT_NAME(SettingKeys.camera_outer_left_name(), Settings.SECTION_CAMERA, "ndk"),
CAMERA_OUTER_LEFT_CONFIG(SettingKeys.camera_outer_left_config(), Settings.SECTION_CAMERA, "_back"),
CAMERA_OUTER_RIGHT_NAME(SettingKeys.camera_outer_right_name(), Settings.SECTION_CAMERA, "ndk"),
CAMERA_OUTER_RIGHT_CONFIG(SettingKeys.camera_outer_right_config(), Settings.SECTION_CAMERA, "_back");
override var string: String = defaultValue

View File

@ -9,6 +9,7 @@ import android.content.SharedPreferences
import android.view.InputDevice
import android.view.InputDevice.MotionRange
import android.view.KeyEvent
import android.view.MotionEvent
import android.widget.Toast
import androidx.preference.PreferenceManager
import org.citra.citra_emu.CitraApplication
@ -128,6 +129,7 @@ class InputBindingSetting(
Settings.KEY_BUTTON_DOWN -> NativeLibrary.ButtonType.DPAD_DOWN
Settings.KEY_BUTTON_LEFT -> NativeLibrary.ButtonType.DPAD_LEFT
Settings.KEY_BUTTON_RIGHT -> NativeLibrary.ButtonType.DPAD_RIGHT
Settings.HOTKEY_ENABLE -> Hotkey.ENABLE.button
Settings.HOTKEY_SCREEN_SWAP -> Hotkey.SWAP_SCREEN.button
Settings.HOTKEY_CYCLE_LAYOUT -> Hotkey.CYCLE_LAYOUT.button
Settings.HOTKEY_CLOSE_GAME -> Hotkey.CLOSE_GAME.button
@ -162,36 +164,40 @@ class InputBindingSetting(
fun removeOldMapping() {
// Try remove all possible keys we wrote for this setting
val oldKey = preferences.getString(reverseKey, "")
(setting as AbstractStringSetting).string = ""
if (oldKey != "") {
(setting as AbstractStringSetting).string = ""
preferences.edit()
.remove(abstractSetting.key) // Used for ui text
.remove(oldKey) // Used for button mapping
.remove(oldKey + "_GuestOrientation") // Used for axis orientation
.remove(oldKey + "_GuestButton") // Used for axis button
.remove(oldKey + "_Inverted") // used for axis inversion
.apply()
.remove(reverseKey)
val buttonCodes = try {
preferences.getStringSet(oldKey, mutableSetOf<String>())!!.toMutableSet()
} catch (e: ClassCastException) {
// if this is an int pref, either old button or an axis, so just remove it
preferences.edit().remove(oldKey).apply()
return;
}
buttonCodes.remove(buttonCode.toString());
preferences.edit().putStringSet(oldKey,buttonCodes).apply()
}
}
/**
* Helper function to write a gamepad button mapping for the setting.
*/
private fun writeButtonMapping(key: String) {
private fun writeButtonMapping(keyEvent: KeyEvent) {
val editor = preferences.edit()
// Remove mapping for another setting using this input
val oldButtonCode = preferences.getInt(key, -1)
if (oldButtonCode != -1) {
val oldKey = getButtonKey(oldButtonCode)
editor.remove(oldKey) // Only need to remove UI text setting, others will be overwritten
}
val key = getInputButtonKey(keyEvent)
// Pull in all codes associated with this key
// Migrate from the old int preference if need be
val buttonCodes = InputBindingSetting.getButtonSet(keyEvent)
buttonCodes.add(buttonCode)
// Cleanup old mapping for this setting
removeOldMapping()
// Write new mapping
editor.putInt(key, buttonCode)
editor.putStringSet(key, buttonCodes.mapTo(mutableSetOf()) {it.toString()})
// Write next reverse mapping for future cleanup
editor.putString(reverseKey, key)
@ -229,9 +235,8 @@ class InputBindingSetting(
}
val code = translateEventToKeyId(keyEvent)
writeButtonMapping(getInputButtonKey(code))
val uiString = "${keyEvent.device.name}: Button $code"
value = uiString
writeButtonMapping(keyEvent)
value = "${keyEvent.device.name}: ${getButtonName(code)}"
}
/**
@ -255,9 +260,10 @@ class InputBindingSetting(
} else {
buttonCode
}
writeAxisMapping(motionRange.axis, button, axisDir == '-')
val uiString = "${device.name}: Axis ${motionRange.axis}" + axisDir
value = uiString
// use UP (-) to map vertical, but use RIGHT (+) to map horizontal
val inverted = if (isHorizontalOrientation()) axisDir == '-' else axisDir == '+'
writeAxisMapping(motionRange.axis, button, inverted)
value = "Axis ${motionRange.axis}$axisDir"
}
override val type = TYPE_INPUT_BINDING
@ -265,6 +271,241 @@ class InputBindingSetting(
companion object {
private const val INPUT_MAPPING_PREFIX = "InputMapping"
private fun toTitleCase(raw: String): String =
raw.replace("_", " ").lowercase()
.split(" ").joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } }
private const val BUTTON_NAME_L3 = "Button L3"
private const val BUTTON_NAME_R3 = "Button R3"
private val buttonNameOverrides = mapOf(
KeyEvent.KEYCODE_BUTTON_THUMBL to BUTTON_NAME_L3,
KeyEvent.KEYCODE_BUTTON_THUMBR to BUTTON_NAME_R3,
LINUX_BTN_DPAD_UP to "Dpad Up",
LINUX_BTN_DPAD_DOWN to "Dpad Down",
LINUX_BTN_DPAD_LEFT to "Dpad Left",
LINUX_BTN_DPAD_RIGHT to "Dpad Right"
)
fun getButtonName(keyCode: Int): String =
buttonNameOverrides[keyCode]
?: toTitleCase(KeyEvent.keyCodeToString(keyCode).removePrefix("KEYCODE_"))
private data class DefaultButtonMapping(
val settingKey: String,
val hostKeyCode: Int,
val guestButtonCode: Int
)
// Auto-map always sets inverted = false. Users needing inverted axes should remap manually.
private data class DefaultAxisMapping(
val settingKey: String,
val hostAxis: Int,
val guestButton: Int,
val orientation: Int,
val inverted: Boolean
)
private val xboxFaceButtonMappings = listOf(
DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_A),
DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_B),
DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_X),
DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_Y)
)
private val nintendoFaceButtonMappings = listOf(
DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_A),
DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_B),
DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_X),
DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_Y)
)
private val commonButtonMappings = listOf(
DefaultButtonMapping(Settings.KEY_BUTTON_L, KeyEvent.KEYCODE_BUTTON_L1, NativeLibrary.ButtonType.TRIGGER_L),
DefaultButtonMapping(Settings.KEY_BUTTON_R, KeyEvent.KEYCODE_BUTTON_R1, NativeLibrary.ButtonType.TRIGGER_R),
DefaultButtonMapping(Settings.KEY_BUTTON_ZL, KeyEvent.KEYCODE_BUTTON_L2, NativeLibrary.ButtonType.BUTTON_ZL),
DefaultButtonMapping(Settings.KEY_BUTTON_ZR, KeyEvent.KEYCODE_BUTTON_R2, NativeLibrary.ButtonType.BUTTON_ZR),
DefaultButtonMapping(Settings.KEY_BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_SELECT, NativeLibrary.ButtonType.BUTTON_SELECT),
DefaultButtonMapping(Settings.KEY_BUTTON_START, KeyEvent.KEYCODE_BUTTON_START, NativeLibrary.ButtonType.BUTTON_START)
)
private val dpadButtonMappings = listOf(
DefaultButtonMapping(Settings.KEY_BUTTON_UP, KeyEvent.KEYCODE_DPAD_UP, NativeLibrary.ButtonType.DPAD_UP),
DefaultButtonMapping(Settings.KEY_BUTTON_DOWN, KeyEvent.KEYCODE_DPAD_DOWN, NativeLibrary.ButtonType.DPAD_DOWN),
DefaultButtonMapping(Settings.KEY_BUTTON_LEFT, KeyEvent.KEYCODE_DPAD_LEFT, NativeLibrary.ButtonType.DPAD_LEFT),
DefaultButtonMapping(Settings.KEY_BUTTON_RIGHT, KeyEvent.KEYCODE_DPAD_RIGHT, NativeLibrary.ButtonType.DPAD_RIGHT)
)
private val stickAxisMappings = listOf(
DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_X, NativeLibrary.ButtonType.STICK_LEFT, 0, false),
DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_VERTICAL, MotionEvent.AXIS_Y, NativeLibrary.ButtonType.STICK_LEFT, 1, false),
DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_HORIZONTAL, MotionEvent.AXIS_Z, NativeLibrary.ButtonType.STICK_C, 0, false),
DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_VERTICAL, MotionEvent.AXIS_RZ, NativeLibrary.ButtonType.STICK_C, 1, false)
)
private val dpadAxisMappings = listOf(
DefaultAxisMapping(Settings.KEY_DPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_HAT_X, NativeLibrary.ButtonType.DPAD, 0, false),
DefaultAxisMapping(Settings.KEY_DPAD_AXIS_VERTICAL, MotionEvent.AXIS_HAT_Y, NativeLibrary.ButtonType.DPAD, 1, false)
)
// Nintendo Switch Joy-Con specific mappings.
// Joy-Cons connected via Bluetooth on Android have several quirks:
// - They register as two separate InputDevices (left and right)
// - Android's evdev translation swaps A<->B (BTN_EAST->BUTTON_B, BTN_SOUTH->BUTTON_A)
// but does NOT swap X<->Y (BTN_NORTH->BUTTON_X, BTN_WEST->BUTTON_Y)
// - D-pad buttons arrive as KEYCODE_UNKNOWN (0) with Linux BTN_DPAD_* scan codes
// - Right stick uses AXIS_RX/AXIS_RY instead of AXIS_Z/AXIS_RZ
private const val NINTENDO_VENDOR_ID = 0x057e
// Linux BTN_DPAD_* values (0x220-0x223). Joy-Con D-pad buttons arrive as
// KEYCODE_UNKNOWN with these scan codes because Android's input layer doesn't
// translate them to KEYCODE_DPAD_*. translateEventToKeyId() falls back to
// the scan code in that case.
private const val LINUX_BTN_DPAD_UP = 0x220 // 544
private const val LINUX_BTN_DPAD_DOWN = 0x221 // 545
private const val LINUX_BTN_DPAD_LEFT = 0x222 // 546
private const val LINUX_BTN_DPAD_RIGHT = 0x223 // 547
// Joy-Con face buttons: A/B are swapped by Android's evdev layer, but X/Y are not.
// This is different from both the standard Xbox table (full swap) and the
// Nintendo table (no swap).
private val joyconFaceButtonMappings = listOf(
DefaultButtonMapping(Settings.KEY_BUTTON_A, KeyEvent.KEYCODE_BUTTON_B, NativeLibrary.ButtonType.BUTTON_A),
DefaultButtonMapping(Settings.KEY_BUTTON_B, KeyEvent.KEYCODE_BUTTON_A, NativeLibrary.ButtonType.BUTTON_B),
DefaultButtonMapping(Settings.KEY_BUTTON_X, KeyEvent.KEYCODE_BUTTON_X, NativeLibrary.ButtonType.BUTTON_X),
DefaultButtonMapping(Settings.KEY_BUTTON_Y, KeyEvent.KEYCODE_BUTTON_Y, NativeLibrary.ButtonType.BUTTON_Y)
)
// Joy-Con D-pad: uses Linux scan codes because Android reports BTN_DPAD_* as KEYCODE_UNKNOWN
private val joyconDpadButtonMappings = listOf(
DefaultButtonMapping(Settings.KEY_BUTTON_UP, LINUX_BTN_DPAD_UP, NativeLibrary.ButtonType.DPAD_UP),
DefaultButtonMapping(Settings.KEY_BUTTON_DOWN, LINUX_BTN_DPAD_DOWN, NativeLibrary.ButtonType.DPAD_DOWN),
DefaultButtonMapping(Settings.KEY_BUTTON_LEFT, LINUX_BTN_DPAD_LEFT, NativeLibrary.ButtonType.DPAD_LEFT),
DefaultButtonMapping(Settings.KEY_BUTTON_RIGHT, LINUX_BTN_DPAD_RIGHT, NativeLibrary.ButtonType.DPAD_RIGHT)
)
// Joy-Con sticks: left stick is AXIS_X/Y (standard), right stick is AXIS_RX/RY
// (not Z/RZ like most controllers). The horizontal axis is inverted relative to
// the standard orientation - verified empirically on paired Joy-Cons via Bluetooth.
private val joyconStickAxisMappings = listOf(
DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_HORIZONTAL, MotionEvent.AXIS_X, NativeLibrary.ButtonType.STICK_LEFT, 0, false),
DefaultAxisMapping(Settings.KEY_CIRCLEPAD_AXIS_VERTICAL, MotionEvent.AXIS_Y, NativeLibrary.ButtonType.STICK_LEFT, 1, false),
DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_HORIZONTAL, MotionEvent.AXIS_RX, NativeLibrary.ButtonType.STICK_C, 0, true),
DefaultAxisMapping(Settings.KEY_CSTICK_AXIS_VERTICAL, MotionEvent.AXIS_RY, NativeLibrary.ButtonType.STICK_C, 1, false)
)
/**
* Detects whether a device is a Nintendo Switch Joy-Con (as opposed to a
* Pro Controller or other Nintendo device) by checking vendor ID + device
* capabilities. Joy-Cons lack AXIS_HAT_X/Y and use AXIS_RX/RY for the
* right stick, while the Pro Controller has standard HAT axes and Z/RZ.
*/
fun isJoyCon(device: InputDevice?): Boolean {
if (device == null) return false
if (device.vendorId != NINTENDO_VENDOR_ID) return false
// Pro Controllers have HAT_X/HAT_Y (D-pad) and Z/RZ (right stick).
// Joy-Cons lack both: no HAT axes, right stick on RX/RY instead of Z/RZ.
var hasHatAxes = false
var hasStandardRightStick = false
for (range in device.motionRanges) {
when (range.axis) {
MotionEvent.AXIS_HAT_X, MotionEvent.AXIS_HAT_Y -> hasHatAxes = true
MotionEvent.AXIS_Z, MotionEvent.AXIS_RZ -> hasStandardRightStick = true
}
}
return !hasHatAxes && !hasStandardRightStick
}
private val allBindingKeys: Set<String> by lazy {
(Settings.buttonKeys + Settings.triggerKeys +
Settings.circlePadKeys + Settings.cStickKeys + Settings.dPadAxisKeys +
Settings.dPadButtonKeys).toSet()
}
fun clearAllBindings() {
val prefs = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
val editor = prefs.edit()
val allKeys = prefs.all.keys.toList()
for (key in allKeys) {
if (key.startsWith(INPUT_MAPPING_PREFIX) || key in allBindingKeys) {
editor.remove(key)
}
}
editor.apply()
}
private fun applyBindings(
buttonMappings: List<DefaultButtonMapping>,
axisMappings: List<DefaultAxisMapping>
) {
val prefs = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
val editor = prefs.edit()
buttonMappings.forEach { applyDefaultButtonMapping(editor, it) }
axisMappings.forEach { applyDefaultAxisMapping(editor, it) }
editor.apply()
}
/**
* Applies Joy-Con specific bindings: scan code D-pad, partial face button
* swap, and AXIS_RX/RY right stick.
*/
fun applyJoyConBindings() {
applyBindings(
joyconFaceButtonMappings + commonButtonMappings + joyconDpadButtonMappings,
joyconStickAxisMappings
)
}
/**
* Applies auto-mapped bindings based on detected controller layout and d-pad type.
*
* @param isNintendoLayout true if the controller uses Nintendo face button layout
* (A=east, B=south), false for Xbox layout (A=south, B=east)
* @param useAxisDpad true if the d-pad should be mapped as axis (HAT_X/HAT_Y),
* false if it should be mapped as individual button keycodes (DPAD_UP/DOWN/LEFT/RIGHT)
*/
fun applyAutoMapBindings(isNintendoLayout: Boolean, useAxisDpad: Boolean) {
val faceButtons = if (isNintendoLayout) nintendoFaceButtonMappings else xboxFaceButtonMappings
val buttonMappings = if (useAxisDpad) {
faceButtons + commonButtonMappings
} else {
faceButtons + commonButtonMappings + dpadButtonMappings
}
val axisMappings = if (useAxisDpad) {
stickAxisMappings + dpadAxisMappings
} else {
stickAxisMappings
}
applyBindings(buttonMappings, axisMappings)
}
private fun applyDefaultButtonMapping(
editor: SharedPreferences.Editor,
mapping: DefaultButtonMapping
) {
val prefKey = getInputButtonKey(mapping.hostKeyCode)
editor.putInt(prefKey, mapping.guestButtonCode)
editor.putString(mapping.settingKey, getButtonName(mapping.hostKeyCode))
editor.putString(
"${INPUT_MAPPING_PREFIX}_ReverseMapping_${mapping.settingKey}",
prefKey
)
}
private fun applyDefaultAxisMapping(
editor: SharedPreferences.Editor,
mapping: DefaultAxisMapping
) {
val axisKey = getInputAxisKey(mapping.hostAxis)
editor.putInt(getInputAxisOrientationKey(mapping.hostAxis), mapping.orientation)
editor.putInt(getInputAxisButtonKey(mapping.hostAxis), mapping.guestButton)
editor.putBoolean(getInputAxisInvertedKey(mapping.hostAxis), mapping.inverted)
val dir = if (mapping.orientation == 0) '+' else '-'
editor.putString(mapping.settingKey, "Axis ${mapping.hostAxis}$dir")
val reverseKey = "${INPUT_MAPPING_PREFIX}_ReverseMapping_${mapping.settingKey}_${mapping.orientation}"
editor.putString(reverseKey, axisKey)
}
/**
* Returns the settings key for the specified Citra button code.
*/
@ -287,19 +528,31 @@ class InputBindingSetting(
NativeLibrary.ButtonType.DPAD_RIGHT -> Settings.KEY_BUTTON_RIGHT
else -> ""
}
/**
* Helper function to get the settings key for an gamepad button.
*
* Get the mutable set of int button values this key should map to given an event
*/
@Deprecated("Use the new getInputButtonKey(keyEvent) method to handle unknown keys")
fun getInputButtonKey(keyCode: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyCode}"
fun getButtonSet(keyCode: KeyEvent):MutableSet<Int> {
val key = getInputButtonKey(keyCode)
val preferences = PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
var buttonCodes = try {
preferences.getStringSet(key, mutableSetOf<String>())
} catch (e: ClassCastException) {
val prefInt = preferences.getInt(key, -1);
val migratedSet = if (prefInt != -1) {
mutableSetOf(prefInt.toString())
} else {
mutableSetOf<String>()
}
migratedSet
}
if (buttonCodes == null) buttonCodes = mutableSetOf<String>()
return buttonCodes.mapNotNull { it.toIntOrNull() }.toMutableSet()
}
/**
* Helper function to get the settings key for an gamepad button.
*
*/
fun getInputButtonKey(event: KeyEvent): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${translateEventToKeyId(event)}"
private fun getInputButtonKey(keyId: Int): String = "${INPUT_MAPPING_PREFIX}_HostAxis_${keyId}"
/** Falls back to the scan code when keyCode is KEYCODE_UNKNOWN. */
fun getInputButtonKey(event: KeyEvent): String = getInputButtonKey(translateEventToKeyId(event))
/**
* Helper function to get the settings key for an gamepad axis.

View File

@ -0,0 +1,46 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.model.view
import org.citra.citra_emu.features.settings.model.AbstractSetting
import org.citra.citra_emu.features.settings.model.IntListSetting
class MultiChoiceSetting(
setting: AbstractSetting?,
titleId: Int,
descriptionId: Int,
val choicesId: Int,
val valuesId: Int,
val key: String? = null,
val defaultValue: List<Int>? = null,
override var isEnabled: Boolean = true
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_MULTI_CHOICE
val selectedValues: List<Int>
get() {
if (setting == null) {
return defaultValue!!
}
try {
val setting = setting as IntListSetting
return setting.list
}catch (_: ClassCastException) {
}
return defaultValue!!
}
/**
* Write a value to the backing list. If that int was previously null,
* initializes a new one and returns it, so it can be added to the Hashmap.
*
* @param selection New value of the int.
* @return the existing setting with the new value applied.
*/
fun setSelectedValue(selection: List<Int>): IntListSetting {
val intSetting = setting as IntListSetting
intSetting.list = selection
return intSetting
}
}

View File

@ -1,10 +1,11 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.model.view
import androidx.annotation.DrawableRes
import org.citra.citra_emu.activities.EmulationActivity
class RunnableSetting(
titleId: Int,
@ -12,7 +13,11 @@ class RunnableSetting(
val isRuntimeRunnable: Boolean,
@DrawableRes val iconId: Int = 0,
val runnable: () -> Unit,
val value: (() -> String)? = null
val value: (() -> String)? = null,
val onLongClick: (() -> Boolean)? = null
) : SettingsItem(null, titleId, descriptionId) {
override val type = TYPE_RUNNABLE
override val isEditable: Boolean
get() = if (EmulationActivity.isRunning()) isRuntimeRunnable else true
}

View File

@ -22,7 +22,7 @@ abstract class SettingsItem(
) {
abstract val type: Int
val isEditable: Boolean
open val isEditable: Boolean
get() {
if (!EmulationActivity.isRunning()) return true
return setting?.isRuntimeEditable ?: false
@ -47,5 +47,6 @@ abstract class SettingsItem(
const val TYPE_INPUT_BINDING = 8
const val TYPE_STRING_INPUT = 9
const val TYPE_FLOAT_INPUT = 10
const val TYPE_MULTI_CHOICE = 11
}
}

View File

@ -41,12 +41,14 @@ import org.citra.citra_emu.features.settings.model.AbstractIntSetting
import org.citra.citra_emu.features.settings.model.AbstractSetting
import org.citra.citra_emu.features.settings.model.AbstractStringSetting
import org.citra.citra_emu.features.settings.model.FloatSetting
import org.citra.citra_emu.features.settings.model.IntListSetting
import org.citra.citra_emu.features.settings.model.ScaledFloatSetting
import org.citra.citra_emu.features.settings.model.AbstractShortSetting
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
import org.citra.citra_emu.features.settings.model.view.SettingsItem
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting
import org.citra.citra_emu.features.settings.model.view.MultiChoiceSetting
import org.citra.citra_emu.features.settings.model.view.SliderSetting
import org.citra.citra_emu.features.settings.model.view.StringInputSetting
import org.citra.citra_emu.features.settings.model.view.StringSingleChoiceSetting
@ -55,6 +57,7 @@ import org.citra.citra_emu.features.settings.model.view.SwitchSetting
import org.citra.citra_emu.features.settings.ui.viewholder.DateTimeViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.HeaderViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.InputBindingSettingViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.MultiChoiceViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.RunnableViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.SettingViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.SingleChoiceViewHolder
@ -62,6 +65,7 @@ import org.citra.citra_emu.features.settings.ui.viewholder.SliderViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.StringInputViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.SubmenuViewHolder
import org.citra.citra_emu.features.settings.ui.viewholder.SwitchSettingViewHolder
import org.citra.citra_emu.fragments.AutoMapDialogFragment
import org.citra.citra_emu.fragments.MessageDialogFragment
import org.citra.citra_emu.fragments.MotionBottomSheetDialogFragment
import org.citra.citra_emu.utils.SystemSaveGame
@ -72,7 +76,8 @@ import kotlin.math.roundToInt
class SettingsAdapter(
private val fragmentView: SettingsFragmentView,
public val context: Context
) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener {
) : RecyclerView.Adapter<SettingViewHolder?>(), DialogInterface.OnClickListener,
DialogInterface.OnMultiChoiceClickListener {
private var settings: ArrayList<SettingsItem>? = null
private var clickedItem: SettingsItem? = null
private var clickedPosition: Int
@ -104,6 +109,10 @@ class SettingsAdapter(
SingleChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
SettingsItem.TYPE_MULTI_CHOICE -> {
MultiChoiceViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
SettingsItem.TYPE_SLIDER -> {
SliderViewHolder(ListItemSettingBinding.inflate(inflater), this)
}
@ -181,21 +190,30 @@ class SettingsAdapter(
SettingsItem.TYPE_SLIDER -> {
(oldItem as SliderSetting).isEnabled == (newItem as SliderSetting).isEnabled
}
SettingsItem.TYPE_SWITCH -> {
(oldItem as SwitchSetting).isEnabled == (newItem as SwitchSetting).isEnabled
}
SettingsItem.TYPE_SINGLE_CHOICE -> {
(oldItem as SingleChoiceSetting).isEnabled == (newItem as SingleChoiceSetting).isEnabled
}
SettingsItem.TYPE_MULTI_CHOICE -> {
(oldItem as MultiChoiceSetting).isEnabled == (newItem as MultiChoiceSetting).isEnabled
}
SettingsItem.TYPE_DATETIME_SETTING -> {
(oldItem as DateTimeSetting).isEnabled == (newItem as DateTimeSetting).isEnabled
}
SettingsItem.TYPE_STRING_SINGLE_CHOICE -> {
(oldItem as StringSingleChoiceSetting).isEnabled == (newItem as StringSingleChoiceSetting).isEnabled
}
SettingsItem.TYPE_STRING_INPUT -> {
(oldItem as StringInputSetting).isEnabled == (newItem as StringInputSetting).isEnabled
}
else -> {
oldItem == newItem
}
@ -214,7 +232,7 @@ class SettingsAdapter(
// If statement is required otherwise the app will crash on activity recreate ex. theme settings
if (fragmentView.activityView != null)
// Reload the settings list to update the UI
// Reload the settings list to update the UI
fragmentView.loadSettingsList()
}
@ -232,6 +250,27 @@ class SettingsAdapter(
onSingleChoiceClick(item)
}
private fun onMultiChoiceClick(item: MultiChoiceSetting) {
clickedItem = item
val value: BooleanArray = getSelectionForMultiChoiceValue(item);
dialog = MaterialAlertDialogBuilder(context)
.setTitle(item.nameId)
.setMultiChoiceItems(item.choicesId, value, this)
.setOnDismissListener {
if (clickedPosition != -1) {
notifyItemChanged(clickedPosition)
clickedPosition = -1
}
}
.show()
}
fun onMultiChoiceClick(item: MultiChoiceSetting, position: Int) {
clickedPosition = position
onMultiChoiceClick(item)
}
private fun onStringSingleChoiceClick(item: StringSingleChoiceSetting) {
clickedItem = item
dialog = context?.let {
@ -360,14 +399,14 @@ class SettingsAdapter(
sliderString = sliderProgress.roundToInt().toString()
if (textSliderValue?.text.toString() != sliderString) {
textSliderValue?.setText(sliderString)
textSliderValue?.setSelection(textSliderValue?.length() ?: 0 )
textSliderValue?.setSelection(textSliderValue?.length() ?: 0)
}
} else {
val currentText = textSliderValue?.text.toString()
val currentTextValue = currentText.toFloat()
if (currentTextValue != sliderProgress) {
textSliderValue?.setText(sliderString)
textSliderValue?.setSelection(textSliderValue?.length() ?: 0 )
textSliderValue?.setSelection(textSliderValue?.length() ?: 0)
}
}
}
@ -447,6 +486,7 @@ class SettingsAdapter(
}
it.setSelectedValue(value)
}
is AbstractShortSetting -> {
val value = getValueForSingleChoiceSelection(it, which).toShort()
if (it.selectedValue.toShort() != value) {
@ -454,6 +494,7 @@ class SettingsAdapter(
}
it.setSelectedValue(value)
}
else -> throw IllegalStateException("Unrecognized type used for SingleChoiceSetting!")
}
fragmentView?.putSetting(setting)
@ -499,11 +540,12 @@ class SettingsAdapter(
val setting = it.setSelectedValue(value)
fragmentView?.putSetting(setting)
}
else -> {
val setting = it.setSelectedValue(sliderProgress)
fragmentView?.putSetting(setting)
}
}
}
fragmentView.loadSettingsList()
closeDialog()
}
@ -519,7 +561,7 @@ class SettingsAdapter(
fragmentView?.putSetting(setting)
fragmentView.loadSettingsList()
closeDialog()
}
}
}
}
clickedItem = null
@ -527,6 +569,21 @@ class SettingsAdapter(
textInputValue = ""
}
//onclick for multichoice
override fun onClick(dialog: DialogInterface?, which: Int, isChecked: Boolean) {
val mcsetting = clickedItem as? MultiChoiceSetting
mcsetting?.let {
val value = getValueForMultiChoiceSelection(it, which)
if (it.selectedValues.contains(value) != isChecked) {
val setting = it.setSelectedValue((if (isChecked) it.selectedValues + value else it.selectedValues - value).sorted())
fragmentView?.putSetting(setting)
fragmentView?.onSettingChanged()
}
fragmentView.loadSettingsList()
}
}
fun onLongClick(setting: AbstractSetting, position: Int): Boolean {
MaterialAlertDialogBuilder(context)
.setMessage(R.string.reset_setting_confirmation)
@ -586,26 +643,42 @@ class SettingsAdapter(
).show((fragmentView as SettingsFragment).childFragmentManager, MessageDialogFragment.TAG)
}
fun onClickAutoMap() {
val activity = fragmentView.activityView as FragmentActivity
AutoMapDialogFragment.newInstance {
fragmentView.loadSettingsList()
fragmentView.onSettingChanged()
}.show(activity.supportFragmentManager, AutoMapDialogFragment.TAG)
}
fun onLongClickAutoMap(): Boolean {
showConfirmationDialog(R.string.controller_clear_all, R.string.controller_clear_all_confirm) {
InputBindingSetting.clearAllBindings()
fragmentView.loadSettingsList()
fragmentView.onSettingChanged()
}
return true
}
fun onClickRegenerateConsoleId() {
MaterialAlertDialogBuilder(context)
.setTitle(R.string.regenerate_console_id)
.setMessage(R.string.regenerate_console_id_description)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
SystemSaveGame.regenerateConsoleId()
notifyDataSetChanged()
}
.setNegativeButton(android.R.string.cancel, null)
.show()
showConfirmationDialog(R.string.regenerate_console_id, R.string.regenerate_console_id_description) {
SystemSaveGame.regenerateConsoleId()
notifyDataSetChanged()
}
}
fun onClickRegenerateMAC() {
showConfirmationDialog(R.string.regenerate_mac_address, R.string.regenerate_mac_address_description) {
SystemSaveGame.regenerateMac()
notifyDataSetChanged()
}
}
private fun showConfirmationDialog(titleId: Int, messageId: Int, onConfirm: () -> Unit) {
MaterialAlertDialogBuilder(context)
.setTitle(R.string.regenerate_mac_address)
.setMessage(R.string.regenerate_mac_address_description)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
SystemSaveGame.regenerateMac()
notifyDataSetChanged()
}
.setTitle(titleId)
.setMessage(messageId)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int -> onConfirm() }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
@ -631,6 +704,16 @@ class SettingsAdapter(
}
}
private fun getValueForMultiChoiceSelection(item: MultiChoiceSetting, which: Int): Int {
val valuesId = item.valuesId
return if (valuesId > 0) {
val valuesArray = context.resources.getIntArray(valuesId)
valuesArray[which]
} else {
which
}
}
private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
val value = item.selectedValue
val valuesId = item.valuesId
@ -647,4 +730,20 @@ class SettingsAdapter(
}
return -1
}
private fun getSelectionForMultiChoiceValue(item: MultiChoiceSetting): BooleanArray {
val value = item.selectedValues;
val valuesId = item.valuesId;
if (valuesId > 0) {
val valuesArray = context.resources.getIntArray(valuesId);
val res = BooleanArray(valuesArray.size){false}
for (index in valuesArray.indices) {
if (value.contains(valuesArray[index])) {
res[index] = true;
}
}
return res;
}
return BooleanArray(1){false};
}
}

View File

@ -14,6 +14,7 @@ import android.os.Build
import android.text.TextUtils
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.serialization.builtins.IntArraySerializer
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.R
import org.citra.citra_emu.display.ScreenLayout
@ -27,12 +28,14 @@ import org.citra.citra_emu.features.settings.model.AbstractStringSetting
import org.citra.citra_emu.features.settings.model.BooleanSetting
import org.citra.citra_emu.features.settings.model.FloatSetting
import org.citra.citra_emu.features.settings.model.IntSetting
import org.citra.citra_emu.features.settings.model.IntListSetting
import org.citra.citra_emu.features.settings.model.ScaledFloatSetting
import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.features.settings.model.StringSetting
import org.citra.citra_emu.features.settings.model.view.DateTimeSetting
import org.citra.citra_emu.features.settings.model.view.HeaderSetting
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
import org.citra.citra_emu.features.settings.model.view.MultiChoiceSetting
import org.citra.citra_emu.features.settings.model.view.RunnableSetting
import org.citra.citra_emu.features.settings.model.view.SettingsItem
import org.citra.citra_emu.features.settings.model.view.SingleChoiceSetting
@ -776,6 +779,16 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
private fun addControlsSettings(sl: ArrayList<SettingsItem>) {
settingsActivity.setToolbarTitle(settingsActivity.getString(R.string.preferences_controls))
sl.apply {
add(
RunnableSetting(
R.string.controller_auto_map,
R.string.controller_auto_map_description,
true,
R.drawable.ic_controller,
{ settingsAdapter.onClickAutoMap() },
onLongClick = { settingsAdapter.onLongClickAutoMap() }
)
)
add(HeaderSetting(R.string.generic_buttons))
Settings.buttonKeys.forEachIndexed { i: Int, key: String ->
val button = getInputObject(key)
@ -811,7 +824,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
add(InputBindingSetting(button, Settings.triggerTitles[i]))
}
add(HeaderSetting(R.string.controller_hotkeys))
add(HeaderSetting(R.string.controller_hotkeys,R.string.controller_hotkeys_description))
Settings.hotKeys.forEachIndexed { i: Int, key: String ->
val button = getInputObject(key)
add(InputBindingSetting(button, Settings.hotkeyTitles[i]))
@ -898,6 +911,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
IntSetting.RESOLUTION_FACTOR.key,
IntSetting.RESOLUTION_FACTOR.defaultValue
)
)
add(
SwitchSetting(
BooleanSetting.USE_INTEGER_SCALING,
R.string.use_integer_scaling,
R.string.use_integer_scaling_description,
BooleanSetting.USE_INTEGER_SCALING.key,
BooleanSetting.USE_INTEGER_SCALING.defaultValue
)
)
add(
SwitchSetting(
@ -1148,6 +1170,17 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
BooleanSetting.UPRIGHT_SCREEN.defaultValue
)
)
add(
MultiChoiceSetting(
IntListSetting.LAYOUTS_TO_CYCLE,
R.string.layouts_to_cycle,
R.string.layouts_to_cycle_description,
R.array.landscapeLayouts,
R.array.landscapeLayoutValues,
IntListSetting.LAYOUTS_TO_CYCLE.key,
IntListSetting.LAYOUTS_TO_CYCLE.defaultValue
)
)
add(
SingleChoiceSetting(
IntSetting.PORTRAIT_SCREEN_LAYOUT,
@ -1242,7 +1275,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
override val section = null
override val isRuntimeEditable = false
override val valueAsString = int.toString()
override val defaultValue = FloatSetting.BACKGROUND_RED.defaultValue
override val defaultValue = FloatSetting.BACKGROUND_RED.defaultValue.toInt()
}
add(
SliderSetting(
@ -1265,7 +1298,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
override val section = null
override val isRuntimeEditable = false
override val valueAsString = int.toString()
override val defaultValue = FloatSetting.BACKGROUND_GREEN.defaultValue
override val defaultValue = FloatSetting.BACKGROUND_GREEN.defaultValue.toInt()
}
add(
SliderSetting(
@ -1288,7 +1321,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
override val section = null
override val isRuntimeEditable = false
override val valueAsString = int.toString()
override val defaultValue = FloatSetting.BACKGROUND_BLUE.defaultValue
override val defaultValue = FloatSetting.BACKGROUND_BLUE.defaultValue.toInt()
}
add(
SliderSetting(
@ -1784,6 +1817,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
BooleanSetting.ENABLE_RPC_SERVER.defaultValue
)
)
add(
SwitchSetting(
BooleanSetting.TOGGLE_UNIQUE_DATA_CONSOLE_TYPE,
R.string.toggle_unique_data_console_type,
R.string.toggle_unique_data_console_type_desc,
BooleanSetting.TOGGLE_UNIQUE_DATA_CONSOLE_TYPE.key,
BooleanSetting.TOGGLE_UNIQUE_DATA_CONSOLE_TYPE.defaultValue
)
)
add(
SwitchSetting(
BooleanSetting.DELAY_START_LLE_MODULES,

View File

@ -0,0 +1,80 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.settings.ui.viewholder
import android.view.View
import org.citra.citra_emu.databinding.ListItemSettingBinding
import org.citra.citra_emu.features.settings.model.view.SettingsItem
import org.citra.citra_emu.features.settings.model.view.MultiChoiceSetting
import org.citra.citra_emu.features.settings.ui.SettingsAdapter
class MultiChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
private lateinit var setting: SettingsItem
override fun bind(item: SettingsItem) {
setting = item
binding.textSettingName.setText(item.nameId)
if (item.descriptionId != 0) {
binding.textSettingDescription.visibility = View.VISIBLE
binding.textSettingDescription.setText(item.descriptionId)
} else {
binding.textSettingDescription.visibility = View.GONE
}
binding.textSettingValue.visibility = View.VISIBLE
binding.textSettingValue.text = getTextSetting()
if (setting.isActive) {
binding.textSettingName.alpha = 1f
binding.textSettingDescription.alpha = 1f
binding.textSettingValue.alpha = 1f
} else {
binding.textSettingName.alpha = 0.5f
binding.textSettingDescription.alpha = 0.5f
binding.textSettingValue.alpha = 0.5f
}
}
private fun getTextSetting(): String {
when (val item = setting) {
is MultiChoiceSetting -> {
val resMgr = binding.textSettingDescription.context.resources
val values = resMgr.getIntArray(item.valuesId)
var resList:List<String> = emptyList();
values.forEachIndexed { i: Int, value: Int ->
if ((setting as MultiChoiceSetting).selectedValues.contains(value)) {
resList = resList + resMgr.getStringArray(item.choicesId)[i];
}
}
return resList.joinToString();
}
else -> return ""
}
}
override fun onClick(clicked: View) {
if (!setting.isEditable || !setting.isEnabled) {
adapter.onClickDisabledSetting(!setting.isEditable)
return
}
if (setting is MultiChoiceSetting) {
adapter.onMultiChoiceClick(
(setting as MultiChoiceSetting),
bindingAdapterPosition
)
}
}
override fun onLongClick(clicked: View): Boolean {
if (setting.isActive) {
return adapter.onLongClick(setting.setting!!, bindingAdapterPosition)
} else {
adapter.onClickDisabledSetting(!setting.isEditable)
}
return false
}
}

View File

@ -67,7 +67,10 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
}
override fun onLongClick(clicked: View): Boolean {
// no-op
return true
if (!setting.isEditable) {
adapter.onClickDisabledSetting(true)
return true
}
return setting.onLongClick?.invoke() ?: true
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -12,6 +12,7 @@ import org.citra.citra_emu.R
import org.citra.citra_emu.features.settings.model.AbstractSetting
import org.citra.citra_emu.features.settings.model.BooleanSetting
import org.citra.citra_emu.features.settings.model.FloatSetting
import org.citra.citra_emu.features.settings.model.IntListSetting
import org.citra.citra_emu.features.settings.model.IntSetting
import org.citra.citra_emu.features.settings.model.ScaledFloatSetting
import org.citra.citra_emu.features.settings.model.SettingSection
@ -255,6 +256,11 @@ object SettingsFile {
return stringSetting
}
val intListSetting = IntListSetting.from(key)
if (intListSetting != null) {
intListSetting.list = value.split(", ").map { it.toInt() }
}
return null
}

View File

@ -0,0 +1,152 @@
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.fragments
import android.os.Bundle
import android.view.InputDevice
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import org.citra.citra_emu.R
import org.citra.citra_emu.databinding.DialogAutoMapBinding
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
import org.citra.citra_emu.utils.Log
/**
* Captures a single button press to detect controller layout (Xbox vs Nintendo)
* and d-pad type (axis vs button), then applies the appropriate bindings.
*/
class AutoMapDialogFragment : BottomSheetDialogFragment() {
private var _binding: DialogAutoMapBinding? = null
private val binding get() = _binding!!
private var onComplete: (() -> Unit)? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = DialogAutoMapBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
BottomSheetBehavior.from<View>(view.parent as View).state =
BottomSheetBehavior.STATE_EXPANDED
isCancelable = false
view.requestFocus()
view.setOnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
binding.textTitle.setText(R.string.controller_auto_map)
binding.textMessage.setText(R.string.auto_map_prompt)
binding.imageFaceButtons.setImageResource(R.drawable.automap_face_buttons)
dialog?.setOnKeyListener { _, _, event -> onKeyEvent(event) }
binding.buttonCancel.setOnClickListener {
dismiss()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
private fun onKeyEvent(event: KeyEvent): Boolean {
if (event.action != KeyEvent.ACTION_UP) return false
val keyCode = event.keyCode
val device = event.device
// Check if this is a Nintendo Switch Joy-Con (not Pro Controller).
// Joy-Cons have unique quirks: split devices, non-standard D-pad scan codes,
// partial A/B swap but no X/Y swap from Android's evdev layer.
val isJoyCon = InputBindingSetting.isJoyCon(device)
if (isJoyCon) {
Log.info("[AutoMap] Detected Joy-Con - using Joy-Con mappings")
InputBindingSetting.clearAllBindings()
InputBindingSetting.applyJoyConBindings()
onComplete?.invoke()
dismiss()
return true
}
// For non-Joy-Con controllers, determine layout from which keycode arrives
// for the east/right position.
// The user is pressing the button in the "A" (east/right) position on the 3DS diamond.
// Xbox layout: east position sends KEYCODE_BUTTON_B (97)
// Nintendo layout: east position sends KEYCODE_BUTTON_A (96)
val isNintendoLayout = when (keyCode) {
KeyEvent.KEYCODE_BUTTON_A -> true
KeyEvent.KEYCODE_BUTTON_B -> false
else -> {
// Unrecognized button - ignore and wait for a valid press
Log.warning("[AutoMap] Ignoring unrecognized keycode $keyCode, waiting for A or B")
return true
}
}
val layoutName = if (isNintendoLayout) "Nintendo" else "Xbox"
Log.info("[AutoMap] Detected $layoutName layout (keyCode=$keyCode)")
val useAxisDpad = detectDpadType(device)
val dpadName = if (useAxisDpad) "axis" else "button"
Log.info("[AutoMap] Detected $dpadName d-pad (device=${device?.name})")
InputBindingSetting.clearAllBindings()
InputBindingSetting.applyAutoMapBindings(isNintendoLayout, useAxisDpad)
onComplete?.invoke()
dismiss()
return true
}
companion object {
const val TAG = "AutoMapDialogFragment"
fun newInstance(
onComplete: () -> Unit
): AutoMapDialogFragment {
val dialog = AutoMapDialogFragment()
dialog.onComplete = onComplete
return dialog
}
/**
* Returns true for axis d-pad (HAT_X/HAT_Y), false for button d-pad (DPAD_UP/DOWN/LEFT/RIGHT).
* Prefers axis when both are present. Defaults to axis if detection fails.
*/
private fun detectDpadType(device: InputDevice?): Boolean {
if (device == null) return true
val hasAxisDpad = device.motionRanges.any {
it.axis == MotionEvent.AXIS_HAT_X || it.axis == MotionEvent.AXIS_HAT_Y
}
if (hasAxisDpad) return true
val dpadKeyCodes = intArrayOf(
KeyEvent.KEYCODE_DPAD_UP,
KeyEvent.KEYCODE_DPAD_DOWN,
KeyEvent.KEYCODE_DPAD_LEFT,
KeyEvent.KEYCODE_DPAD_RIGHT
)
val hasButtonDpad = device.hasKeys(*dpadKeyCodes).any { it }
return !hasButtonDpad
}
}
}

View File

@ -11,12 +11,14 @@ import android.content.DialogInterface
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.content.res.Configuration
import android.net.Uri
import android.os.BatteryManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.ParcelFileDescriptor
import android.os.SystemClock
import android.text.Editable
import android.text.TextWatcher
@ -72,6 +74,7 @@ import org.citra.citra_emu.features.settings.model.SettingsViewModel
import org.citra.citra_emu.features.settings.ui.SettingsActivity
import org.citra.citra_emu.features.settings.utils.SettingsFile
import org.citra.citra_emu.model.Game
import org.citra.citra_emu.utils.BuildUtil
import org.citra.citra_emu.utils.DirectoryInitialization
import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState
import org.citra.citra_emu.utils.EmulationMenuSettings
@ -107,6 +110,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
private val onPause = Runnable{ togglePause() }
private val onShutdown = Runnable{ emulationState.stop() }
// Only used if a game is passed through intent on google play variant
private var gameFd: Int? = null
override fun onAttach(context: Context) {
super.onAttach(context)
if (context is EmulationActivity) {
@ -124,25 +130,37 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
super.onCreate(savedInstanceState)
val intent = requireActivity().intent
val intentUri: Uri? = intent.data
var intentUri: Uri? = intent.data
val oldIntentInfo = Pair(
intent.getStringExtra("SelectedGame"),
intent.getStringExtra("SelectedTitle")
)
var intentGame: Game? = null
intentUri = if (intentUri == null && oldIntentInfo.first != null) {
Uri.parse(oldIntentInfo.first)
} else {
intentUri
}
if (intentUri != null) {
intentGame = if (Game.extensions.contains(FileUtil.getExtension(intentUri))) {
GameHelper.getGame(intentUri, isInstalled = false, addedToLibrary = false)
} else {
null
}
} else if (oldIntentInfo.first != null) {
val gameUri = Uri.parse(oldIntentInfo.first)
intentGame = if (Game.extensions.contains(FileUtil.getExtension(gameUri))) {
GameHelper.getGame(gameUri, isInstalled = false, addedToLibrary = false)
} else {
null
if (!BuildUtil.isGooglePlayBuild) {
val intentUriString = intentUri.toString()
// We need to build a special path as the incoming URI may be SAF exclusive
Log.warning("[EmulationFragment] Cannot determine native path of URI \"" +
intentUriString + "\", using file descriptor instead.")
if (!intentUriString.startsWith("!")) {
gameFd = requireContext().contentResolver.openFileDescriptor(intentUri, "r")?.detachFd()
intentUri = if (gameFd != null) {
Uri.parse("fd://" + gameFd.toString())
} else {
null
}
}
}
intentGame =
intentUri?.let {
// isInstalled, addedToLibrary and mediaType do not matter here
GameHelper.getGame(it, isInstalled = false, addedToLibrary = false, mediaType = Game.MediaType.GAME_CARD)
}
}
val insertedCartridge = preferences.getString("insertedCartridge", "")
@ -160,6 +178,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
return
}
Log.info("[EmulationFragment] Starting application " + game.path)
// So this fragment doesn't restart on configuration changes; i.e. rotation.
retainInstance = true
emulationState = EmulationState(game.path)
@ -175,6 +195,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
savedInstanceState: Bundle?
): View {
_binding = FragmentEmulationBinding.inflate(inflater)
binding.inGameMenu.menu.findItem(R.id.menu_landscape_screen_layout).isVisible =
CitraApplication.appContext.resources.configuration.orientation !=
Configuration.ORIENTATION_PORTRAIT
binding.inGameMenu.menu.findItem(R.id.menu_portrait_screen_layout).isVisible =
CitraApplication.appContext.resources.configuration.orientation ==
Configuration.ORIENTATION_PORTRAIT
return binding.root
}
@ -479,7 +505,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
super.onResume()
Choreographer.getInstance().postFrameCallback(this)
if (NativeLibrary.isRunning()) {
emulationState.pause()
emulationState.unpause()
// If the overlay is enabled, we need to update the position if changed
val position = IntSetting.PERFORMANCE_OVERLAY_POSITION.int
@ -519,6 +545,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
override fun onDestroy() {
EmulationLifecycleUtil.removeHook(onPause)
EmulationLifecycleUtil.removeHook(onShutdown)
if (gameFd != null) {
ParcelFileDescriptor.adoptFd(gameFd!!).close()
gameFd = null
}
super.onDestroy()
}
@ -845,7 +875,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_emulation_amiibo_load -> {
emulationActivity.openFileLauncher.launch(false)
emulationActivity.openAmiiboFileLauncher.launch(false)
true
}

View File

@ -7,16 +7,12 @@ package org.citra.citra_emu.fragments
import android.annotation.SuppressLint
import android.net.Uri
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.core.text.HtmlCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
@ -40,6 +36,7 @@ import org.citra.citra_emu.adapters.GameAdapter
import org.citra.citra_emu.databinding.FragmentGamesBinding
import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.model.Game
import org.citra.citra_emu.utils.BuildUtil
import org.citra.citra_emu.viewmodel.CompressProgressDialogViewModel
import org.citra.citra_emu.viewmodel.GamesViewModel
import org.citra.citra_emu.viewmodel.HomeViewModel
@ -50,7 +47,6 @@ class GamesFragment : Fragment() {
private val gamesViewModel: GamesViewModel by activityViewModels()
private val homeViewModel: HomeViewModel by activityViewModels()
private var show3DSFileWarning: Boolean = true
private lateinit var gameAdapter: GameAdapter
private val openImageLauncher = registerForActivityResult(
@ -65,8 +61,14 @@ class GamesFragment : Fragment() {
companion object {
fun doCompression(fragment: Fragment, gamesViewModel: GamesViewModel, inputPath: String?, outputUri: Uri?, shouldCompress: Boolean) {
if (outputUri != null) {
val outputPath: String =
if (!BuildUtil.isGooglePlayBuild) {
"!" + NativeLibrary.getNativePath(outputUri)
} else {
outputUri.toString()
}
CompressProgressDialogViewModel.reset()
val dialog = CompressProgressDialogFragment.newInstance(shouldCompress, outputUri.toString())
val dialog = CompressProgressDialogFragment.newInstance(shouldCompress, outputPath)
dialog.showNow(
fragment.requireActivity().supportFragmentManager,
CompressProgressDialogFragment.TAG
@ -74,9 +76,9 @@ class GamesFragment : Fragment() {
fragment.lifecycleScope.launch(Dispatchers.IO) {
val status = if (shouldCompress) {
NativeLibrary.compressFile(inputPath, outputUri.toString())
NativeLibrary.compressFile(inputPath, outputPath)
} else {
NativeLibrary.decompressFile(inputPath, outputUri.toString())
NativeLibrary.decompressFile(inputPath, outputPath)
}
fragment.requireActivity().runOnUiThread {
@ -224,34 +226,6 @@ class GamesFragment : Fragment() {
setInsets()
}
override fun onResume() {
super.onResume()
if (show3DSFileWarning &&
!PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
.getBoolean("show_3ds_files_warning", false)) {
val message = HtmlCompat.fromHtml(getString(R.string.warning_3ds_files),
HtmlCompat.FROM_HTML_MODE_LEGACY)
context?.let {
val alert = MaterialAlertDialogBuilder(it)
.setTitle(getString(R.string.important))
.setMessage(message)
.setPositiveButton(R.string.dont_show_again) { _, _ ->
PreferenceManager.getDefaultSharedPreferences(CitraApplication.appContext)
.edit() {
putBoolean("show_3ds_files_warning", true)
}
}
.show()
val alertMessage = alert.findViewById<View>(android.R.id.message) as TextView
alertMessage.movementMethod = LinkMovementMethod.getInstance()
}
}
show3DSFileWarning = false
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null

View File

@ -33,7 +33,7 @@ import org.citra.citra_emu.adapters.HomeSettingAdapter
import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding
import org.citra.citra_emu.databinding.FragmentHomeSettingsBinding
import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.features.settings.model.StringSetting
import org.citra.citra_emu.features.settings.SettingKeys
import org.citra.citra_emu.features.settings.ui.SettingsActivity
import org.citra.citra_emu.features.settings.utils.SettingsFile
import org.citra.citra_emu.model.Game
@ -89,7 +89,7 @@ class HomeSettingsFragment : Fragment() {
{
val inflater = LayoutInflater.from(context)
val inputBinding = DialogSoftwareKeyboardBinding.inflate(inflater)
var textInputValue: String = preferences.getString("last_artic_base_addr", "")!!
var textInputValue: String = preferences.getString(SettingKeys.last_artic_base_addr(), "")!!
inputBinding.editTextInput.setText(textInputValue)
inputBinding.editTextInput.doOnTextChanged { text, _, _, _ ->
@ -103,7 +103,7 @@ class HomeSettingsFragment : Fragment() {
.setPositiveButton(android.R.string.ok) { _, _ ->
if (textInputValue.isNotEmpty()) {
preferences.edit()
.putString("last_artic_base_addr", textInputValue)
.putString(SettingKeys.last_artic_base_addr(), textInputValue)
.apply()
val menu = Game(
title = getString(R.string.artic_base),

View File

@ -23,7 +23,6 @@ import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
@ -32,7 +31,6 @@ import androidx.preference.PreferenceManager
import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.transition.MaterialFadeThrough
import org.citra.citra_emu.BuildConfig
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
@ -92,23 +90,20 @@ class SetupFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mainActivity = requireActivity() as MainActivity
homeViewModel.setNavigationVisibility(visible = false, animated = false)
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (binding.viewPager2.currentItem > 0) {
pageBackward()
} else {
requireActivity().finish()
}
}
homeViewModel.selectedCitraDirectoryLiveData.observe(viewLifecycleOwner) { uri ->
if (uri == null) {
return@observe
}
)
requireActivity().window.navigationBarColor =
ContextCompat.getColor(requireContext(), android.R.color.transparent)
onOpenCitraDirectory(uri)
homeViewModel.selectedCitraDirectory = null
}
homeViewModel.selectedGamesDirectoryLiveData.observe(viewLifecycleOwner) { uri ->
if (uri == null) {
return@observe
}
onGetGamesDirectory(uri)
homeViewModel.selectedGamesDirectory = null
}
pages = mutableListOf()
pages.apply {
@ -320,7 +315,7 @@ class SetupFragment : Fragment() {
R.string.select_citra_user_folder_description,
buttonAction = {
pageButtonCallback = it
PermissionsHandler.compatibleSelectDirectory(openCitraDirectory)
PermissionsHandler.compatibleSelectDirectory(mainActivity.setupOpenCitraDirectory)
},
buttonState = {
if (PermissionsHandler.hasWriteAccess(requireContext())) {
@ -342,9 +337,9 @@ class SetupFragment : Fragment() {
R.drawable.ic_controller,
R.string.games,
R.string.games_description,
buttonAction = {
buttonAction = {
pageButtonCallback = it
getGamesDirectory.launch(
mainActivity.setupGetGamesDirectory.launch(
Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data
)
},
@ -409,27 +404,33 @@ class SetupFragment : Fragment() {
}
binding.viewPager2.registerOnPageChangeCallback(object : OnPageChangeCallback() {
var previousPosition: Int = 0
override fun onPageSelected(position: Int) {
super.onPageSelected(position)
if (position == 1 && previousPosition == 0) {
ViewUtils.showView(binding.buttonNext)
ViewUtils.showView(binding.buttonBack)
} else if (position == 0 && previousPosition == 1) {
ViewUtils.hideView(binding.buttonBack)
ViewUtils.hideView(binding.buttonNext)
} else if (position == pages.size - 1 && previousPosition == pages.size - 2) {
ViewUtils.hideView(binding.buttonNext)
} else if (position == pages.size - 2 && previousPosition == pages.size - 1) {
ViewUtils.showView(binding.buttonNext)
}
previousPosition = position
updateNavigationButtons(position)
}
})
homeViewModel.setNavigationVisibility(visible = false, animated = false)
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (binding.viewPager2.currentItem > 0) {
pageBackward()
} else {
requireActivity().finish()
}
}
}
)
binding.viewPager2.currentItem = homeViewModel.setupCurrentPage
requireActivity().window.navigationBarColor =
ContextCompat.getColor(requireContext(), android.R.color.transparent)
binding.buttonNext.setOnClickListener {
val index = binding.viewPager2.currentItem
val currentPage = pages[index]
@ -479,29 +480,23 @@ class SetupFragment : Fragment() {
}
binding.buttonBack.setOnClickListener { pageBackward() }
if (savedInstanceState != null) {
val nextIsVisible = savedInstanceState.getBoolean(KEY_NEXT_VISIBILITY)
val backIsVisible = savedInstanceState.getBoolean(KEY_BACK_VISIBILITY)
hasBeenWarned = savedInstanceState.getBooleanArray(KEY_HAS_BEEN_WARNED)!!
if (nextIsVisible) {
binding.buttonNext.visibility = View.VISIBLE
}
if (backIsVisible) {
binding.buttonBack.visibility = View.VISIBLE
}
} else {
if (savedInstanceState == null) {
hasBeenWarned = BooleanArray(pages.size)
} else {
hasBeenWarned = savedInstanceState.getBooleanArray(KEY_HAS_BEEN_WARNED) ?: BooleanArray(pages.size)
}
updateNavigationButtons(binding.viewPager2.currentItem)
setInsets()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(KEY_NEXT_VISIBILITY, binding.buttonNext.isVisible)
outState.putBoolean(KEY_BACK_VISIBILITY, binding.buttonBack.isVisible)
outState.putBooleanArray(KEY_HAS_BEEN_WARNED, hasBeenWarned)
if (::hasBeenWarned.isInitialized) {
outState.putBooleanArray(KEY_HAS_BEEN_WARNED, hasBeenWarned)
}
}
override fun onDestroyView() {
@ -510,15 +505,39 @@ class SetupFragment : Fragment() {
}
private lateinit var pageButtonCallback: SetupCallback
private val checkForButtonState: () -> Unit = {
val page = pages[binding.viewPager2.currentItem]
page.pageButtons?.forEach {
if (it.buttonState() == ButtonState.BUTTON_ACTION_COMPLETE) {
pageButtonCallback.onStepCompleted(it.titleId, pageFullyCompleted = false)
}
if (page.pageSteps() == PageState.PAGE_STEPS_COMPLETE) {
pageButtonCallback.onStepCompleted(0, pageFullyCompleted = true)
private fun updateNavigationButtons(position: Int) {
if (position == 0) {
ViewUtils.hideView(binding.buttonBack)
} else {
ViewUtils.showView(binding.buttonBack)
}
if (position == 0 || position == pages.size - 1) {
ViewUtils.hideView(binding.buttonNext)
} else {
ViewUtils.showView(binding.buttonNext)
}
}
private val checkForButtonState: () -> Unit = {
val currentIndex = binding.viewPager2.currentItem
val page = pages[currentIndex]
val isPageComplete = page.pageSteps() == PageState.PAGE_STEPS_COMPLETE
if (isPageComplete) {
binding.viewPager2.adapter?.notifyItemChanged(currentIndex)
ViewUtils.showView(binding.buttonNext)
} else {
page.pageButtons?.forEach {
if (it.buttonState() == ButtonState.BUTTON_ACTION_COMPLETE) {
if (this::pageButtonCallback.isInitialized) {
pageButtonCallback.onStepCompleted(it.titleId, pageFullyCompleted = false)
} else {
binding.viewPager2.adapter?.notifyItemChanged(currentIndex)
}
}
}
}
}
@ -559,48 +578,38 @@ class SetupFragment : Fragment() {
showPermissionDeniedSnackbar()
}
private val openCitraDirectory = registerForActivityResult<Uri, Uri>(
ActivityResultContracts.OpenDocumentTree()
) { result: Uri? ->
if (result == null) {
return@registerForActivityResult
}
private fun onOpenCitraDirectory(result: Uri) {
if (!BuildUtil.isGooglePlayBuild) {
if (NativeLibrary.getUserDirectory(result) == "") {
if (NativeLibrary.getNativePath(result) == "") {
SelectUserDirectoryDialogFragment.newInstance(
mainActivity,
R.string.invalid_selection,
R.string.invalid_user_directory
).show(mainActivity.supportFragmentManager, SelectUserDirectoryDialogFragment.TAG)
return@registerForActivityResult
return
}
}
CitraDirectoryHelper(requireActivity(), true).showCitraDirectoryDialog(result, pageButtonCallback, checkForButtonState)
CitraDirectoryHelper(requireActivity(), true).showCitraDirectoryDialog(result,
null, checkForButtonState)
}
private val getGamesDirectory =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result == null) {
return@registerForActivityResult
}
private fun onGetGamesDirectory(result: Uri) {
requireActivity().contentResolver.takePersistableUriPermission(
result,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
requireActivity().contentResolver.takePersistableUriPermission(
result,
Intent.FLAG_GRANT_READ_URI_PERMISSION
)
// When a new directory is picked, we currently will reset the existing games
// database. This effectively means that only one game directory is supported.
preferences.edit()
.putString(GameHelper.KEY_GAME_PATH, result.toString())
.apply()
// When a new directory is picked, we currently will reset the existing games
// database. This effectively means that only one game directory is supported.
preferences.edit()
.putString(GameHelper.KEY_GAME_PATH, result.toString())
.apply()
homeViewModel.setGamesDir(requireActivity(), result.path!!)
homeViewModel.setGamesDir(requireActivity(), result.path!!)
checkForButtonState.invoke()
}
checkForButtonState.invoke()
}
private fun finishSetup() {
preferences.edit()
@ -611,10 +620,12 @@ class SetupFragment : Fragment() {
fun pageForward() {
binding.viewPager2.currentItem = binding.viewPager2.currentItem + 1
homeViewModel.setupCurrentPage = binding.viewPager2.currentItem
}
fun pageBackward() {
binding.viewPager2.currentItem = binding.viewPager2.currentItem - 1
homeViewModel.setupCurrentPage = binding.viewPager2.currentItem
}
fun setPageWarned(page: Int) {

View File

@ -39,6 +39,7 @@ import org.citra.citra_emu.R
import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding
import org.citra.citra_emu.databinding.FragmentSystemFilesBinding
import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.features.settings.SettingKeys
import org.citra.citra_emu.model.Game
import org.citra.citra_emu.utils.SystemSaveGame
import org.citra.citra_emu.viewmodel.GamesViewModel
@ -177,7 +178,7 @@ class SystemFilesFragment : Fragment() {
binding.buttonSetUpSystemFiles.setOnClickListener {
val inflater = LayoutInflater.from(context)
val inputBinding = DialogSoftwareKeyboardBinding.inflate(inflater)
var textInputValue: String = preferences.getString("last_artic_base_addr", "")!!
var textInputValue: String = preferences.getString(SettingKeys.last_artic_base_addr(), "")!!
val progressDialog = showProgressDialog(
getText(R.string.setup_system_files),
@ -274,7 +275,7 @@ class SystemFilesFragment : Fragment() {
.setPositiveButton(android.R.string.ok) { diag, _ ->
if (textInputValue.isNotEmpty() && !(!buttonO3ds.isChecked && !buttonN3ds.isChecked)) {
preferences.edit()
.putString("last_artic_base_addr", textInputValue)
.putString(SettingKeys.last_artic_base_addr(), textInputValue)
.apply()
val menu = Game(
title = getString(R.string.artic_base),

View File

@ -7,19 +7,26 @@ package org.citra.citra_emu.model
import android.os.Parcelable
import android.content.Intent
import android.net.Uri
import androidx.core.net.toUri
import java.io.File
import java.io.IOException
import java.util.HashSet
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.activities.EmulationActivity
import org.citra.citra_emu.utils.BuildUtil
@Parcelize
@Serializable
class Game(
val valid: Boolean = false,
val title: String = "",
val description: String = "",
val path: String = "",
val titleId: Long = 0L,
val mediaType: MediaType = MediaType.GAME_CARD,
val company: String = "",
val regions: String = "",
val isInstalled: Boolean = false,
@ -35,12 +42,25 @@ class Game(
val keyLastPlayedTime get() = "${filename}_LastPlayed"
val launchIntent: Intent
get() = Intent(CitraApplication.appContext, EmulationActivity::class.java).apply {
action = Intent.ACTION_VIEW
data = if (isInstalled) {
CitraApplication.documentsTree.getUri(path)
get() {
var appUri: Uri
if (isInstalled) {
if (BuildUtil.isGooglePlayBuild) {
appUri = CitraApplication.documentsTree.getUri(path)
} else {
val nativePath = NativeLibrary.getUserDirectory() + "/" + path
val nativeFile = File(nativePath)
if (!nativeFile.exists()) {
throw IOException("Attempting to create shortcut for an executable that doesn't exist: $nativePath")
}
appUri = Uri.fromFile(nativeFile)
}
} else {
Uri.parse(path)
appUri = path.toUri()
}
return Intent(CitraApplication.appContext, EmulationActivity::class.java).apply {
action = Intent.ACTION_VIEW
data = appUri
}
}
@ -58,15 +78,28 @@ class Game(
result = 31 * result + regions.hashCode()
result = 31 * result + path.hashCode()
result = 31 * result + titleId.hashCode()
result = 31 * result + mediaType.hashCode()
result = 31 * result + company.hashCode()
return result
}
enum class MediaType(val value: Int) {
NAND(0),
SDMC(1),
GAME_CARD(2);
companion object {
fun fromInt(value: Int): MediaType? {
return MediaType.entries.find { it.value == value }
}
}
}
companion object {
val allExtensions: Set<String> get() = extensions + badExtensions
val extensions: Set<String> = HashSet(
listOf("3dsx", "app", "axf", "cci", "cxi", "elf", "z3dsx", "zcci", "zcxi")
listOf("3dsx", "app", "axf", "cci", "cxi", "elf", "z3dsx", "zcci", "zcxi", "3ds")
)
val badExtensions: Set<String> = HashSet(

View File

@ -39,6 +39,8 @@ import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkManager
import com.google.android.material.color.MaterialColors
import com.google.android.material.navigation.NavigationBarView
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.TimeSource
import kotlinx.coroutines.launch
import org.citra.citra_emu.BuildConfig
import org.citra.citra_emu.NativeLibrary
@ -74,6 +76,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
override var themeId: Int = 0
companion object {
const val KEY_SETUP_CURRENT_PAGE = "SetupCurrentPage"
}
override fun onCreate(savedInstanceState: Bundle?) {
RefreshRateUtil.enforceRefreshRate(this)
@ -130,12 +136,22 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
)
}
var applicationsClickTimestamp = TimeSource.Monotonic.markNow()
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
setUpNavigation(navHostFragment.navController)
setUpNavigation(savedInstanceState, navHostFragment.navController)
(binding.navigationView as NavigationBarView).setOnItemReselectedListener {
when (it.itemId) {
R.id.gamesFragment -> gamesViewModel.setShouldScrollToTop(true)
R.id.gamesFragment -> {
if (applicationsClickTimestamp.elapsedNow() < 300.milliseconds) {
Toast.makeText(this, BuildConfig.VERSION_NAME, Toast.LENGTH_LONG)
.show()
}
applicationsClickTimestamp = TimeSource.Monotonic.markNow()
gamesViewModel.setShouldScrollToTop(true)
}
R.id.searchFragment -> gamesViewModel.setSearchFocused(true)
R.id.homeSettingsFragment -> SettingsActivity.launch(
this,
@ -176,6 +192,14 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
setInsets()
}
override fun onSaveInstanceState(outState: Bundle) {
// Save the user's current game state.
outState.putInt(KEY_SETUP_CURRENT_PAGE, homeViewModel.setupCurrentPage)
// Always call the superclass so it can save the view hierarchy state.
super.onSaveInstanceState(outState)
}
override fun onResume() {
checkUserPermissions()
@ -251,11 +275,12 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
(binding.navigationView as NavigationBarView).setupWithNavController(navController)
}
private fun setUpNavigation(navController: NavController) {
private fun setUpNavigation(savedInstanceState: Bundle?, navController: NavController) {
val firstTimeSetup = PreferenceManager.getDefaultSharedPreferences(applicationContext)
.getBoolean(Settings.PREF_FIRST_APP_LAUNCH, true)
if (firstTimeSetup && !homeViewModel.navigatedToSetup) {
homeViewModel.setupCurrentPage = savedInstanceState?.getInt(KEY_SETUP_CURRENT_PAGE) ?: 0
navController.navigate(R.id.firstTimeSetupFragment)
homeViewModel.navigatedToSetup = true
} else {
@ -367,7 +392,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
if (!BuildUtil.isGooglePlayBuild) {
if (NativeLibrary.getUserDirectory(result) == "") {
if (NativeLibrary.getNativePath(result) == "") {
SelectUserDirectoryDialogFragment.newInstance(
this,
R.string.invalid_selection,
@ -412,4 +437,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
.build()
)
}
val setupOpenCitraDirectory = registerForActivityResult(
ActivityResultContracts.OpenDocumentTree(),
) { result: Uri? ->
homeViewModel.selectedCitraDirectory = result
}
val setupGetGamesDirectory = registerForActivityResult(
ActivityResultContracts.OpenDocumentTree()
) { result: Uri? ->
homeViewModel.selectedGamesDirectory = result
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -12,9 +12,11 @@ import androidx.core.app.NotificationCompat
import androidx.work.ForegroundInfo
import androidx.work.Worker
import androidx.work.WorkerParameters
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.NativeLibrary.InstallStatus
import org.citra.citra_emu.R
import org.citra.citra_emu.utils.FileUtil.getFilename
import androidx.core.net.toUri
class CiaInstallWorker(
val context: Context,
@ -131,7 +133,7 @@ class CiaInstallWorker(
installProgressBuilder.setOngoing(true)
setProgressCallback(100, 0)
selectedFiles.forEachIndexed { i, file ->
val filename = getFilename(Uri.parse(file))
val filename = getFilename(file.toUri())
installProgressBuilder.setContentText(
context.getString(
R.string.cia_install_notification_installing,
@ -140,7 +142,13 @@ class CiaInstallWorker(
selectedFiles.size
)
)
val res = installCIA(file)
var fileFinal: String
if (BuildUtil.isGooglePlayBuild) {
fileFinal = file
} else {
fileFinal = "!" + NativeLibrary.getNativePath(file.toUri())
}
val res = installCIA(fileFinal)
notifyInstallStatus(filename, res)
}
notificationManager.cancel(PROGRESS_NOTIFICATION_ID)

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -28,8 +28,8 @@ object DirectoryInitialization {
@Volatile
private var directoryState: DirectoryInitializationState? = null
var userPath: String? = null
val internalUserPath
get() = CitraApplication.appContext.getExternalFilesDir(null)!!.canonicalPath
val internalUserPath: String
get() = CitraApplication.appContext.filesDir.canonicalPath
private val isCitraDirectoryInitializationRunning = AtomicBoolean(false)
val context: Context get() = CitraApplication.appContext

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -23,7 +23,7 @@ object DiskShaderCacheProgress {
}
@JvmStatic
fun loadProgress(stage: LoadCallbackStage, progress: Int, max: Int) {
fun loadProgress(stage: LoadCallbackStage, progress: Int, max: Int, obj: String) {
val emulationActivity = NativeLibrary.sEmulationActivity.get()
if (emulationActivity == null) {
Log.error("[DiskShaderCacheProgress] EmulationActivity not present")
@ -40,7 +40,7 @@ object DiskShaderCacheProgress {
)
LoadCallbackStage.Build -> emulationViewModel.updateProgress(
emulationActivity.getString(R.string.building_shaders),
emulationActivity.getString(R.string.building_shaders, obj ),
progress,
max
)

View File

@ -10,6 +10,8 @@ import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.model.CheapDocument
import org.citra.citra_emu.utils.BuildUtil
import java.io.IOException
import java.net.URLDecoder
import java.nio.file.Paths
import java.util.StringTokenizer
@ -77,8 +79,9 @@ class DocumentsTree {
@Synchronized
fun getFilename(filepath: String): String {
val node = resolvePath(filepath) ?: return ""
return node.name
val components = filepath.split(DELIMITER).filter { it.isNotEmpty() }
val filename = components.last()
return filename
}
@Synchronized
@ -260,6 +263,17 @@ class DocumentsTree {
@Synchronized
private fun resolvePath(filepath: String): DocumentsNode? {
if (!BuildUtil.isGooglePlayBuild) {
var isLegalPath = false
kotlinDirectoryAccessWhitelist.forEach {
if (filepath.startsWith(it)) {
isLegalPath = true
}
}
if (!isLegalPath) {
throw IOException("Attempted to resolve forbidden path: " + filepath)
}
}
root ?: return null
val tokens = StringTokenizer(filepath, DELIMITER, false)
var iterator = root
@ -351,5 +365,10 @@ class DocumentsTree {
companion object {
const val DELIMITER = "/"
val kotlinDirectoryAccessWhitelist = arrayOf(
"/config/",
"/log/",
"/gpu_drivers/",
)
}
}

View File

@ -219,10 +219,20 @@ object FileUtil {
*/
@JvmStatic
fun getFilename(uri: Uri): String {
val columns = arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
var filename = ""
var c: Cursor? = null
try {
if (uri.scheme == "fd") {
return ""
}
if (uri.scheme == "file") {
BuildUtil.assertNotGooglePlay()
val file = File(uri.path!!);
return file.name
}
val columns = arrayOf(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
c = context.contentResolver.query(
uri,
columns,
@ -537,7 +547,7 @@ object FileUtil {
}
@JvmStatic
fun isNativePath(path: String): Boolean =
fun isNativePath(path: String): Boolean = // FIXME: This function name is bullshit -OS
try {
path[0] == '/'
} catch (e: StringIndexOutOfBoundsException) {

View File

@ -32,7 +32,7 @@ object GameHelper {
addGamesRecursive(games, FileUtil.listFiles(gamesUri), 3)
NativeLibrary.getInstalledGamePaths().forEach {
games.add(getGame(Uri.parse(it), isInstalled = true, addedToLibrary = true))
games.add(getGame(Uri.parse(it.path), isInstalled = true, addedToLibrary = true, it.mediaType))
}
// Cache list of games found on disk
@ -62,27 +62,46 @@ object GameHelper {
addGamesRecursive(games, FileUtil.listFiles(it.uri), depth - 1)
} else {
if (Game.allExtensions.contains(FileUtil.getExtension(it.uri))) {
games.add(getGame(it.uri, isInstalled = false, addedToLibrary = true))
games.add(getGame(it.uri, isInstalled = false, addedToLibrary = true, Game.MediaType.GAME_CARD))
}
}
}
}
fun getGame(uri: Uri, isInstalled: Boolean, addedToLibrary: Boolean): Game {
fun getGame(uri: Uri, isInstalled: Boolean, addedToLibrary: Boolean, mediaType: Game.MediaType): Game {
val filePath = uri.toString()
var gameInfo: GameInfo? = GameInfo(filePath)
var nativePath: String? = null
var gameInfo: GameInfo?
if (BuildUtil.isGooglePlayBuild || FileUtil.isNativePath(filePath) || filePath.startsWith("!")) {
gameInfo = GameInfo(filePath)
} else {
nativePath = if (uri.scheme == "fd") {
uri.toString()
} else {
"!" + NativeLibrary.getNativePath(uri)
};
gameInfo = GameInfo(nativePath)
}
if (gameInfo?.isValid() == false) {
val valid = gameInfo.isValid()
if (!valid) {
gameInfo = null
}
val isEncrypted = gameInfo?.isEncrypted() == true
val newGame = Game(
valid,
(gameInfo?.getTitle() ?: FileUtil.getFilename(uri)).replace("[\\t\\n\\r]+".toRegex(), " "),
filePath.replace("\n", " "),
filePath,
// TODO: This next line can be deduplicated but I don't want to right now -OS
if (BuildUtil.isGooglePlayBuild || FileUtil.isNativePath(filePath) || filePath.startsWith("!")) {
filePath
} else {
nativePath!!
},
gameInfo?.getTitleID() ?: 0,
mediaType,
gameInfo?.getCompany() ?: "",
if (isEncrypted) { CitraApplication.appContext.getString(R.string.unsupported_encrypted) } else { gameInfo?.getRegions() ?: "" },
isInstalled,

View File

@ -4,28 +4,43 @@
package org.citra.citra_emu.utils
import org.citra.citra_emu.utils.BuildUtil
import java.io.File
import android.content.Context
import android.os.storage.StorageManager
object RemovableStorageHelper {
// This really shouldn't be necessary, but the Android API seemingly
// doesn't have a way of doing this?
fun getRemovableStoragePath(idString: String): String? {
BuildUtil.assertNotGooglePlay()
// On certain Android flavours the external storage mount location can
// vary, so add extra cases here if we discover them.
val possibleMountPaths = listOf("/mnt/media_rw/$idString", "/storage/$idString")
private val pathCache = mutableMapOf<String, String?>()
private var scanned = false
for (mountPath in possibleMountPaths) {
val pathFile = File(mountPath);
if (pathFile.exists()) {
// TODO: Cache which mount location is being used for the remainder of the
// session, as it should never change. -OS
return pathFile.absolutePath
}
private fun scanVolumes(context: Context) {
if (scanned) {
return
}
return null
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
for (volume in storageManager.storageVolumes) {
if (!volume.isRemovable) {
continue
}
val uuid = volume.uuid ?: continue
val dir = volume.directory ?: continue
pathCache[uuid.uppercase()] = dir.absolutePath
}
scanned = true
}
fun getRemovableStoragePath(context: Context, idString: String): String? {
BuildUtil.assertNotGooglePlay()
val key = idString.uppercase()
if (!scanned) {
scanVolumes(context)
}
return pathCache[key]
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -9,6 +9,10 @@ import android.view.ViewGroup
object ViewUtils {
fun showView(view: View, length: Long = 300) {
if (view.visibility == View.VISIBLE) {
return
}
view.apply {
alpha = 0f
visibility = View.VISIBLE

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -78,8 +78,9 @@ class GamesViewModel : ViewModel() {
val filteredList = sortedList.filter {
if (it.isSystemTitle) {
it.isVisibleSystemTitle
} else {
true
}
true
}
_games.value = filteredList

View File

@ -1,4 +1,4 @@
// Copyright 2023 Citra Emulator Project
// Copyright Citra Emulator Project / Azahar Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
@ -7,6 +7,8 @@ package org.citra.citra_emu.viewmodel
import android.content.res.Resources
import android.net.Uri
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.preference.PreferenceManager
@ -62,6 +64,20 @@ class HomeViewModel : ViewModel() {
var navigatedToSetup = false
var setupCurrentPage = 0
private val _selectedCitraDirectory = MutableLiveData<Uri?>()
val selectedCitraDirectoryLiveData: LiveData<Uri?> = _selectedCitraDirectory
var selectedCitraDirectory: Uri?
get() = _selectedCitraDirectory.value
set(value) { _selectedCitraDirectory.value = value }
private val _selectedGamesDirectory = MutableLiveData<Uri?>()
val selectedGamesDirectoryLiveData: LiveData<Uri?> = _selectedGamesDirectory
var selectedGamesDirectory: Uri?
get() = _selectedGamesDirectory.value
set(value) { _selectedGamesDirectory.value = value }
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
if (_navigationVisible.value.first == visible) {
return

Some files were not shown because too many files have changed in this diff Show More