Compare commits

...

114 Commits

Author SHA1 Message Date
liushuyu
69e758d738
dedicated_room: properly initialize logging (#7468) 2024-02-27 20:36:28 +05:30
Steveice10
f4768cd26c
video_core: Remove pre-compilation of Vulkan host-shaders. (#7461) 2024-02-26 10:26:44 -08:00
Théo B
e0d2c1308e
log: fix SOC_U::Accept LOG_DEBUG call, and ensure such mistakes get picked up at compile time (#7463)
* fix SOC_U::Accept invalid log function

* make logging get checked at compile time
- ensures log strings match the amount and type (if the format specifies an integer, for example) of the arguments
- if at any later point a runtime-generated string is used as the log format, FmtLogMessage might require an overload taking a fmt::runtime_format_string<> as the format argument type, everything else being equal. wrap the generated string with fmt::runtime() before passing to the LOG_X function

* formatting fix: aligning the arguments
2024-02-25 21:43:29 -08:00
Steveice10
4f9fc88bb3
apt: Improve accuracy of applet slot states on system applet launch. (#7456) 2024-02-23 16:18:16 -08:00
GPUCode
d857743075
Downgrade blend factor crash to warning (#7459)
* pica_to_vk: Downgrade assert to warning

* pica_to_gl: Downgrade unreachable to warning
2024-02-22 15:43:44 -08:00
kylon
b5042a5257
Core: update kernel config memory to latest 11.17 (#7460) 2024-02-22 15:43:33 -08:00
Wunk
e524542a40
vk_texture_runtime: Use boost-static_vector (#7455)
* vk_texture_runtime: Use boost-`static_vector` for image init-barriers

Uses `static_vector` rather than `std::array`+`u32` when passing input
parameters into the initialization barriers.

* vk_texture_runtime: Use boost-`static_vector` for framebuffer attachments

* vk_texture_runtime: Use boost-`static_vector` for surface uploads
2024-02-22 02:35:57 +02:00
Steveice10
3a4ebb1413
file_util: Make sure portable user path is absolute. (#7448) 2024-02-18 15:21:53 -08:00
Steveice10
cbe8987036
ci: Update action versions. (#7449) 2024-02-18 08:23:15 -08:00
Charles Lombardo
da5aa70fc9
android: Port yuzu system info logging (#7431) 2024-02-17 20:10:10 -08:00
Castor215
749a721aa2
externals: disable system cpp-httplib if it is a shared object (#7446)
Co-authored-by: Castor216 <davidjamescastor215@proton.me>
2024-02-17 06:39:38 -08:00
SachinVin
bb003c2bd4
audio_core\hle\source.cpp: Improve accuracy of SourceStatus (#7432) 2024-02-17 02:12:54 +01:00
Tobias
7638f87f74
Port several small multiplayer PRs from yuzu (#7419)
* yuzu: Use displayed port on direct connect

* Color player counts in the multiplayer public lobby list

- Full lobbies have their player count displayed in red.
- Lobbies with one slot left have their player count displayed in orange.
- Empty lobbies have their player count grayed out.

* Add hotkeys for multiplayer actions

Default shortcuts were chosen as to be intuitive (use the first letter
of the action, or the second word's first letter) and work on all
types of keyboards. The hotkeys can be used while playing a game too,
as they are application-wide.

* Persist filters in multiplayer public lobby list

After connecting to a room, the chosen filter text, "Games I Own",
"Hide Empty Rooms" and "Hide Full Rooms" values are persisted
to configuration so they are preserved across restarts.

This makes it easier to rejoin a room if you regularly play the same
game, or after a crash.

* citra_qt/lobby: Fix multiplayer player count color in dark theme

Co-Authored-By: Kevnkkm <56404895+kevnkkm@users.noreply.github.com>

* Address review comments

---------

Co-authored-by: Narr the Reg <juangerman-13@hotmail.com>
Co-authored-by: Hugo Locurcio <hugo.locurcio@hugo.pro>
Co-authored-by: Kevnkkm <56404895+kevnkkm@users.noreply.github.com>
2024-02-16 04:34:10 -08:00
Steveice10
aa6809e2a8
renderer_vulkan: Use no more than target supported version. (#7439) 2024-02-15 19:38:32 -08:00
Steveice10
5e02be75a3
renderer_vulkan: Use getToolPropertiesEXT instead of getToolProperties (#7434)
getToolProperties is not available until Vulkan 1.3; we need to use the EXT version.
2024-02-13 21:43:09 -08:00
Tobias
b9c9beeee5
android: add basic support for google game dashboard (#7430)
This adds support for the Performance and Battery Saver modes in the Game Dashboard mostly found on Google Pixel devices.
This does not yet define the specifics for the performance modes but does provide the initial basic support.

Co-authored-by: Emma <153868115+gaypotatoemma@users.noreply.github.com>
2024-02-10 17:24:10 -08:00
GPUCode
de993dcfbd
service: Stub mcu::HWC (#7428) 2024-02-09 14:09:05 -08:00
oltolm
3c9157b1ec
fix ASAN error in sdl_impl.cpp (#7427) 2024-02-09 14:08:15 -08:00
Ishan09811
0c40c10022
Update Android Deps (#7383) 2024-02-09 07:24:55 -05:00
Daniel López Guimaraes
2766118e33
http: Implement various missing commands (#7415) 2024-02-08 11:01:46 -08:00
Steveice10
06b26691ba
soc: Pass accurate sockaddr length to socket functions. (#7426) 2024-02-08 11:01:38 -08:00
PabloMK7
d41ce64f7b
Add ipv6 socket support (#7418)
* Add IPV6 socket support

* Suggestions
2024-02-07 19:22:44 -08:00
Tobias
1165a708d5
.tx/config: Use language mappings for android "tx pull" (#7422)
The language names we are using in the android resources differ from those on Transifex.

We need to manually specify mappings for them, so Transifex is able to place the files in the correct folders.
2024-02-07 05:41:29 -08:00
Steveice10
19784355f9
build: Improve support for Windows cross-compilation. (#7389)
* build: Improve support for Windows cross-compilation.

* build: Move linuxdeploy download to bundle target execution time.
2024-02-05 10:09:50 -08:00
SachinVin
aa6a29d7e1
AudioCore/HLE/source: Partially implement last_buffer_id (#7397)
* AudioCore/HLE/source: Partially implement last_buffer_id

shared_memory.h: fix typo

* tests\audio_core\hle\source.cpp: Add test cases to verify last_buffer_id
2024-02-05 09:54:13 -08:00
GPUCode
106364e01e
video_core: Use source3 when GPU_PREVIOUS is used in first stage (#7411) 2024-02-05 09:53:54 -08:00
GPUCode
d5a1bd07f3
glsl_shader_gen: Increase z=0 epsillon (#7408) 2024-02-05 09:53:41 -08:00
Steveice10
8afa27718c
dumpkeys: Add seeddb.bin to output files. (#7417) 2024-02-05 09:14:14 -08:00
zhaobot
8e2415f455
Update translations (2024-02-01) (#7409)
Co-authored-by: The Citra Community <noreply-fake@community.citra-emu.org>
2024-02-01 15:29:49 -08:00
Steveice10
c978c074db
build: Update and re-enable cubeb on macOS. (#7405) 2024-02-01 15:29:14 -08:00
Steveice10
cb92ec278e
ci: Move non-x86_64 macOS jobs to M1 systems. (#7406) 2024-02-01 06:39:29 -08:00
Steveice10
9f5d5c6ddd
externals: Remove broken android-ifaddrs. (#7410) 2024-02-01 06:39:13 -08:00
GPUCode
480604ec72
glsl_shader_fs_gen: Apply shadow before ambient light (#7404) 2024-01-31 23:29:39 +02:00
merry
63feac6bb3
externals: Update dynarmic to 6.6.1, Update oaknut to 2.0.1 (#7398) 2024-01-30 19:50:39 -08:00
Steveice10
469f76b075
qt: Display OpenGL renderer name and add Mesa override to support Windows OpenGLOn12. (#7395) 2024-01-29 12:24:41 -08:00
SachinVin
7a4854c519
shader_setup.h: Initialise program_code (#7396) 2024-01-28 06:02:40 -08:00
Steveice10
d1e3dddf6a
core: Fix invalid log formatting in ARM interpreter. (#7391) 2024-01-27 00:39:27 -08:00
Charles Lombardo
265e8193b9
Merge pull request #7392 from amwatson/patch-1
[SettingsFragmentPresenter.kt] correct RESOLUTION_FACTOR key/default
2024-01-26 22:43:04 -05:00
Amanda Watson
e8c20fa782
[SettingsFragmentPresenter.kt] set RESOLUTION_FACTOR preference with RESOLUTION_FACTOR setting instead of GRAPHICS_API
Currently, the RESOLUTION_FACTOR preference is being set with the GRAPHICS_API key and default. Therefore, it will set/retrieve the wrong values

This revision updates the RESOLUTION_FACTOR preference to use the RESOLUTION_FACTOR key and default value. As a result, RESOLUTION_FACTOR and GRAPHICS_API should store and return the correct (separate) values
2024-01-26 19:14:27 -06:00
PabloMK7
95ae46f6a8
SOC_U: Account for variable CTRSockAddr size (#7387)
* SOC_U: Account for variable CTRSockAddr size.

* Apply suggestions
2024-01-26 08:00:19 -08:00
Steveice10
41fe75acb7
renderer_vulkan: Pass physical device API version to VMA instead of instance version. (#7390) 2024-01-26 16:34:12 +02:00
Tobias
1744537d85
Small improvements to Citra translations (#7379)
* dist: Remove duplicated Finnish translation

For some reason, we had Finnish listed twice on Transifex, causing it be shown twice in Citra.
It has already been deleted again from Transifex, now we only need to remove it from the repo as well.

* citra_qt/configure_ui: Show country of language in the combobox

This prevents an issue where we had seperate versions of the same language for different regions and they were not distinguishable (e.g. "Chinese (China)" and "Chinese (Taiwan)").
2024-01-24 15:17:15 -08:00
GPUCode
bea863efff
general: Fixes for Tales of the Abyss (#7381)
* geometry_pipeline: Remove unneeded assert

* Has been hw-tested that gs works correctly even when not in exclusive mode

* pica_core: Propagate output_mask to gs

* Has been hw-tested to occur under the same conditions that other uniforms are shared

* regs_shader: Intialize GPUREG_SH_INPUTBUFFER_CONFIG to default value

* Default value verified on hw. Tales of Abyss does not update the number of vertex attributes for the geometry unit and expects it to be 2

* texture_codec: Align buffer sizes to bpp

* Prevents out of bounds texture reads when launching TOA from the HOME menu

* pica_core: Make default value more clear
2024-01-24 19:22:10 +02:00
Daniel López Guimaraes
89e13a85a7
Implement NEWS service (#7377) 2024-01-24 19:21:48 +02:00
GPUCode
549fdd0736
pica_core: Propogate vertex uniforms to geometry setup when not in exclusive mode (#7367) 2024-01-24 04:47:08 +02:00
GPUCode
eddc4a029c
cam: Ensure camera implementation is not null before using it (#7368)
* cam: Use PopEnum and update result names

* cam: Make sure impl is not null before using it
2024-01-21 23:32:46 -08:00
Steveice10
82294425e3
build: Add flags to toggle specific renderer backends. (#7375) 2024-01-21 23:29:46 -08:00
Charles Lombardo
77fce3cf82
android: Sync translations (#7374)
* android: Sync translations

* android: Enable generateLocaleConfig
2024-01-22 03:46:49 +01:00
GPUCode
8d82adb3d3
glsl_shader_gen: Remove invariant qualifier (#7376)
* glsl_shader_gen: Remove invariant qualifier

* Causes visual regressions in Pokemon with RADV

* rasterizer_cache: Clear null surface to transparent
2024-01-21 13:39:35 +02:00
SachinVin
228f26d1e4
tests: Port merry's audio tests (#7354) 2024-01-21 05:16:00 +01:00
GPUCode
789654d7da
core: Do not update framebuffer layout on android (#7330) 2024-01-20 22:16:43 +02:00
GPUCode
ca3b2306d5
shader_unit: Intialize temporaries on shader invocation (#7366) 2024-01-20 22:13:31 +02:00
GPUCode
8e87bd606c
glsl_shader_gen: Use epsilon for both ends of NDC range (#7355) 2024-01-20 22:13:16 +02:00
Steveice10
f26044bb88
frontend: Add setting for whether to use LLE applets. (#7345) 2024-01-20 22:13:06 +02:00
Daniel López Guimaraes
c59ef7d793
cecd: Fixup GetCecInfoBuffer params order (#7361)
While I was looking at the NEWS sysmodule, I noticed the params order
for this command were backwards: the info type is the first param,
followed by the buffer size.

This is accurate to my reverse engineered code for the NEWS sysmodule.
2024-01-16 22:48:42 -08:00
PabloMK7
6a7841d4b0
fs: Update comment in Get[This]SaveDataSecureValue (#7359)
Upon further research, I found out the unknown value in FS::Get[This]SaveDataSecureValue indicates that the requesting process is a game card. I have updated the comment for future reference.
2024-01-15 11:42:28 -08:00
Steveice10
a2d1c4a94c
kernel: Move serialization code out of headers. (#7312) 2024-01-14 16:18:31 -08:00
SachinVin
9c84721d84
audio_core/hle/source.cpp: clear config.play_position_dirty regardless of config.play_position value. (#7349)
Cosmetic-ish change so we dont incorrectly log about unhandled dirty flags
2024-01-14 12:27:28 -08:00
Steveice10
cca8c08a9a
build: Fix non-PCH build on Linux and add non-PCH verification to CI. (#7351) 2024-01-13 19:58:09 -08:00
PabloMK7
72c1075402
Reorder error handling in extdata FS::CreateFile (#7346)
* Reorder error handling in extdata CreateFile

* Apply suggestions
2024-01-13 12:37:06 -08:00
Steveice10
30c53c9509
build: Disable _FORTIFY_SOURCE on Debug builds. (#7348) 2024-01-12 20:24:23 -08:00
xperia64
da9f382d2c
web_service: avoid undefined behavior assert of std::string::back (#7347)
Co-authored-by: BuildTools <unconfigured@null.spigotmc.org>
2024-01-12 20:24:01 -08:00
PabloMK7
a177769c3b
Add random sleep to game main thread on first boot when using LLE modules (#7199)
* Add random delay to app main thread

* Suggestions

* Remove randomness, only delay with lle

* Apply suggestions

* Fix clang format

* Fix compilation (again)

* Remove unused include
2024-01-12 12:48:00 -08:00
James Forward
f346949989
fix(android): Fix issue where motion controls were being locked incorrectly due to mismatch of initialised swap screen code. (#7344) 2024-01-12 10:28:10 -08:00
Steveice10
37f0a7484f
renderer_vulkan: Revert vkGetInstanceProcAddr symbol change for MoltenVK. (#7341) 2024-01-12 09:16:04 -08:00
PabloMK7
19d5695aa3
Implement some missing/wrong AC functionality. (#7171)
* Implement some missing/wrong AC functionality.

* Schedule NDM connect event into the future

* Disable NDM connect for now as it's causing issues

* Apply latest changes and suggestions.

* Workaround to fake wifi connection.

* Add missing command to ac:i

* Fix compilation

* Fix error codes for CamcelConnectAsync

* Fix missing global state.
2024-01-12 09:15:47 -08:00
Steveice10
6cbdc73f53
boss: Fix debug assert when session is not initialized. (#7337) 2024-01-10 13:00:03 -08:00
Steveice10
81ee7ad893
boss: Add some missing result codes. (#7334) 2024-01-09 19:32:52 -08:00
Steveice10
2ce0a9e899
renderer_vulkan: Update to support MoltenVK 1.2.7 (#7335) 2024-01-09 11:33:47 -08:00
Steveice10
015e42be05
Port yuzu-emu/yuzu#7506 & yuzu-emu/yuzu#7861: "Fix yuzu-emu/yuzu#7502" & "yuzu: Mute audio when in background" (#7321) 2024-01-09 09:56:39 -08:00
Steveice10
57696b2c11
core: Persist plg:ldr state across resets without static state. (#7327) 2024-01-08 09:20:14 -08:00
Vitor K
c8c2beaeff
misc: fix issues pointed out by msvc (#7316)
* do not move constant variables

* applet_manager: avoid possible use after move

* use constant references where pointed out by msvc

* extra_hid: initialize response

* ValidateSaveState: passing slot separately is not necessary

* common: mark HashCombine as nodiscard

* cityhash: remove use of using namespace std

* Prefix all size_t with std::

done automatically by executing regex replace `([^:0-9a-zA-Z_])size_t([^0-9a-zA-Z_])` -> `$1std::size_t$2`
based on 7d8f115

* shared_memory.cpp: fix log error format

* fix compiling with pch off
2024-01-07 12:37:42 -08:00
Steveice10
6069fac76d
video_core: Fix crash when no debug context is provided. (#7324) 2024-01-07 10:29:43 -08:00
Steveice10
7bacb78ce3
boss: Add some missing property IDs and fix file enumeration. (#7322) 2024-01-07 09:38:41 -08:00
Steveice10
0165012ba4
core_timing: Allow configuring a fixed or random initial system tick value. (#7309)
* core_timing: Apply random base ticks value on startup.

* core: Maintain consistent base system ticks in TAS movies.

* frontend: Add setting to configure a fixed base system ticks value.
2024-01-07 09:38:02 -08:00
Steveice10
96aa1b3a08
memory: Fix order of checks in PhysicalToVirtualAddressForRasterizer. (#7328) 2024-01-06 22:49:32 -08:00
Steveice10
b2c740ee0e
plg_ldr: Revert state back to static for now. (#7326) 2024-01-06 15:21:42 -08:00
Steveice10
bc352e8168
applet_manager: Fix checking if HLE applet exists. (#7325) 2024-01-06 15:21:35 -08:00
Steveice10
4f00eb20db
add volume quicksetting with volume slider (#7307)
Co-authored-by: Jonas Gutenschwager <spam.saikai@googlemail.com>
Co-authored-by: Morph <39850852+Morph1984@users.noreply.github.com>
2024-01-06 10:30:22 -08:00
Steveice10
8b6a9b0dd8
dsp: Fix mask sizes in LoadComponent. (#7319) 2024-01-06 08:46:19 -08:00
GPUCode
62409f8139
kernel: Release thread resource limit in Thread::Stop (#7318)
* core: Config plg_ldr after its creation

* Also use service manager to retrieve the service

* thread: Release resource limit in Thread::Stop

* service: Undo plgldr change
2024-01-05 16:12:00 -08:00
Steveice10
0df72f3873
ir: Set ir:rst max sessions to 2. (#7317) 2024-01-05 14:21:30 -08:00
Steveice10
f2ee9baec7
core: Eliminate more uses of Core::System::GetInstance(). (#7313) 2024-01-05 12:07:28 -08:00
Steveice10
8e2037b3ff
audio_core: Clean up AAC decoder infrastructure. (#7310) 2024-01-04 11:00:03 -08:00
Steveice10
c6bcbc02de
frontend: Fix missing persistence for texture sampling setting. (#7305) 2024-01-02 12:05:22 -08:00
Steveice10
36db566428
qt: Add support for opening files directly on macOS. (#7304)
* Associate 3ds files with Citra in Info.plist

* qt: Add support for opening files directly on macOS.

---------

Co-authored-by: shinra-electric <50119606+shinra-electric@users.noreply.github.com>
2024-01-02 12:05:12 -08:00
SachinVin
9b147d3f9c
framebuffer_layout.cpp mini refactor (#7300)
* framebuffer_layout.cpp: simplify FrameLayoutFromResolutionScale

- upright_screen seems to only be swapped width and height calculation, so it is replaced with std::swap
- Get rid of call to GetCardboardSettings, The FrameLayoutFromResolutionScale function is used for Screenshots and Video Dumping where we dont need 3D effects

* framebuffer_layout.cpp: Combine SideFrameLayout and MobileLandscapeFrameLayout into variants of LargeFrameLayout

* framebuffer_layout.{cpp,h}: rename maxRectangle to MaxRectangle, plus

minor documentation update

* clang-format
2024-01-02 00:52:03 -08:00
Steveice10
7dd9174d31
cheats: Use global cheat engine (#7291)
* cheats: Use global cheat engine

* cheats: Prevent wasted double-load of cheat file.

* android: Fix for cheat engine updates.

---------

Co-authored-by: GPUCode <geoster3d@gmail.com>
2024-01-01 12:49:08 -08:00
GPUCode
5a7f615da1
kernel: Update to use atmosphere macros and correct Result (#7242)
* kernel: Switch to atmosphere style macros

* code: Rename ResultCode to Result

* code: Result constants are lower case

* Address review comments

* core: Remove CASCADE_CODE

* R_TRY replaces completely

* core: Run clang format
2023-12-31 09:01:40 -08:00
Steveice10
811303ea54
kernel: Fix freeing shared memory with wrong region. (#7301) 2023-12-30 15:36:12 -08:00
Steveice10
5bcdcffd96
kernel: Add some missing state to process serialization. (#7295) 2023-12-28 08:25:46 -08:00
GPUCode
2bb7f89c30
video_core: Refactor GPU interface (#7272)
* video_core: Refactor GPU interface

* citra_qt: Better debug widget lifetime
2023-12-28 11:46:57 +01:00
Steveice10
602f4f60d8
boss: Implement some NsData header and read commands. (#7283)
* boss: Implement some NsData header and read commands.

Co-authored-by: Rokkubro <lachlanb03@gmail.com>

* boss: Move opening ext data to common function and improve logging.

---------

Co-authored-by: Rokkubro <lachlanb03@gmail.com>
2023-12-26 09:01:32 -08:00
Steveice10
3113ae6616
cfg: Only select preferred region once per instance. (#7284) 2023-12-26 09:01:16 -08:00
Steveice10
bd4ec251cd
gsp_gpu: Implement TryAcquireRight and stub SetInternalPriorities. (#7285)
* gsp_gpu: Implement TryAcquireRight.

* gsp_gpu: Stub SetInternalPriorities.

* gsp_gpu: Move serialization logic into implementation.

* gsp_gpu: Replace UINT32_MAX with std::numeric_limits<u32>::max().
2023-12-25 08:29:17 -08:00
Daniel López Guimaraes
b6b98af105
cecd: Stub GetCecInfoEventHandleSys (#7278)
This allows usage of the LLE news sysmodule.
2023-12-22 19:52:27 -08:00
James Forward
60a280af24
feat(android-hotkeys): Introduce hotkey support for Android app and add missing hybrid layout (#7241)
* feat(android-hotkeys): Introduce hotkey support for Android app

* android: Fix settings not saving for layout options - screen swap + layout.

* android: Fix `from` method to default to "DEFAULT" if passed an invalid method (and also not be based on ordering)

* android: PR response - name to togglePause
2023-12-22 19:52:12 -08:00
Steveice10
178e602589
misc: Improve defaults for macOS and handling of missing audio backends. (#7273)
* misc: Improve backend defaults for macOS.

* audio_core: Improve handling of missing audio backends.
2023-12-22 11:38:06 -08:00
Daniel López Guimaraes
dccb8f6b17
gamemode: Fix compile issues (#7276)
The Linux build fails to compile because gamemode will try to link
against `common` when it's not needed.
2023-12-22 19:29:44 +05:30
Steveice10
f177433d41
cfg: Set sound mode to stereo by default. (#7268) 2023-12-21 02:34:22 -08:00
Charles Lombardo
71b88c4c1f
android: Disable focus color on input overlay (#7271) 2023-12-21 09:15:08 +01:00
Tobias
c7e9f8449e
Port yuzu-emu/yuzu#11946: "Enable (Feral Interactive) Gamemode on Linux" (#7245) 2023-12-20 06:08:07 -08:00
Steveice10
2e369c03b8
ci: Revert back to unzipped Android artifacts. (#7258) 2023-12-19 18:51:47 -08:00
PabloMK7
a47d8a7b4d
Fix incorrect service name in SOC_U::GetService (#7261) 2023-12-19 08:04:28 -08:00
CasualPokePlayer
02ba5c652b
Add circle_pad_old_* to savestates. (#7250)
This is particularly relavant for TASing, not savestating these values will often cause dropped inputs on loading a savestate, due to the previous old circle pad values being used rather than the ones used during the savestate.
For casual usage, this likely doesn't have much effect compared to the previous code, considering a casual user is probably not likely to care if inputs on the first frame of loading a savestate is dropped or not.
2023-12-19 00:43:44 -08:00
Charles Lombardo
762ddfd07b
Android UI Overhaul Part 4/4 (#7235)
* android: Rework cheats

Reworks cheats to use the navigation component, kotlin, and a tweaked layout for a better tuned look.

* android: Convert remaining files to kotlin and add overlay home button

* android: Remove Picasso dependency

* android: Fix home option layout centering

* android: Adjust logo size in-app
2023-12-17 17:32:30 -08:00
PabloMK7
d680b79725
Implement some missing SOC functionality (#7176)
* Implement some missing SOC functionality

* Add LOG_POLL macro for debugging

* Fix compilation

* Temporary fix for Android

* Temporary fix for Android (for real)

* Apply suggestions

* Add stubbed notice to android sockatmark

* Apply suggestions
2023-12-17 08:50:24 -08:00
GPUCode
2b20082581
common: Miscellaneous cleanups (#7239)
* code: Remove some old msvc workarounds

* android: Upgrade to NDK 26

* Allows access to newer libc++

* common/swap: Make use of std::endian

Allows removing a bunch of defines in favor of a two liner.

* common: Remove misc.cpp

* GetLastErrorMsg has been in error.h for a while and also helps removing a depedency from a hot header like common_funcs

* common: use SetThreadDescription API for thread names

* common: Remove linear disk cache

* Has never been used?

* bit_set: Make constexpr

* ring_buffer: Use feature macro

* bit_set: Use <bit> and concepts

* gsp_gpu: Restore comment

* core: Ignore GCC warning

---------

Co-authored-by: Lioncash <mathew1800@gmail.com>
Co-authored-by: Liam <byteslice@airmail.cc>
2023-12-14 16:26:33 +02:00
Tobias
15ea0c6336
Port yuzu-emu/yuzu#12100: "translations: Add android translations to transifex config" (#7246)
Port of https://github.com/yuzu-emu/yuzu/pull/12100.

Co-authored-by: Charles Lombardo <clombardo169@gmail.com>
2023-12-14 04:57:08 +01:00
Steveice10
9a6d15ab74
ci: Only use Linux clang for app image build. (#7244)
* ci: Only use Linux clang for app image build.

* build: Re-add -Wno-attributes for GCC 11.
2023-12-12 09:48:06 -08:00
Steveice10
60584e861d
fs: Stub ControlArchive. (#7237) 2023-12-08 23:35:01 -08:00
Steveice10
070853b465
apt: Stub ReplySleepQuery and ReplySleepNotificationComplete. (#7236) 2023-12-08 23:34:54 -08:00
Steveice10
24b5ffbfca
boss: Implement Spotpass service (part 1) (#7232)
* boss: Implement Spotpass service (part 1)

* boss: Fix save state (de)serialization.

* boss: Fix casing of SpotPass in log messages.

* boss: Minor logging improvements.

* common: Add boost serialization support for std::variant.

---------

Co-authored-by: Rokkubro <lachlanb03@gmail.com>
Co-authored-by: FearlessTobi <thm.frey@gmail.com>
2023-12-08 23:34:44 -08:00
Wunk
4d9eedd0d8
video_core/vulkan: Add debug object names (#7233)
* vk_platform: Add `SetObjectName`

Creates a name-info struct and automatically deduces the object handle type using vulkan-hpp's handle trait data.
Supports `string_view` and `fmt` arguments.

* vk_texture_runtime: Use `SetObjectName` for surface handles

Names both the image handle and the image-view.

* vk_stream_buffer: Add debug object names

Names the buffer and its device memory based on its size and type.

* vk_swapchain: Set swapchain handle debug names

Identifies the swapchain images themselves as well as the semaphores

* vk_present_window: Set handle debug names

* vk_resource_pool: Set debug handle names

* vk_blit_helper: Set debug handle names

* vk_platform: Use `VulkanHandleType` concept

Use a new `concept`-type rather than `enable_if`-patterns to restrict
this function to Vulkan handle-types only.
2023-12-08 06:58:47 +02:00
653 changed files with 35295 additions and 32776 deletions

View File

@ -1,13 +1,19 @@
#!/bin/sh -ex
#!/bin/bash -ex
if [ "$TARGET" = "appimage" ]; then
# Compile the AppImage we distribute with Clang.
export EXTRA_CMAKE_FLAGS=(-DCMAKE_CXX_COMPILER=clang++ -DCMAKE_C_COMPILER=clang -DCMAKE_LINKER=/etc/bin/ld.lld)
else
# For the linux-fresh verification target, verify compilation without PCH as well.
export EXTRA_CMAKE_FLAGS=(-DCITRA_USE_PRECOMPILED_HEADERS=OFF)
fi
mkdir build && cd build
cmake .. -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_CXX_COMPILER=clang++ \
-DCMAKE_C_COMPILER=clang \
-DCMAKE_LINKER=/etc/bin/ld.lld \
"${EXTRA_CMAKE_FLAGS[@]}" \
-DENABLE_QT_TRANSLATION=ON \
-DCITRA_ENABLE_COMPATIBILITY_REPORTING=ON \
-DENABLE_COMPATIBILITY_LIST_DOWNLOAD=ON \

View File

@ -61,12 +61,20 @@ function pack_artifacts() {
fi
}
if [ -z "$PACK_INDIVIDUALLY" ]; then
# Pack all of the artifacts at once.
pack_artifacts build/bundle
else
if [ -n "$UNPACKED" ]; then
# Copy the artifacts to be uploaded unpacked.
for ARTIFACT in build/bundle/*; do
FILENAME=$(basename "$ARTIFACT")
EXTENSION="${FILENAME##*.}"
mv "$ARTIFACT" "artifacts/$REV_NAME.$EXTENSION"
done
elif [ -n "$PACK_INDIVIDUALLY" ]; then
# Pack and upload the artifacts one-by-one.
for ARTIFACT in build/bundle/*; do
pack_artifacts "$ARTIFACT"
done
else
# Pack all of the artifacts into a single archive.
pack_artifacts build/bundle
fi

View File

@ -12,13 +12,13 @@ jobs:
if: ${{ !github.head_ref }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Pack
run: ./.ci/source.sh
- name: Upload
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: source
path: artifacts/
@ -37,11 +37,11 @@ jobs:
OS: linux
TARGET: ${{ matrix.target }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ${{ env.CCACHE_DIR }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
@ -53,13 +53,13 @@ jobs:
run: ./.ci/pack.sh
if: ${{ matrix.target == 'appimage' }}
- name: Upload
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: ${{ matrix.target == 'appimage' }}
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: artifacts/
macos:
runs-on: macos-13
runs-on: ${{ (matrix.target == 'x86_64' && 'macos-13') || 'macos-14' }}
strategy:
matrix:
target: ["x86_64", "arm64"]
@ -70,43 +70,43 @@ jobs:
OS: macos
TARGET: ${{ matrix.target }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ${{ env.CCACHE_DIR }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-${{ matrix.target }}-
- name: Install tools
run: brew install ccache glslang ninja
run: brew install ccache ninja
- name: Build
run: ./.ci/macos.sh
- name: Prepare outputs for caching
run: mv build/bundle $OS-$TARGET
- name: Cache outputs for universal build
uses: actions/cache/save@v3
uses: actions/cache/save@v4
with:
path: ${{ env.OS }}-${{ env.TARGET }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
macos-universal:
runs-on: macos-13
runs-on: macos-14
needs: macos
env:
OS: macos
TARGET: universal
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Download x86_64 build from cache
uses: actions/cache/restore@v3
uses: actions/cache/restore@v4
with:
path: ${{ env.OS }}-x86_64
key: ${{ runner.os }}-x86_64-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
fail-on-cache-miss: true
- name: Download ARM64 build from cache
uses: actions/cache/restore@v3
uses: actions/cache/restore@v4
with:
path: ${{ env.OS }}-arm64
key: ${{ runner.os }}-arm64-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
@ -118,7 +118,7 @@ jobs:
- name: Pack
run: ./.ci/pack.sh
- name: Upload
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: artifacts/
@ -137,11 +137,11 @@ jobs:
OS: windows
TARGET: ${{ matrix.target }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ${{ env.CCACHE_DIR }}
key: ${{ runner.os }}-${{ matrix.target }}-${{ github.sha }}
@ -153,13 +153,6 @@ jobs:
- name: Install extra tools (MSVC)
run: choco install ccache ninja wget
if: ${{ matrix.target == 'msvc' }}
- name: Set up Vulkan SDK (MSVC)
uses: humbletim/setup-vulkan-sdk@v1.2.0
if: ${{ matrix.target == 'msvc' }}
with:
vulkan-query-version: latest
vulkan-components: SPIRV-Tools, Glslang
vulkan-use-cache: true
- name: Set up MSYS2
uses: msys2/setup-msys2@v2
if: ${{ matrix.target == 'msys2' }}
@ -168,10 +161,8 @@ jobs:
update: true
install: git make p7zip
pacboy: >-
toolchain:p ccache:p cmake:p ninja:p glslang:p
toolchain:p ccache:p cmake:p ninja:p
qt6-base:p qt6-multimedia:p qt6-multimedia-wmf:p qt6-tools:p qt6-translations:p
- name: Test glslang
run: glslang --version || glslangValidator --version
- name: Disable line ending translation
run: git config --global core.autocrlf input
- name: Build
@ -179,7 +170,7 @@ jobs:
- name: Pack
run: ./.ci/pack.sh
- name: Upload
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: artifacts/
@ -192,11 +183,11 @@ jobs:
OS: android
TARGET: universal
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
@ -215,7 +206,7 @@ jobs:
run: |
sudo add-apt-repository -y ppa:theofficialgman/gpu-tools
sudo apt-get update -y
sudo apt-get install ccache glslang-dev glslang-tools apksigner -y
sudo apt-get install ccache apksigner -y
- name: Build
run: JAVA_HOME=$JAVA_HOME_17_X64 ./.ci/android.sh
env:
@ -226,15 +217,14 @@ jobs:
run: ../../../.ci/pack.sh
working-directory: src/android/app
env:
PACK_INDIVIDUALLY: 1
SKIP_7Z: 1
UNPACKED: 1
- name: Upload
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: ${{ env.OS }}-${{ env.TARGET }}
path: src/android/app/artifacts/
ios:
runs-on: macos-13
runs-on: macos-14
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
env:
CCACHE_DIR: ${{ github.workspace }}/.ccache
@ -243,18 +233,18 @@ jobs:
OS: ios
TARGET: arm64
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
- name: Set up cache
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: ${{ env.CCACHE_DIR }}
key: ${{ runner.os }}-ios-${{ github.sha }}
restore-keys: |
${{ runner.os }}-ios-
- name: Install tools
run: brew install ccache glslang ninja
run: brew install ccache ninja
- name: Build
run: ./.ci/ios.sh
release:
@ -262,7 +252,7 @@ jobs:
needs: [windows, linux, macos-universal, android, source]
if: ${{ startsWith(github.ref, 'refs/tags/') }}
steps:
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
- name: Create release
uses: actions/create-release@v1
env:

View File

@ -13,7 +13,7 @@ jobs:
image: citraemu/build-environments:linux-fresh
options: -u 1001
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build

View File

@ -20,11 +20,11 @@ jobs:
if: ${{ github.event.inputs.nightly != 'false' && github.repository == 'citra-emu/citra' }}
steps:
# this checkout is required to make sure the GitHub Actions scripts are available
- uses: actions/checkout@v3
- uses: actions/checkout@v4
name: Pre-checkout
with:
submodules: false
- uses: actions/github-script@v6
- uses: actions/github-script@v7
id: check-changes
name: 'Check for new changes'
env:
@ -38,7 +38,7 @@ jobs:
return checkBaseChanges(github, context);
- run: npm install execa@5
if: ${{ steps.check-changes.outputs.result == 'true' }}
- uses: actions/checkout@v3
- uses: actions/checkout@v4
name: Checkout
if: ${{ steps.check-changes.outputs.result == 'true' }}
with:
@ -46,7 +46,7 @@ jobs:
fetch-depth: 0
submodules: true
token: ${{ secrets.ALT_GITHUB_TOKEN }}
- uses: actions/github-script@v6
- uses: actions/github-script@v7
name: 'Update and tag new commits'
if: ${{ steps.check-changes.outputs.result == 'true' }}
env:
@ -62,11 +62,11 @@ jobs:
if: ${{ github.event.inputs.canary != 'false' && github.repository == 'citra-emu/citra' }}
steps:
# this checkout is required to make sure the GitHub Actions scripts are available
- uses: actions/checkout@v3
- uses: actions/checkout@v4
name: Pre-checkout
with:
submodules: false
- uses: actions/github-script@v6
- uses: actions/github-script@v7
id: check-changes
name: 'Check for new changes'
env:
@ -79,7 +79,7 @@ jobs:
return checkCanaryChanges(github, context);
- run: npm install execa@5
if: ${{ steps.check-changes.outputs.result == 'true' }}
- uses: actions/checkout@v3
- uses: actions/checkout@v4
name: Checkout
if: ${{ steps.check-changes.outputs.result == 'true' }}
with:
@ -87,7 +87,7 @@ jobs:
fetch-depth: 0
submodules: true
token: ${{ secrets.ALT_GITHUB_TOKEN }}
- uses: actions/github-script@v6
- uses: actions/github-script@v7
name: 'Check and merge canary changes'
if: ${{ steps.check-changes.outputs.result == 'true' }}
env:

View File

@ -10,7 +10,7 @@ jobs:
container: citraemu/build-environments:linux-fresh
if: ${{ github.repository == 'citra-emu/citra' }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
with:
submodules: recursive
fetch-depth: 0

View File

@ -79,9 +79,11 @@ option(ENABLE_OPENAL "Enables the OpenAL audio backend" ON)
CMAKE_DEPENDENT_OPTION(ENABLE_LIBUSB "Enable libusb for GameCube Adapter support" ON "NOT IOS" OFF)
option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
CMAKE_DEPENDENT_OPTION(ENABLE_SOFTWARE_RENDERER "Enables the software renderer" ON "NOT ANDROID" OFF)
CMAKE_DEPENDENT_OPTION(ENABLE_OPENGL "Enables the OpenGL renderer" ON "NOT APPLE" OFF)
option(ENABLE_VULKAN "Enables the Vulkan renderer" ON)
CMAKE_DEPENDENT_OPTION(CITRA_ENABLE_BUNDLE_TARGET "Enable the distribution bundling target." ON "NOT ANDROID AND NOT IOS" OFF)
option(USE_DISCORD_PRESENCE "Enables Discord Rich Presence" OFF)
# Compile options
CMAKE_DEPENDENT_OPTION(COMPILE_WITH_DWARF "Add DWARF debugging information" ${IS_DEBUG_BUILD} "MINGW" OFF)
@ -245,6 +247,26 @@ if (ENABLE_QT)
if (ENABLE_QT_TRANSLATION)
find_package(Qt6 REQUIRED COMPONENTS LinguistTools)
endif()
if (NOT DEFINED QT_TARGET_PATH)
# Determine the location of the compile target's Qt.
get_target_property(qtcore_path Qt6::Core LOCATION_Release)
string(FIND "${qtcore_path}" "/bin/" qtcore_path_bin_pos REVERSE)
string(FIND "${qtcore_path}" "/lib/" qtcore_path_lib_pos REVERSE)
if (qtcore_path_bin_pos GREATER qtcore_path_lib_pos)
string(SUBSTRING "${qtcore_path}" 0 ${qtcore_path_bin_pos} QT_TARGET_PATH)
else()
string(SUBSTRING "${qtcore_path}" 0 ${qtcore_path_lib_pos} QT_TARGET_PATH)
endif()
endif()
if (NOT DEFINED QT_HOST_PATH)
# Use the same for host Qt if none is defined.
set(QT_HOST_PATH "${QT_TARGET_PATH}")
endif()
message(STATUS "Using target Qt at ${QT_TARGET_PATH}")
message(STATUS "Using host Qt at ${QT_HOST_PATH}")
endif()
# Use system tsl::robin_map if available (otherwise we fallback to version bundled with dynarmic)
@ -254,20 +276,22 @@ find_package(tsl-robin-map QUIET)
# ======================================
if (APPLE)
if (NOT USE_SYSTEM_MOLTENVK)
download_moltenvk()
endif()
find_library(MOLTENVK_LIBRARY MoltenVK REQUIRED)
message(STATUS "Using MoltenVK at ${MOLTENVK_LIBRARY}.")
if (NOT IOS)
# Umbrella framework for everything GUI-related
find_library(COCOA_LIBRARY Cocoa REQUIRED)
endif()
find_library(AVFOUNDATION_LIBRARY AVFoundation REQUIRED)
find_library(IOSURFACE_LIBRARY IOSurface REQUIRED)
set(PLATFORM_LIBRARIES ${COCOA_LIBRARY} ${AVFOUNDATION_LIBRARY} ${IOSURFACE_LIBRARY} ${MOLTENVK_LIBRARY})
if (ENABLE_VULKAN)
if (NOT USE_SYSTEM_MOLTENVK)
download_moltenvk()
endif()
find_library(MOLTENVK_LIBRARY MoltenVK REQUIRED)
message(STATUS "Using MoltenVK at ${MOLTENVK_LIBRARY}.")
set(PLATFORM_LIBRARIES ${PLATFORM_LIBRARIES} ${MOLTENVK_LIBRARY})
endif()
elseif (WIN32)
set(PLATFORM_LIBRARIES winmm ws2_32)
if (MINGW)
@ -418,7 +442,8 @@ else()
endif()
# Create target for outputting distributable bundles.
if (CITRA_ENABLE_BUNDLE_TARGET)
# Not supported for mobile platforms as distributables are built differently.
if (NOT ANDROID AND NOT IOS)
include(BundleTarget)
if (ENABLE_SDL2_FRONTEND)
bundle_target(citra)

View File

@ -2,37 +2,104 @@
if (BUNDLE_TARGET_EXECUTE)
# --- Bundling method logic ---
function(symlink_safe_copy from to)
if (WIN32)
# Use cmake copy for maximum compatibility.
execute_process(COMMAND ${CMAKE_COMMAND} -E copy "${from}" "${to}"
RESULT_VARIABLE cp_result)
else()
# Use native copy to turn symlinks into normal files.
execute_process(COMMAND cp -L "${from}" "${to}"
RESULT_VARIABLE cp_result)
endif()
if (NOT cp_result EQUAL "0")
message(FATAL_ERROR "cp \"${from}\" \"${to}\" failed: ${cp_result}")
endif()
endfunction()
function(bundle_qt executable_path)
if (WIN32)
# Perform standalone bundling first to copy over all used libraries, as windeployqt does not do this.
bundle_standalone("${executable_path}" "${EXECUTABLE_PATH}" "${BUNDLE_LIBRARY_PATHS}")
get_filename_component(executable_parent_dir "${executable_path}" DIRECTORY)
find_program(windeployqt_executable windeployqt6)
# Create a qt.conf file pointing to the app directory.
# This ensures Qt can find its plugins.
file(WRITE "${executable_parent_dir}/qt.conf" "[Paths]\nprefix = .")
file(WRITE "${executable_parent_dir}/qt.conf" "[Paths]\nPrefix = .")
find_program(windeployqt_executable windeployqt6 PATHS "${QT_HOST_PATH}/bin")
find_program(qtpaths_executable qtpaths6 PATHS "${QT_HOST_PATH}/bin")
# TODO: Hack around windeployqt's poor cross-compilation support by
# TODO: making a local copy with a prefix pointing to the target Qt.
if (NOT "${QT_HOST_PATH}" STREQUAL "${QT_TARGET_PATH}")
set(windeployqt_dir "${BINARY_PATH}/windeployqt_copy")
file(MAKE_DIRECTORY "${windeployqt_dir}")
symlink_safe_copy("${windeployqt_executable}" "${windeployqt_dir}/windeployqt.exe")
symlink_safe_copy("${qtpaths_executable}" "${windeployqt_dir}/qtpaths.exe")
symlink_safe_copy("${QT_HOST_PATH}/bin/Qt6Core.dll" "${windeployqt_dir}")
if (EXISTS "${QT_TARGET_PATH}/share")
# Unix-style Qt; we need to wire up the paths manually.
file(WRITE "${windeployqt_dir}/qt.conf" "\
[Paths]\n
Prefix = ${QT_TARGET_PATH}\n \
ArchData = ${QT_TARGET_PATH}/share/qt6\n \
Binaries = ${QT_TARGET_PATH}/bin\n \
Data = ${QT_TARGET_PATH}/share/qt6\n \
Documentation = ${QT_TARGET_PATH}/share/qt6/doc\n \
Headers = ${QT_TARGET_PATH}/include/qt6\n \
Libraries = ${QT_TARGET_PATH}/lib\n \
LibraryExecutables = ${QT_TARGET_PATH}/share/qt6/bin\n \
Plugins = ${QT_TARGET_PATH}/share/qt6/plugins\n \
QmlImports = ${QT_TARGET_PATH}/share/qt6/qml\n \
Translations = ${QT_TARGET_PATH}/share/qt6/translations\n \
")
else()
# Windows-style Qt; the defaults should suffice.
file(WRITE "${windeployqt_dir}/qt.conf" "[Paths]\nPrefix = ${QT_TARGET_PATH}")
endif()
set(windeployqt_executable "${windeployqt_dir}/windeployqt.exe")
set(qtpaths_executable "${windeployqt_dir}/qtpaths.exe")
endif()
message(STATUS "Executing windeployqt for executable ${executable_path}")
execute_process(COMMAND "${windeployqt_executable}" "${executable_path}"
--qtpaths "${qtpaths_executable}"
--no-compiler-runtime --no-system-d3d-compiler --no-opengl-sw --no-translations
--plugindir "${executable_parent_dir}/plugins")
--plugindir "${executable_parent_dir}/plugins"
RESULT_VARIABLE windeployqt_result)
if (NOT windeployqt_result EQUAL "0")
message(FATAL_ERROR "windeployqt failed: ${windeployqt_result}")
endif()
# Remove the FFmpeg multimedia plugin as we don't include FFmpeg.
# We want to use the Windows media plugin instead, which is also included.
file(REMOVE "${executable_parent_dir}/plugins/multimedia/ffmpegmediaplugin.dll")
elseif (APPLE)
get_filename_component(executable_name "${executable_path}" NAME_WE)
find_program(MACDEPLOYQT_EXECUTABLE macdeployqt6)
find_program(macdeployqt_executable macdeployqt6 PATHS "${QT_HOST_PATH}/bin")
message(STATUS "Executing macdeployqt for executable ${executable_path}")
message(STATUS "Executing macdeployqt at \"${macdeployqt_executable}\" for executable \"${executable_path}\"")
execute_process(
COMMAND "${MACDEPLOYQT_EXECUTABLE}"
COMMAND "${macdeployqt_executable}"
"${executable_path}"
"-executable=${executable_path}/Contents/MacOS/${executable_name}"
-always-overwrite)
-always-overwrite
RESULT_VARIABLE macdeployqt_result)
if (NOT macdeployqt_result EQUAL "0")
message(FATAL_ERROR "macdeployqt failed: ${macdeployqt_result}")
endif()
# Bundling libraries can rewrite path information and break code signatures of system libraries.
# Perform an ad-hoc re-signing on the whole app bundle to fix this.
execute_process(COMMAND codesign --deep -fs - "${executable_path}")
execute_process(COMMAND codesign --deep -fs - "${executable_path}"
RESULT_VARIABLE codesign_result)
if (NOT codesign_result EQUAL "0")
message(FATAL_ERROR "codesign failed: ${codesign_result}")
endif()
else()
message(FATAL_ERROR "Unsupported OS for Qt bundling.")
endif()
@ -44,9 +111,9 @@ if (BUNDLE_TARGET_EXECUTE)
if (enable_qt)
# Find qmake to make sure the plugin uses the right version of Qt.
find_program(QMAKE_EXECUTABLE qmake6)
find_program(qmake_executable qmake6 PATHS "${QT_HOST_PATH}/bin")
set(extra_linuxdeploy_env "QMAKE=${QMAKE_EXECUTABLE}")
set(extra_linuxdeploy_env "QMAKE=${qmake_executable}")
set(extra_linuxdeploy_args --plugin qt)
endif()
@ -59,7 +126,11 @@ if (BUNDLE_TARGET_EXECUTE)
--executable "${executable_path}"
--icon-file "${source_path}/dist/citra.svg"
--desktop-file "${source_path}/dist/${executable_name}.desktop"
--appdir "${appdir_path}")
--appdir "${appdir_path}"
RESULT_VARIABLE linuxdeploy_appdir_result)
if (NOT linuxdeploy_appdir_result EQUAL "0")
message(FATAL_ERROR "linuxdeploy failed to create AppDir: ${linuxdeploy_appdir_result}")
endif()
if (enable_qt)
set(qt_hook_file "${appdir_path}/apprun-hooks/linuxdeploy-plugin-qt-hook.sh")
@ -82,7 +153,11 @@ if (BUNDLE_TARGET_EXECUTE)
"OUTPUT=${bundle_dir}/${executable_name}.AppImage"
"${linuxdeploy_executable}"
--output appimage
--appdir "${appdir_path}")
--appdir "${appdir_path}"
RESULT_VARIABLE linuxdeploy_appimage_result)
if (NOT linuxdeploy_appimage_result EQUAL "0")
message(FATAL_ERROR "linuxdeploy failed to create AppImage: ${linuxdeploy_appimage_result}")
endif()
endfunction()
function(bundle_standalone executable_path original_executable_path bundle_library_paths)
@ -109,16 +184,23 @@ if (BUNDLE_TARGET_EXECUTE)
file(MAKE_DIRECTORY ${lib_dir})
foreach (lib_file IN LISTS resolved_deps)
message(STATUS "Bundling library ${lib_file}")
# Use native copy to turn symlinks into normal files.
execute_process(COMMAND cp -L "${lib_file}" "${lib_dir}")
symlink_safe_copy("${lib_file}" "${lib_dir}")
endforeach()
endif()
# Add libs directory to executable rpath where applicable.
if (APPLE)
execute_process(COMMAND install_name_tool -add_rpath "@loader_path/libs" "${executable_path}")
execute_process(COMMAND install_name_tool -add_rpath "@loader_path/libs" "${executable_path}"
RESULT_VARIABLE install_name_tool_result)
if (NOT install_name_tool_result EQUAL "0")
message(FATAL_ERROR "install_name_tool failed: ${install_name_tool_result}")
endif()
elseif (UNIX)
execute_process(COMMAND patchelf --set-rpath '$ORIGIN/../libs' "${executable_path}")
execute_process(COMMAND patchelf --set-rpath '$ORIGIN/../libs' "${executable_path}"
RESULT_VARIABLE patchelf_result)
if (NOT patchelf_result EQUAL "0")
message(FATAL_ERROR "patchelf failed: ${patchelf_result}")
endif()
endif()
endfunction()
@ -127,7 +209,7 @@ if (BUNDLE_TARGET_EXECUTE)
set(bundle_dir ${BINARY_PATH}/bundle)
# On Linux, always bundle an AppImage.
if (DEFINED LINUXDEPLOY)
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
if (IN_PLACE)
message(FATAL_ERROR "Cannot bundle for Linux in-place.")
endif()
@ -146,14 +228,12 @@ if (BUNDLE_TARGET_EXECUTE)
if (BUNDLE_QT)
bundle_qt("${bundled_executable_path}")
endif()
if (WIN32 OR NOT BUNDLE_QT)
else()
bundle_standalone("${bundled_executable_path}" "${EXECUTABLE_PATH}" "${BUNDLE_LIBRARY_PATHS}")
endif()
endif()
else()
# --- Bundling target creation logic ---
elseif (BUNDLE_TARGET_DOWNLOAD_LINUXDEPLOY)
# --- linuxdeploy download logic ---
# Downloads and extracts a linuxdeploy component.
function(download_linuxdeploy_component base_dir name executable_name)
@ -161,7 +241,7 @@ else()
if (NOT EXISTS "${executable_file}")
message(STATUS "Downloading ${executable_name}")
file(DOWNLOAD
"https://github.com/linuxdeploy/${name}/releases/download/continuous/${executable_name}"
"https://github.com/${name}/releases/download/continuous/${executable_name}"
"${executable_file}" SHOW_PROGRESS)
file(CHMOD "${executable_file}" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE)
@ -170,7 +250,11 @@ else()
message(STATUS "Extracting ${executable_name}")
execute_process(
COMMAND "${executable_file}" --appimage-extract
WORKING_DIRECTORY "${base_dir}")
WORKING_DIRECTORY "${base_dir}"
RESULT_VARIABLE extract_result)
if (NOT extract_result EQUAL "0")
message(FATAL_ERROR "AppImage extract failed: ${extract_result}")
endif()
else()
message(STATUS "Copying ${executable_name}")
file(COPY "${executable_file}" DESTINATION "${base_dir}/squashfs-root/usr/bin/")
@ -178,89 +262,102 @@ else()
endif()
endfunction()
# Download plugins first so they don't overwrite linuxdeploy's AppRun file.
download_linuxdeploy_component("${LINUXDEPLOY_PATH}" "linuxdeploy/linuxdeploy-plugin-qt" "linuxdeploy-plugin-qt-${LINUXDEPLOY_ARCH}.AppImage")
download_linuxdeploy_component("${LINUXDEPLOY_PATH}" "darealshinji/linuxdeploy-plugin-checkrt" "linuxdeploy-plugin-checkrt.sh")
download_linuxdeploy_component("${LINUXDEPLOY_PATH}" "linuxdeploy/linuxdeploy" "linuxdeploy-${LINUXDEPLOY_ARCH}.AppImage")
else()
# --- Bundling target creation logic ---
# Creates the base bundle target with common files and pre-bundle steps.
function(create_base_bundle_target)
message(STATUS "Creating base bundle target")
add_custom_target(bundle)
add_custom_command(
TARGET bundle
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bundle/")
add_custom_command(
TARGET bundle
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bundle/dist/")
add_custom_command(
TARGET bundle
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dist/icon.png" "${CMAKE_BINARY_DIR}/bundle/dist/citra.png")
add_custom_command(
TARGET bundle
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/license.txt" "${CMAKE_BINARY_DIR}/bundle/")
add_custom_command(
TARGET bundle
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/README.md" "${CMAKE_BINARY_DIR}/bundle/")
add_custom_command(
TARGET bundle
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/dist/scripting" "${CMAKE_BINARY_DIR}/bundle/scripting")
# On Linux, add a command to prepare linuxdeploy and any required plugins before any bundling occurs.
if (CMAKE_HOST_SYSTEM_NAME STREQUAL "Linux")
add_custom_command(
TARGET bundle
COMMAND ${CMAKE_COMMAND}
"-DBUNDLE_TARGET_DOWNLOAD_LINUXDEPLOY=1"
"-DLINUXDEPLOY_PATH=${CMAKE_BINARY_DIR}/externals/linuxdeploy"
"-DLINUXDEPLOY_ARCH=${CMAKE_HOST_SYSTEM_PROCESSOR}"
-P "${CMAKE_SOURCE_DIR}/CMakeModules/BundleTarget.cmake"
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}")
endif()
endfunction()
# Adds a target to the bundle target, packing in required libraries.
# If in_place is true, the bundling will be done in-place as part of the specified target.
function(bundle_target_internal target_name in_place)
# Create base bundle target if it does not exist.
if (NOT in_place AND NOT TARGET bundle)
message(STATUS "Creating base bundle target")
add_custom_target(bundle)
add_custom_command(
TARGET bundle
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bundle/")
add_custom_command(
TARGET bundle
COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/bundle/dist/")
add_custom_command(
TARGET bundle
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/dist/icon.png" "${CMAKE_BINARY_DIR}/bundle/dist/citra.png")
add_custom_command(
TARGET bundle
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/license.txt" "${CMAKE_BINARY_DIR}/bundle/")
add_custom_command(
TARGET bundle
COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/README.md" "${CMAKE_BINARY_DIR}/bundle/")
add_custom_command(
TARGET bundle
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/dist/scripting" "${CMAKE_BINARY_DIR}/bundle/scripting")
create_base_bundle_target()
endif()
set(BUNDLE_EXECUTABLE_PATH "$<TARGET_FILE:${target_name}>")
set(bundle_executable_path "$<TARGET_FILE:${target_name}>")
if (target_name MATCHES ".*qt")
set(BUNDLE_QT ON)
set(bundle_qt ON)
if (APPLE)
# For Qt targets on Apple, expect an app bundle.
set(BUNDLE_EXECUTABLE_PATH "$<TARGET_BUNDLE_DIR:${target_name}>")
set(bundle_executable_path "$<TARGET_BUNDLE_DIR:${target_name}>")
endif()
else()
set(BUNDLE_QT OFF)
set(bundle_qt OFF)
endif()
# Build a list of library search paths from prefix paths.
foreach(prefix_path IN LISTS CMAKE_PREFIX_PATH CMAKE_SYSTEM_PREFIX_PATH)
foreach(prefix_path IN LISTS CMAKE_FIND_ROOT_PATH CMAKE_PREFIX_PATH CMAKE_SYSTEM_PREFIX_PATH)
if (WIN32)
list(APPEND BUNDLE_LIBRARY_PATHS "${prefix_path}/bin")
list(APPEND bundle_library_paths "${prefix_path}/bin")
endif()
list(APPEND BUNDLE_LIBRARY_PATHS "${prefix_path}/lib")
list(APPEND bundle_library_paths "${prefix_path}/lib")
endforeach()
foreach(library_path IN LISTS CMAKE_SYSTEM_LIBRARY_PATH)
list(APPEND BUNDLE_LIBRARY_PATHS "${library_path}")
list(APPEND bundle_library_paths "${library_path}")
endforeach()
# On Linux, prepare linuxdeploy and any required plugins.
if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(LINUXDEPLOY_BASE "${CMAKE_BINARY_DIR}/externals/linuxdeploy")
# Download plugins first so they don't overwrite linuxdeploy's AppRun file.
download_linuxdeploy_component("${LINUXDEPLOY_BASE}" "linuxdeploy-plugin-qt" "linuxdeploy-plugin-qt-x86_64.AppImage")
download_linuxdeploy_component("${LINUXDEPLOY_BASE}" "linuxdeploy-plugin-checkrt" "linuxdeploy-plugin-checkrt-x86_64.sh")
download_linuxdeploy_component("${LINUXDEPLOY_BASE}" "linuxdeploy" "linuxdeploy-x86_64.AppImage")
set(EXTRA_BUNDLE_ARGS "-DLINUXDEPLOY=${LINUXDEPLOY_BASE}/squashfs-root/AppRun")
endif()
if (in_place)
message(STATUS "Adding in-place bundling to ${target_name}")
set(DEST_TARGET ${target_name})
set(dest_target ${target_name})
else()
message(STATUS "Adding ${target_name} to bundle target")
set(DEST_TARGET bundle)
set(dest_target bundle)
add_dependencies(bundle ${target_name})
endif()
add_custom_command(TARGET ${DEST_TARGET} POST_BUILD
add_custom_command(TARGET ${dest_target} POST_BUILD
COMMAND ${CMAKE_COMMAND}
"-DCMAKE_PREFIX_PATH=\"${CMAKE_PREFIX_PATH}\""
"-DQT_HOST_PATH=\"${QT_HOST_PATH}\""
"-DQT_TARGET_PATH=\"${QT_TARGET_PATH}\""
"-DBUNDLE_TARGET_EXECUTE=1"
"-DTARGET=${target_name}"
"-DSOURCE_PATH=${CMAKE_SOURCE_DIR}"
"-DBINARY_PATH=${CMAKE_BINARY_DIR}"
"-DEXECUTABLE_PATH=${BUNDLE_EXECUTABLE_PATH}"
"-DBUNDLE_LIBRARY_PATHS=\"${BUNDLE_LIBRARY_PATHS}\""
"-DBUNDLE_QT=${BUNDLE_QT}"
"-DEXECUTABLE_PATH=${bundle_executable_path}"
"-DBUNDLE_LIBRARY_PATHS=\"${bundle_library_paths}\""
"-DBUNDLE_QT=${bundle_qt}"
"-DIN_PLACE=${in_place}"
${EXTRA_BUNDLE_ARGS}
"-DLINUXDEPLOY=${CMAKE_BINARY_DIR}/externals/linuxdeploy/squashfs-root/AppRun"
-P "${CMAKE_SOURCE_DIR}/CMakeModules/BundleTarget.cmake"
WORKING_DIRECTORY "${CMAKE_BINARY_DIR}")
endfunction()

View File

@ -1,21 +1,20 @@
set(CURRENT_MODULE_DIR ${CMAKE_CURRENT_LIST_DIR})
# This function downloads Qt using aqt. The path of the downloaded content will be added to the CMAKE_PREFIX_PATH.
# Params:
# target: Qt dependency to install. Specify a version number to download Qt, or "tools_(name)" for a specific build tool.
function(download_qt target)
# Determines parameters based on the host and target for downloading the right Qt binaries.
function(determine_qt_parameters target host_out type_out arch_out arch_path_out host_type_out host_arch_out host_arch_path_out)
if (target MATCHES "tools_.*")
set(DOWNLOAD_QT_TOOL ON)
set(tool ON)
else()
set(DOWNLOAD_QT_TOOL OFF)
set(tool OFF)
endif()
# Determine installation parameters for OS, architecture, and compiler
if (WIN32)
set(host "windows")
set(type "desktop")
if (NOT DOWNLOAD_QT_TOOL)
if (NOT tool)
if (MINGW)
set(arch "win64_mingw")
set(arch_path "mingw_64")
@ -28,21 +27,35 @@ function(download_qt target)
message(FATAL_ERROR "Unsupported bundled Qt architecture. Enable USE_SYSTEM_QT and provide your own.")
endif()
set(arch "win64_${arch_path}")
# In case we're cross-compiling, prepare to also fetch the correct host Qt tools.
if (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "AMD64")
set(host_arch_path "msvc2019_64")
elseif (CMAKE_HOST_SYSTEM_PROCESSOR STREQUAL "ARM64")
# TODO: msvc2019_arm64 doesn't include some of the required tools for some reason,
# TODO: so until it does, just use msvc2019_64 under x86_64 emulation.
# set(host_arch_path "msvc2019_arm64")
set(host_arch_path "msvc2019_64")
endif()
set(host_arch "win64_${host_arch_path}")
else()
message(FATAL_ERROR "Unsupported bundled Qt toolchain. Enable USE_SYSTEM_QT and provide your own.")
endif()
endif()
elseif (APPLE)
set(host "mac")
if (IOS AND NOT DOWNLOAD_QT_TOOL)
set(type "desktop")
set(arch "clang_64")
set(arch_path "macos")
if (IOS AND NOT tool)
set(host_type "${type}")
set(host_arch "${arch}")
set(host_arch_path "${arch_path}")
set(type "ios")
set(arch "ios")
set(arch_path "ios")
set(host_arch_path "macos")
else()
set(type "desktop")
set(arch "clang_64")
set(arch_path "macos")
endif()
else()
set(host "linux")
@ -51,38 +64,64 @@ function(download_qt target)
set(arch_path "linux")
endif()
get_external_prefix(qt base_path)
file(MAKE_DIRECTORY "${base_path}")
set(${host_out} "${host}" PARENT_SCOPE)
set(${type_out} "${type}" PARENT_SCOPE)
set(${arch_out} "${arch}" PARENT_SCOPE)
set(${arch_path_out} "${arch_path}" PARENT_SCOPE)
if (DEFINED host_type)
set(${host_type_out} "${host_type}" PARENT_SCOPE)
else()
set(${host_type_out} "${type}" PARENT_SCOPE)
endif()
if (DEFINED host_arch)
set(${host_arch_out} "${host_arch}" PARENT_SCOPE)
else()
set(${host_arch_out} "${arch}" PARENT_SCOPE)
endif()
if (DEFINED host_arch_path)
set(${host_arch_path_out} "${host_arch_path}" PARENT_SCOPE)
else()
set(${host_arch_path_out} "${arch_path}" PARENT_SCOPE)
endif()
endfunction()
# Download Qt binaries for a specifc configuration.
function(download_qt_configuration prefix_out target host type arch arch_path base_path)
if (target MATCHES "tools_.*")
set(tool ON)
else()
set(tool OFF)
endif()
set(install_args -c "${CURRENT_MODULE_DIR}/aqt_config.ini")
if (DOWNLOAD_QT_TOOL)
if (tool)
set(prefix "${base_path}/Tools")
set(install_args ${install_args} install-tool --outputdir ${base_path} ${host} desktop ${target})
else()
set(prefix "${base_path}/${target}/${arch_path}")
if (host_arch_path)
set(host_flag "--autodesktop")
set(host_prefix "${base_path}/${target}/${host_arch_path}")
endif()
set(install_args ${install_args} install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch} ${host_flag}
-m qtmultimedia --archives qttranslations qttools qtsvg qtbase)
set(install_args ${install_args} install-qt --outputdir ${base_path} ${host} ${type} ${target} ${arch}
-m qtmultimedia --archives qttranslations qttools qtsvg qtbase)
endif()
if (NOT EXISTS "${prefix}")
message(STATUS "Downloading binaries for Qt...")
message(STATUS "Downloading Qt binaries for ${target}:${host}:${type}:${arch}:${arch_path}")
set(AQT_PREBUILD_BASE_URL "https://github.com/miurahr/aqtinstall/releases/download/v3.1.9")
if (WIN32)
set(aqt_path "${base_path}/aqt.exe")
file(DOWNLOAD
${AQT_PREBUILD_BASE_URL}/aqt.exe
${aqt_path} SHOW_PROGRESS)
if (NOT EXISTS "${aqt_path}")
file(DOWNLOAD
${AQT_PREBUILD_BASE_URL}/aqt.exe
${aqt_path} SHOW_PROGRESS)
endif()
execute_process(COMMAND ${aqt_path} ${install_args}
WORKING_DIRECTORY ${base_path})
elseif (APPLE)
set(aqt_path "${base_path}/aqt-macos")
file(DOWNLOAD
${AQT_PREBUILD_BASE_URL}/aqt-macos
${aqt_path} SHOW_PROGRESS)
if (NOT EXISTS "${aqt_path}")
file(DOWNLOAD
${AQT_PREBUILD_BASE_URL}/aqt-macos
${aqt_path} SHOW_PROGRESS)
endif()
execute_process(COMMAND chmod +x ${aqt_path})
execute_process(COMMAND ${aqt_path} ${install_args}
WORKING_DIRECTORY ${base_path})
@ -96,18 +135,38 @@ function(download_qt target)
execute_process(COMMAND ${CMAKE_COMMAND} -E env PYTHONPATH=${aqt_install_path} python3 -m aqt ${install_args}
WORKING_DIRECTORY ${base_path})
endif()
message(STATUS "Downloaded Qt binaries for ${target}:${host}:${type}:${arch}:${arch_path} to ${prefix}")
endif()
message(STATUS "Using downloaded Qt binaries at ${prefix}")
set(${prefix_out} "${prefix}" PARENT_SCOPE)
endfunction()
# Add the Qt prefix path so CMake can locate it.
# This function downloads Qt using aqt.
# The path of the downloaded content will be added to the CMAKE_PREFIX_PATH.
# QT_TARGET_PATH is set to the Qt for the compile target platform.
# QT_HOST_PATH is set to a host-compatible Qt, for running tools.
# Params:
# target: Qt dependency to install. Specify a version number to download Qt, or "tools_(name)" for a specific build tool.
function(download_qt target)
determine_qt_parameters("${target}" host type arch arch_path host_type host_arch host_arch_path)
get_external_prefix(qt base_path)
file(MAKE_DIRECTORY "${base_path}")
download_qt_configuration(prefix "${target}" "${host}" "${type}" "${arch}" "${arch_path}" "${base_path}")
if (DEFINED host_arch_path AND NOT "${host_arch_path}" STREQUAL "${arch_path}")
download_qt_configuration(host_prefix "${target}" "${host}" "${host_type}" "${host_arch}" "${host_arch_path}" "${base_path}")
else()
set(host_prefix "${prefix}")
endif()
set(QT_TARGET_PATH "${prefix}" CACHE STRING "")
set(QT_HOST_PATH "${host_prefix}" CACHE STRING "")
# Add the target Qt prefix path so CMake can locate it.
list(APPEND CMAKE_PREFIX_PATH "${prefix}")
set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} PARENT_SCOPE)
if (DEFINED host_prefix)
message(STATUS "Using downloaded host Qt binaries at ${host_prefix}")
set(QT_HOST_PATH "${host_prefix}" CACHE STRING "")
endif()
endfunction()
function(download_moltenvk)
@ -121,7 +180,7 @@ function(download_moltenvk)
set(MOLTENVK_TAR "${CMAKE_BINARY_DIR}/externals/MoltenVK.tar")
if (NOT EXISTS ${MOLTENVK_DIR})
if (NOT EXISTS ${MOLTENVK_TAR})
file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/latest/download/MoltenVK-all.tar
file(DOWNLOAD https://github.com/KhronosGroup/MoltenVK/releases/download/v1.2.7-rc2/MoltenVK-all.tar
${MOLTENVK_TAR} SHOW_PROGRESS)
endif()

View File

@ -26,16 +26,14 @@ set(HASH_FILES
"${VIDEO_CORE}/shader/generator/spv_fs_shader_gen.h"
"${VIDEO_CORE}/shader/shader.cpp"
"${VIDEO_CORE}/shader/shader.h"
"${VIDEO_CORE}/pica.cpp"
"${VIDEO_CORE}/pica.h"
"${VIDEO_CORE}/regs_framebuffer.h"
"${VIDEO_CORE}/regs_lighting.h"
"${VIDEO_CORE}/regs_pipeline.h"
"${VIDEO_CORE}/regs_rasterizer.h"
"${VIDEO_CORE}/regs_shader.h"
"${VIDEO_CORE}/regs_texturing.h"
"${VIDEO_CORE}/regs.cpp"
"${VIDEO_CORE}/regs.h"
"${VIDEO_CORE}/pica/regs_framebuffer.h"
"${VIDEO_CORE}/pica/regs_lighting.h"
"${VIDEO_CORE}/pica/regs_pipeline.h"
"${VIDEO_CORE}/pica/regs_rasterizer.h"
"${VIDEO_CORE}/pica/regs_shader.h"
"${VIDEO_CORE}/pica/regs_texturing.h"
"${VIDEO_CORE}/pica/regs_internal.cpp"
"${VIDEO_CORE}/pica/regs_internal.h"
)
set(COMBINED "")
foreach (F IN LISTS HASH_FILES)

View File

@ -26,6 +26,38 @@
<!-- Fixed -->
<key>LSApplicationCategoryType</key>
<string>public.app-category.games</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>3ds</string>
<string>3dsx</string>
<string>cci</string>
<string>cxi</string>
<string>cia</string>
</array>
<key>CFBundleTypeName</key>
<string>Nintendo 3DS File</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Default</string>
</dict>
<dict>
<key>CFBundleTypeExtensions</key>
<array>
<string>elf</string>
<string>axf</string>
</array>
<key>CFBundleTypeName</key>
<string>Unix Executable and Linkable Format</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
</dict>
</array>
<key>NSCameraUsageDescription</key>
<string>This app requires camera access to emulate the 3DS&apos;s cameras.</string>
<key>NSMicrophoneUsageDescription</key>

View File

@ -287,5 +287,13 @@ dumptxt -p $[OUT] "nfcSecret1Seed=$[NFC_SEED_1]"
dumptxt -p $[OUT] "nfcSecret1HmacKey=$[NFC_HMAC_KEY_1]"
dumptxt -p $[OUT] "nfcIv=$[NFC_IV]"
# Dump seeddb.bin as well
set SEEDDB_IN "0:/gm9/out/seeddb.bin"
set SEEDDB_OUT "0:/gm9/seeddb.bin"
sdump -w seeddb.bin
cp -w $[SEEDDB_IN] $[SEEDDB_OUT]
@Exit

View File

@ -6,5 +6,5 @@ Usage:
1. Copy "DumpKeys.gm9" into the "gm9/scripts/" directory on your SD card.
2. Launch GodMode9, press the HOME button, select Scripts, and select "DumpKeys" from the list of scripts that appears.
3. Wait for the script to complete and return you to the GodMode9 main menu.
4. Power off your system and copy the "gm9/aes_keys.txt" file off of your SD card into "(Citra directory)/sysdata/".
4. Power off your system and copy the "gm9/aes_keys.txt" and "gm9/seeddb.bin" files off of your SD card into "(Citra directory)/sysdata/".

View File

@ -7,3 +7,8 @@ source_file = en.ts
source_lang = en
type = QT
[o:citra:p:citra:r:android]
file_filter = ../../src/android/app/src/main/res/values-<lang>/strings.xml
source_file = ../../src/android/app/src/main/res/values/strings.xml
type = ANDROID
lang_map = es_ES:es, hu_HU:hu, ru_RU:ru, pt_BR:pt, zh_CN:zh

File diff suppressed because it is too large Load Diff

1519
dist/languages/de.ts vendored

File diff suppressed because it is too large Load Diff

854
dist/languages/el.ts vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

854
dist/languages/fi.ts vendored

File diff suppressed because it is too large Load Diff

6235
dist/languages/fi_FI.ts vendored

File diff suppressed because it is too large Load Diff

894
dist/languages/fr.ts vendored

File diff suppressed because it is too large Load Diff

1482
dist/languages/hu_HU.ts vendored

File diff suppressed because it is too large Load Diff

854
dist/languages/id.ts vendored

File diff suppressed because it is too large Load Diff

854
dist/languages/it.ts vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

854
dist/languages/nb.ts vendored

File diff suppressed because it is too large Load Diff

854
dist/languages/nl.ts vendored

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -12,18 +12,19 @@ QPushButton#GraphicsAPIStatusBarButton:hover {
border: 1px solid #76797C;
}
QPushButton#3DOptionStatusBarButton {
color: #A5A5A5;
font-weight: bold;
QPushButton#TogglableStatusBarButton {
color: #959595;
border: 1px solid transparent;
background-color: transparent;
padding: 0px 3px 0px 3px;
text-align: center;
min-width: 60px;
min-height: 20px;
}
QPushButton#3DOptionStatusBarButton:hover {
QPushButton#TogglableStatusBarButton:checked {
color: #00FF00;
}
QPushButton#TogglableStatusBarButton:hover {
border: 1px solid #76797C;
}

View File

@ -1,19 +1,3 @@
QPushButton#TogglableStatusBarButton {
color: #959595;
border: 1px solid transparent;
background-color: transparent;
padding: 0px 3px 0px 3px;
text-align: center;
}
QPushButton#TogglableStatusBarButton:checked {
color: palette(text);
}
QPushButton#TogglableStatusBarButton:hover {
border: 1px solid #76797C;
}
QPushButton#GraphicsAPIStatusBarButton {
color: #656565;
border: 1px solid transparent;
@ -26,6 +10,23 @@ QPushButton#GraphicsAPIStatusBarButton:hover {
border: 1px solid #76797C;
}
QPushButton#TogglableStatusBarButton {
min-width: 0px;
color: #656565;
border: 1px solid transparent;
background-color: transparent;
padding: 0px 3px 0px 3px;
text-align: center;
}
QPushButton#TogglableStatusBarButton:checked {
color: #00FF00;
}
QPushButton#TogglableStatusBarButton:hover {
border: 1px solid #76797C;
}
QToolTip {
border: 1px solid #76797C;
background-color: #5A7566;

View File

@ -57,6 +57,12 @@ if(USE_SYSTEM_CRYPTOPP)
add_library(cryptopp INTERFACE)
target_link_libraries(cryptopp INTERFACE cryptopp::cryptopp)
else()
if (WIN32 AND NOT MSVC AND "arm64" IN_LIST ARCHITECTURE)
# TODO: CryptoPP ARM64 ASM does not seem to support Windows unless compiled with MSVC.
# TODO: See https://github.com/weidai11/cryptopp/issues/1260
set(CRYPTOPP_DISABLE_ASM ON CACHE BOOL "")
endif()
set(CRYPTOPP_BUILD_DOCUMENTATION OFF CACHE BOOL "")
set(CRYPTOPP_BUILD_TESTING OFF CACHE BOOL "")
set(CRYPTOPP_INSTALL OFF CACHE BOOL "")
@ -120,29 +126,6 @@ if (MSVC)
add_subdirectory(getopt)
endif()
# Glad
add_subdirectory(glad)
# glslang
if(USE_SYSTEM_GLSLANG)
find_package(glslang REQUIRED)
add_library(glslang INTERFACE)
add_library(SPIRV INTERFACE)
target_link_libraries(glslang INTERFACE glslang::glslang)
target_link_libraries(SPIRV INTERFACE glslang::SPIRV)
# System include path is different from submodule include path
get_target_property(GLSLANG_PREFIX glslang::SPIRV INTERFACE_INCLUDE_DIRECTORIES)
target_include_directories(SPIRV SYSTEM INTERFACE "${GLSLANG_PREFIX}/glslang")
else()
set(SKIP_GLSLANG_INSTALL ON CACHE BOOL "")
set(ENABLE_GLSLANG_BINARIES OFF CACHE BOOL "")
set(ENABLE_SPVREMAPPER OFF CACHE BOOL "")
set(ENABLE_CTEST OFF CACHE BOOL "")
set(ENABLE_HLSL OFF CACHE BOOL "")
set(BUILD_EXTERNAL OFF CACHE BOOL "")
add_subdirectory(glslang)
endif()
# inih
if(USE_SYSTEM_INIH)
find_package(inih REQUIRED COMPONENTS inih inir)
@ -197,9 +180,6 @@ if(NOT USE_SYSTEM_SOUNDTOUCH)
target_compile_definitions(SoundTouch PUBLIC SOUNDTOUCH_INTEGER_SAMPLES)
endif()
# sirit
add_subdirectory(sirit EXCLUDE_FROM_ALL)
# Teakra
add_subdirectory(teakra EXCLUDE_FROM_ALL)
@ -261,6 +241,18 @@ endif()
# DiscordRPC
if (USE_DISCORD_PRESENCE)
# rapidjson used by discord-rpc is old and doesn't correctly detect endianness for some platforms.
include(TestBigEndian)
test_big_endian(RAPIDJSON_BIG_ENDIAN)
if(RAPIDJSON_BIG_ENDIAN)
add_compile_definitions(RAPIDJSON_ENDIAN=1)
else()
add_compile_definitions(RAPIDJSON_ENDIAN=0)
endif()
# Apply a dummy CLANG_FORMAT_SUFFIX to disable discord-rpc's unnecessary automatic clang-format.
set(CLANG_FORMAT_SUFFIX "dummy")
add_subdirectory(discord-rpc EXCLUDE_FROM_ALL)
target_include_directories(discord-rpc INTERFACE ./discord-rpc/include)
endif()
@ -302,11 +294,20 @@ endif()
add_library(httplib INTERFACE)
if(USE_SYSTEM_CPP_HTTPLIB)
find_package(CppHttp 0.14.1)
if(CppHttp_FOUND)
target_link_libraries(httplib INTERFACE httplib::httplib)
else()
message(STATUS "Cpp-httplib not found or not suitable version! Falling back to bundled...")
# Detect if system cpphttplib is a shared library
# this breaks building as Citra relies on functions that are moved
# into the shared object.
get_target_property(HTTP_LIBS httplib::httplib INTERFACE_LINK_LIBRARIES)
if(HTTP_LIBS)
message(WARNING "Shared cpp-http (${HTTP_LIBS}) not supported. Falling back to bundled...")
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
else()
if(CppHttp_FOUND)
target_link_libraries(httplib INTERFACE httplib::httplib)
else()
message(STATUS "Cpp-httplib not found or not suitable version! Falling back to bundled...")
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
endif()
endif()
else()
target_include_directories(httplib SYSTEM INTERFACE ./httplib)
@ -314,9 +315,8 @@ endif()
target_compile_options(httplib INTERFACE -DCPPHTTPLIB_OPENSSL_SUPPORT)
target_link_libraries(httplib INTERFACE ${OPENSSL_LIBRARIES})
if(ANDROID)
add_subdirectory(android-ifaddrs)
target_link_libraries(httplib INTERFACE ifaddrs)
if (UNIX AND NOT APPLE)
add_subdirectory(gamemode)
endif()
# cpp-jwt
@ -367,35 +367,64 @@ if (ENABLE_OPENAL)
endif()
endif()
# VMA
if(USE_SYSTEM_VMA)
add_library(vma INTERFACE)
find_package(VulkanMemoryAllocator REQUIRED)
if(TARGET GPUOpen::VulkanMemoryAllocator)
message(STATUS "Found VulkanMemoryAllocator")
target_link_libraries(vma INTERFACE GPUOpen::VulkanMemoryAllocator)
endif()
else()
add_library(vma INTERFACE)
target_include_directories(vma SYSTEM INTERFACE ./vma/include)
# OpenGL dependencies
if (ENABLE_OPENGL)
# Glad
add_subdirectory(glad)
endif()
# vulkan-headers
add_library(vulkan-headers INTERFACE)
if(USE_SYSTEM_VULKAN_HEADERS)
find_package(Vulkan REQUIRED)
if(TARGET Vulkan::Headers)
message(STATUS "Found Vulkan headers")
target_link_libraries(vulkan-headers INTERFACE Vulkan::Headers)
# Vulkan dependencies
if (ENABLE_VULKAN)
# glslang
if(USE_SYSTEM_GLSLANG)
find_package(glslang REQUIRED)
add_library(glslang INTERFACE)
add_library(SPIRV INTERFACE)
target_link_libraries(glslang INTERFACE glslang::glslang)
target_link_libraries(SPIRV INTERFACE glslang::SPIRV)
# System include path is different from submodule include path
get_target_property(GLSLANG_PREFIX glslang::SPIRV INTERFACE_INCLUDE_DIRECTORIES)
target_include_directories(SPIRV SYSTEM INTERFACE "${GLSLANG_PREFIX}/glslang")
else()
set(SKIP_GLSLANG_INSTALL ON CACHE BOOL "")
set(ENABLE_GLSLANG_BINARIES OFF CACHE BOOL "")
set(ENABLE_SPVREMAPPER OFF CACHE BOOL "")
set(ENABLE_CTEST OFF CACHE BOOL "")
set(ENABLE_HLSL OFF CACHE BOOL "")
set(BUILD_EXTERNAL OFF CACHE BOOL "")
add_subdirectory(glslang)
endif()
else()
target_include_directories(vulkan-headers SYSTEM INTERFACE ./vulkan-headers/include)
endif()
if (APPLE)
target_include_directories(vulkan-headers SYSTEM INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/MoltenVK)
endif()
# adrenotools
if (ANDROID AND "arm64" IN_LIST ARCHITECTURE)
add_subdirectory(libadrenotools)
# sirit
add_subdirectory(sirit EXCLUDE_FROM_ALL)
# VMA
if(USE_SYSTEM_VMA)
add_library(vma INTERFACE)
find_package(VulkanMemoryAllocator REQUIRED)
if(TARGET GPUOpen::VulkanMemoryAllocator)
message(STATUS "Found VulkanMemoryAllocator")
target_link_libraries(vma INTERFACE GPUOpen::VulkanMemoryAllocator)
endif()
else()
add_library(vma INTERFACE)
target_include_directories(vma SYSTEM INTERFACE ./vma/include)
endif()
# vulkan-headers
add_library(vulkan-headers INTERFACE)
if(USE_SYSTEM_VULKAN_HEADERS)
find_package(Vulkan REQUIRED)
if(TARGET Vulkan::Headers)
message(STATUS "Found Vulkan headers")
target_link_libraries(vulkan-headers INTERFACE Vulkan::Headers)
endif()
else()
target_include_directories(vulkan-headers SYSTEM INTERFACE ./vulkan-headers/include)
endif()
# adrenotools
if (ANDROID AND "arm64" IN_LIST ARCHITECTURE)
add_subdirectory(libadrenotools)
endif()
endif()

View File

@ -1,8 +0,0 @@
add_library(ifaddrs
ifaddrs.c
ifaddrs.h
)
create_target_directory_groups(ifaddrs)
target_include_directories(ifaddrs INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

View File

@ -1,600 +0,0 @@
/*
Copyright (c) 2013, Kenneth MacKay
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "ifaddrs.h"
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/socket.h>
#include <net/if_arp.h>
#include <netinet/in.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
typedef struct NetlinkList
{
struct NetlinkList *m_next;
struct nlmsghdr *m_data;
unsigned int m_size;
} NetlinkList;
static int netlink_socket(void)
{
int l_socket = socket(PF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
if(l_socket < 0)
{
return -1;
}
struct sockaddr_nl l_addr;
memset(&l_addr, 0, sizeof(l_addr));
l_addr.nl_family = AF_NETLINK;
if(bind(l_socket, (struct sockaddr *)&l_addr, sizeof(l_addr)) < 0)
{
close(l_socket);
return -1;
}
return l_socket;
}
static int netlink_send(int p_socket, int p_request)
{
char l_buffer[NLMSG_ALIGN(sizeof(struct nlmsghdr)) + NLMSG_ALIGN(sizeof(struct rtgenmsg))];
memset(l_buffer, 0, sizeof(l_buffer));
struct nlmsghdr *l_hdr = (struct nlmsghdr *)l_buffer;
struct rtgenmsg *l_msg = (struct rtgenmsg *)NLMSG_DATA(l_hdr);
l_hdr->nlmsg_len = NLMSG_LENGTH(sizeof(*l_msg));
l_hdr->nlmsg_type = p_request;
l_hdr->nlmsg_flags = NLM_F_ROOT | NLM_F_MATCH | NLM_F_REQUEST;
l_hdr->nlmsg_pid = 0;
l_hdr->nlmsg_seq = p_socket;
l_msg->rtgen_family = AF_UNSPEC;
struct sockaddr_nl l_addr;
memset(&l_addr, 0, sizeof(l_addr));
l_addr.nl_family = AF_NETLINK;
return (sendto(p_socket, l_hdr, l_hdr->nlmsg_len, 0, (struct sockaddr *)&l_addr, sizeof(l_addr)));
}
static int netlink_recv(int p_socket, void *p_buffer, size_t p_len)
{
struct msghdr l_msg;
struct iovec l_iov = { p_buffer, p_len };
struct sockaddr_nl l_addr;
int l_result;
for(;;)
{
l_msg.msg_name = (void *)&l_addr;
l_msg.msg_namelen = sizeof(l_addr);
l_msg.msg_iov = &l_iov;
l_msg.msg_iovlen = 1;
l_msg.msg_control = NULL;
l_msg.msg_controllen = 0;
l_msg.msg_flags = 0;
int l_result = recvmsg(p_socket, &l_msg, 0);
if(l_result < 0)
{
if(errno == EINTR)
{
continue;
}
return -2;
}
if(l_msg.msg_flags & MSG_TRUNC)
{ // buffer was too small
return -1;
}
return l_result;
}
}
static struct nlmsghdr *getNetlinkResponse(int p_socket, int *p_size, int *p_done)
{
size_t l_size = 4096;
void *l_buffer = NULL;
for(;;)
{
free(l_buffer);
l_buffer = malloc(l_size);
int l_read = netlink_recv(p_socket, l_buffer, l_size);
*p_size = l_read;
if(l_read == -2)
{
free(l_buffer);
return NULL;
}
if(l_read >= 0)
{
pid_t l_pid = getpid();
struct nlmsghdr *l_hdr;
for(l_hdr = (struct nlmsghdr *)l_buffer; NLMSG_OK(l_hdr, (unsigned int)l_read); l_hdr = (struct nlmsghdr *)NLMSG_NEXT(l_hdr, l_read))
{
if((pid_t)l_hdr->nlmsg_pid != l_pid || (int)l_hdr->nlmsg_seq != p_socket)
{
continue;
}
if(l_hdr->nlmsg_type == NLMSG_DONE)
{
*p_done = 1;
break;
}
if(l_hdr->nlmsg_type == NLMSG_ERROR)
{
free(l_buffer);
return NULL;
}
}
return l_buffer;
}
l_size *= 2;
}
}
static NetlinkList *newListItem(struct nlmsghdr *p_data, unsigned int p_size)
{
NetlinkList *l_item = malloc(sizeof(NetlinkList));
l_item->m_next = NULL;
l_item->m_data = p_data;
l_item->m_size = p_size;
return l_item;
}
static void freeResultList(NetlinkList *p_list)
{
NetlinkList *l_cur;
while(p_list)
{
l_cur = p_list;
p_list = p_list->m_next;
free(l_cur->m_data);
free(l_cur);
}
}
static NetlinkList *getResultList(int p_socket, int p_request)
{
if(netlink_send(p_socket, p_request) < 0)
{
return NULL;
}
NetlinkList *l_list = NULL;
NetlinkList *l_end = NULL;
int l_size;
int l_done = 0;
while(!l_done)
{
struct nlmsghdr *l_hdr = getNetlinkResponse(p_socket, &l_size, &l_done);
if(!l_hdr)
{ // error
freeResultList(l_list);
return NULL;
}
NetlinkList *l_item = newListItem(l_hdr, l_size);
if(!l_list)
{
l_list = l_item;
}
else
{
l_end->m_next = l_item;
}
l_end = l_item;
}
return l_list;
}
static size_t maxSize(size_t a, size_t b)
{
return (a > b ? a : b);
}
static size_t calcAddrLen(sa_family_t p_family, int p_dataSize)
{
switch(p_family)
{
case AF_INET:
return sizeof(struct sockaddr_in);
case AF_INET6:
return sizeof(struct sockaddr_in6);
case AF_PACKET:
return maxSize(sizeof(struct sockaddr_ll), offsetof(struct sockaddr_ll, sll_addr) + p_dataSize);
default:
return maxSize(sizeof(struct sockaddr), offsetof(struct sockaddr, sa_data) + p_dataSize);
}
}
static void makeSockaddr(sa_family_t p_family, struct sockaddr *p_dest, void *p_data, size_t p_size)
{
switch(p_family)
{
case AF_INET:
memcpy(&((struct sockaddr_in*)p_dest)->sin_addr, p_data, p_size);
break;
case AF_INET6:
memcpy(&((struct sockaddr_in6*)p_dest)->sin6_addr, p_data, p_size);
break;
case AF_PACKET:
memcpy(((struct sockaddr_ll*)p_dest)->sll_addr, p_data, p_size);
((struct sockaddr_ll*)p_dest)->sll_halen = p_size;
break;
default:
memcpy(p_dest->sa_data, p_data, p_size);
break;
}
p_dest->sa_family = p_family;
}
static void addToEnd(struct ifaddrs **p_resultList, struct ifaddrs *p_entry)
{
if(!*p_resultList)
{
*p_resultList = p_entry;
}
else
{
struct ifaddrs *l_cur = *p_resultList;
while(l_cur->ifa_next)
{
l_cur = l_cur->ifa_next;
}
l_cur->ifa_next = p_entry;
}
}
static void interpretLink(struct nlmsghdr *p_hdr, struct ifaddrs **p_links, struct ifaddrs **p_resultList)
{
struct ifinfomsg *l_info = (struct ifinfomsg *)NLMSG_DATA(p_hdr);
size_t l_nameSize = 0;
size_t l_addrSize = 0;
size_t l_dataSize = 0;
size_t l_rtaSize = NLMSG_PAYLOAD(p_hdr, sizeof(struct ifinfomsg));
struct rtattr *l_rta;
for(l_rta = (struct rtattr *)(((char *)l_info) + NLMSG_ALIGN(sizeof(struct ifinfomsg))); RTA_OK(l_rta, l_rtaSize); l_rta = RTA_NEXT(l_rta, l_rtaSize))
{
void *l_rtaData = RTA_DATA(l_rta);
size_t l_rtaDataSize = RTA_PAYLOAD(l_rta);
switch(l_rta->rta_type)
{
case IFLA_ADDRESS:
case IFLA_BROADCAST:
l_addrSize += NLMSG_ALIGN(calcAddrLen(AF_PACKET, l_rtaDataSize));
break;
case IFLA_IFNAME:
l_nameSize += NLMSG_ALIGN(l_rtaSize + 1);
break;
case IFLA_STATS:
l_dataSize += NLMSG_ALIGN(l_rtaSize);
break;
default:
break;
}
}
struct ifaddrs *l_entry = malloc(sizeof(struct ifaddrs) + l_nameSize + l_addrSize + l_dataSize);
memset(l_entry, 0, sizeof(struct ifaddrs));
l_entry->ifa_name = "";
char *l_name = ((char *)l_entry) + sizeof(struct ifaddrs);
char *l_addr = l_name + l_nameSize;
char *l_data = l_addr + l_addrSize;
l_entry->ifa_flags = l_info->ifi_flags;
l_rtaSize = NLMSG_PAYLOAD(p_hdr, sizeof(struct ifinfomsg));
for(l_rta = (struct rtattr *)(((char *)l_info) + NLMSG_ALIGN(sizeof(struct ifinfomsg))); RTA_OK(l_rta, l_rtaSize); l_rta = RTA_NEXT(l_rta, l_rtaSize))
{
void *l_rtaData = RTA_DATA(l_rta);
size_t l_rtaDataSize = RTA_PAYLOAD(l_rta);
switch(l_rta->rta_type)
{
case IFLA_ADDRESS:
case IFLA_BROADCAST:
{
size_t l_addrLen = calcAddrLen(AF_PACKET, l_rtaDataSize);
makeSockaddr(AF_PACKET, (struct sockaddr *)l_addr, l_rtaData, l_rtaDataSize);
((struct sockaddr_ll *)l_addr)->sll_ifindex = l_info->ifi_index;
((struct sockaddr_ll *)l_addr)->sll_hatype = l_info->ifi_type;
if(l_rta->rta_type == IFLA_ADDRESS)
{
l_entry->ifa_addr = (struct sockaddr *)l_addr;
}
else
{
l_entry->ifa_broadaddr = (struct sockaddr *)l_addr;
}
l_addr += NLMSG_ALIGN(l_addrLen);
break;
}
case IFLA_IFNAME:
strncpy(l_name, l_rtaData, l_rtaDataSize);
l_name[l_rtaDataSize] = '\0';
l_entry->ifa_name = l_name;
break;
case IFLA_STATS:
memcpy(l_data, l_rtaData, l_rtaDataSize);
l_entry->ifa_data = l_data;
break;
default:
break;
}
}
addToEnd(p_resultList, l_entry);
p_links[l_info->ifi_index - 1] = l_entry;
}
static void interpretAddr(struct nlmsghdr *p_hdr, struct ifaddrs **p_links, struct ifaddrs **p_resultList)
{
struct ifaddrmsg *l_info = (struct ifaddrmsg *)NLMSG_DATA(p_hdr);
size_t l_nameSize = 0;
size_t l_addrSize = 0;
int l_addedNetmask = 0;
size_t l_rtaSize = NLMSG_PAYLOAD(p_hdr, sizeof(struct ifaddrmsg));
struct rtattr *l_rta;
for(l_rta = (struct rtattr *)(((char *)l_info) + NLMSG_ALIGN(sizeof(struct ifaddrmsg))); RTA_OK(l_rta, l_rtaSize); l_rta = RTA_NEXT(l_rta, l_rtaSize))
{
void *l_rtaData = RTA_DATA(l_rta);
size_t l_rtaDataSize = RTA_PAYLOAD(l_rta);
if(l_info->ifa_family == AF_PACKET)
{
continue;
}
switch(l_rta->rta_type)
{
case IFA_ADDRESS:
case IFA_LOCAL:
if((l_info->ifa_family == AF_INET || l_info->ifa_family == AF_INET6) && !l_addedNetmask)
{ // make room for netmask
l_addrSize += NLMSG_ALIGN(calcAddrLen(l_info->ifa_family, l_rtaDataSize));
l_addedNetmask = 1;
}
case IFA_BROADCAST:
l_addrSize += NLMSG_ALIGN(calcAddrLen(l_info->ifa_family, l_rtaDataSize));
break;
case IFA_LABEL:
l_nameSize += NLMSG_ALIGN(l_rtaSize + 1);
break;
default:
break;
}
}
struct ifaddrs *l_entry = malloc(sizeof(struct ifaddrs) + l_nameSize + l_addrSize);
memset(l_entry, 0, sizeof(struct ifaddrs));
l_entry->ifa_name = p_links[l_info->ifa_index - 1]->ifa_name;
char *l_name = ((char *)l_entry) + sizeof(struct ifaddrs);
char *l_addr = l_name + l_nameSize;
l_entry->ifa_flags = l_info->ifa_flags | p_links[l_info->ifa_index - 1]->ifa_flags;
l_rtaSize = NLMSG_PAYLOAD(p_hdr, sizeof(struct ifaddrmsg));
for(l_rta = (struct rtattr *)(((char *)l_info) + NLMSG_ALIGN(sizeof(struct ifaddrmsg))); RTA_OK(l_rta, l_rtaSize); l_rta = RTA_NEXT(l_rta, l_rtaSize))
{
void *l_rtaData = RTA_DATA(l_rta);
size_t l_rtaDataSize = RTA_PAYLOAD(l_rta);
switch(l_rta->rta_type)
{
case IFA_ADDRESS:
case IFA_BROADCAST:
case IFA_LOCAL:
{
size_t l_addrLen = calcAddrLen(l_info->ifa_family, l_rtaDataSize);
makeSockaddr(l_info->ifa_family, (struct sockaddr *)l_addr, l_rtaData, l_rtaDataSize);
if(l_info->ifa_family == AF_INET6)
{
if(IN6_IS_ADDR_LINKLOCAL((struct in6_addr *)l_rtaData) || IN6_IS_ADDR_MC_LINKLOCAL((struct in6_addr *)l_rtaData))
{
((struct sockaddr_in6 *)l_addr)->sin6_scope_id = l_info->ifa_index;
}
}
if(l_rta->rta_type == IFA_ADDRESS)
{ // apparently in a point-to-point network IFA_ADDRESS contains the dest address and IFA_LOCAL contains the local address
if(l_entry->ifa_addr)
{
l_entry->ifa_dstaddr = (struct sockaddr *)l_addr;
}
else
{
l_entry->ifa_addr = (struct sockaddr *)l_addr;
}
}
else if(l_rta->rta_type == IFA_LOCAL)
{
if(l_entry->ifa_addr)
{
l_entry->ifa_dstaddr = l_entry->ifa_addr;
}
l_entry->ifa_addr = (struct sockaddr *)l_addr;
}
else
{
l_entry->ifa_broadaddr = (struct sockaddr *)l_addr;
}
l_addr += NLMSG_ALIGN(l_addrLen);
break;
}
case IFA_LABEL:
strncpy(l_name, l_rtaData, l_rtaDataSize);
l_name[l_rtaDataSize] = '\0';
l_entry->ifa_name = l_name;
break;
default:
break;
}
}
if(l_entry->ifa_addr && (l_entry->ifa_addr->sa_family == AF_INET || l_entry->ifa_addr->sa_family == AF_INET6))
{
unsigned l_maxPrefix = (l_entry->ifa_addr->sa_family == AF_INET ? 32 : 128);
unsigned l_prefix = (l_info->ifa_prefixlen > l_maxPrefix ? l_maxPrefix : l_info->ifa_prefixlen);
char l_mask[16] = {0};
unsigned i;
for(i=0; i<(l_prefix/8); ++i)
{
l_mask[i] = 0xff;
}
l_mask[i] = 0xff << (8 - (l_prefix % 8));
makeSockaddr(l_entry->ifa_addr->sa_family, (struct sockaddr *)l_addr, l_mask, l_maxPrefix / 8);
l_entry->ifa_netmask = (struct sockaddr *)l_addr;
}
addToEnd(p_resultList, l_entry);
}
static void interpret(int p_socket, NetlinkList *p_netlinkList, struct ifaddrs **p_links, struct ifaddrs **p_resultList)
{
pid_t l_pid = getpid();
for(; p_netlinkList; p_netlinkList = p_netlinkList->m_next)
{
unsigned int l_nlsize = p_netlinkList->m_size;
struct nlmsghdr *l_hdr;
for(l_hdr = p_netlinkList->m_data; NLMSG_OK(l_hdr, l_nlsize); l_hdr = NLMSG_NEXT(l_hdr, l_nlsize))
{
if((pid_t)l_hdr->nlmsg_pid != l_pid || (int)l_hdr->nlmsg_seq != p_socket)
{
continue;
}
if(l_hdr->nlmsg_type == NLMSG_DONE)
{
break;
}
if(l_hdr->nlmsg_type == RTM_NEWLINK)
{
interpretLink(l_hdr, p_links, p_resultList);
}
else if(l_hdr->nlmsg_type == RTM_NEWADDR)
{
interpretAddr(l_hdr, p_links, p_resultList);
}
}
}
}
static unsigned countLinks(int p_socket, NetlinkList *p_netlinkList)
{
unsigned l_links = 0;
pid_t l_pid = getpid();
for(; p_netlinkList; p_netlinkList = p_netlinkList->m_next)
{
unsigned int l_nlsize = p_netlinkList->m_size;
struct nlmsghdr *l_hdr;
for(l_hdr = p_netlinkList->m_data; NLMSG_OK(l_hdr, l_nlsize); l_hdr = NLMSG_NEXT(l_hdr, l_nlsize))
{
if((pid_t)l_hdr->nlmsg_pid != l_pid || (int)l_hdr->nlmsg_seq != p_socket)
{
continue;
}
if(l_hdr->nlmsg_type == NLMSG_DONE)
{
break;
}
if(l_hdr->nlmsg_type == RTM_NEWLINK)
{
++l_links;
}
}
}
return l_links;
}
int getifaddrs(struct ifaddrs **ifap)
{
if(!ifap)
{
return -1;
}
*ifap = NULL;
int l_socket = netlink_socket();
if(l_socket < 0)
{
return -1;
}
NetlinkList *l_linkResults = getResultList(l_socket, RTM_GETLINK);
if(!l_linkResults)
{
close(l_socket);
return -1;
}
NetlinkList *l_addrResults = getResultList(l_socket, RTM_GETADDR);
if(!l_addrResults)
{
close(l_socket);
freeResultList(l_linkResults);
return -1;
}
unsigned l_numLinks = countLinks(l_socket, l_linkResults) + countLinks(l_socket, l_addrResults);
struct ifaddrs *l_links[l_numLinks];
memset(l_links, 0, l_numLinks * sizeof(struct ifaddrs *));
interpret(l_socket, l_linkResults, l_links, ifap);
interpret(l_socket, l_addrResults, l_links, ifap);
freeResultList(l_linkResults);
freeResultList(l_addrResults);
close(l_socket);
return 0;
}
void freeifaddrs(struct ifaddrs *ifa)
{
struct ifaddrs *l_cur;
while(ifa)
{
l_cur = ifa;
ifa = ifa->ifa_next;
free(l_cur);
}
}

View File

@ -1,54 +0,0 @@
/*
* Copyright (c) 1995, 1999
* Berkeley Software Design, Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* THIS SOFTWARE IS PROVIDED BY Berkeley Software Design, Inc. ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL Berkeley Software Design, Inc. BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* BSDI ifaddrs.h,v 2.5 2000/02/23 14:51:59 dab Exp
*/
#ifndef _IFADDRS_H_
#define _IFADDRS_H_
struct ifaddrs {
struct ifaddrs *ifa_next;
char *ifa_name;
unsigned int ifa_flags;
struct sockaddr *ifa_addr;
struct sockaddr *ifa_netmask;
struct sockaddr *ifa_dstaddr;
void *ifa_data;
};
/*
* This may have been defined in <net/if.h>. Note that if <net/if.h> is
* to be included it must be included before this header file.
*/
#ifndef ifa_broadaddr
#define ifa_broadaddr ifa_dstaddr /* broadcast address interface */
#endif
#include <sys/cdefs.h>
__BEGIN_DECLS
extern int getifaddrs(struct ifaddrs **ifap);
extern void freeifaddrs(struct ifaddrs *ifa);
__END_DECLS
#endif

@ -1 +1 @@
Subproject commit 9327192b0095dc1f420b2082d37bd427b5750d48
Subproject commit a99c80c26686e44eddf0432140ae397f3efbd0b3

2
externals/cubeb vendored

@ -1 +1 @@
Subproject commit 48689ae7a73caeb747953f9ed664dc71d2f918d8
Subproject commit 799e775484b8fce7e986ee7a4f4b651fec2bca07

2
externals/dynarmic vendored

@ -1 +1 @@
Subproject commit d333a09b3b9152af3cb442902ae8ea18d8416470
Subproject commit 30f1a3c6289075ef4af08f5ec502be2fc8627a0c

9
externals/gamemode/CMakeLists.txt vendored Normal file
View File

@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: Copyright 2023 yuzu Emulator Project
# SPDX-License-Identifier: GPL-3.0-or-later
project(gamemode LANGUAGES CXX C)
add_library(gamemode include/gamemode_client.h)
target_include_directories(gamemode PUBLIC include)
set_target_properties(gamemode PROPERTIES LINKER_LANGUAGE C)

View File

@ -0,0 +1,379 @@
// SPDX-FileCopyrightText: Copyright 2017-2019 Feral Interactive
// SPDX-License-Identifier: BSD-3-Clause
/*
Copyright (c) 2017-2019, Feral Interactive
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Feral Interactive nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
#ifndef CLIENT_GAMEMODE_H
#define CLIENT_GAMEMODE_H
/*
* GameMode supports the following client functions
* Requests are refcounted in the daemon
*
* int gamemode_request_start() - Request gamemode starts
* 0 if the request was sent successfully
* -1 if the request failed
*
* int gamemode_request_end() - Request gamemode ends
* 0 if the request was sent successfully
* -1 if the request failed
*
* GAMEMODE_AUTO can be defined to make the above two functions apply during static init and
* destruction, as appropriate. In this configuration, errors will be printed to stderr
*
* int gamemode_query_status() - Query the current status of gamemode
* 0 if gamemode is inactive
* 1 if gamemode is active
* 2 if gamemode is active and this client is registered
* -1 if the query failed
*
* int gamemode_request_start_for(pid_t pid) - Request gamemode starts for another process
* 0 if the request was sent successfully
* -1 if the request failed
* -2 if the request was rejected
*
* int gamemode_request_end_for(pid_t pid) - Request gamemode ends for another process
* 0 if the request was sent successfully
* -1 if the request failed
* -2 if the request was rejected
*
* int gamemode_query_status_for(pid_t pid) - Query status of gamemode for another process
* 0 if gamemode is inactive
* 1 if gamemode is active
* 2 if gamemode is active and this client is registered
* -1 if the query failed
*
* const char* gamemode_error_string() - Get an error string
* returns a string describing any of the above errors
*
* Note: All the above requests can be blocking - dbus requests can and will block while the daemon
* handles the request. It is not recommended to make these calls in performance critical code
*/
#include <stdbool.h>
#include <stdio.h>
#include <dlfcn.h>
#include <string.h>
#include <assert.h>
#include <sys/types.h>
static char internal_gamemode_client_error_string[512] = { 0 };
/**
* Load libgamemode dynamically to dislodge us from most dependencies.
* This allows clients to link and/or use this regardless of runtime.
* See SDL2 for an example of the reasoning behind this in terms of
* dynamic versioning as well.
*/
static volatile int internal_libgamemode_loaded = 1;
/* Typedefs for the functions to load */
typedef int (*api_call_return_int)(void);
typedef const char *(*api_call_return_cstring)(void);
typedef int (*api_call_pid_return_int)(pid_t);
/* Storage for functors */
static api_call_return_int REAL_internal_gamemode_request_start = NULL;
static api_call_return_int REAL_internal_gamemode_request_end = NULL;
static api_call_return_int REAL_internal_gamemode_query_status = NULL;
static api_call_return_cstring REAL_internal_gamemode_error_string = NULL;
static api_call_pid_return_int REAL_internal_gamemode_request_start_for = NULL;
static api_call_pid_return_int REAL_internal_gamemode_request_end_for = NULL;
static api_call_pid_return_int REAL_internal_gamemode_query_status_for = NULL;
/**
* Internal helper to perform the symbol binding safely.
*
* Returns 0 on success and -1 on failure
*/
__attribute__((always_inline)) static inline int internal_bind_libgamemode_symbol(
void *handle, const char *name, void **out_func, size_t func_size, bool required)
{
void *symbol_lookup = NULL;
char *dl_error = NULL;
/* Safely look up the symbol */
symbol_lookup = dlsym(handle, name);
dl_error = dlerror();
if (required && (dl_error || !symbol_lookup)) {
snprintf(internal_gamemode_client_error_string,
sizeof(internal_gamemode_client_error_string),
"dlsym failed - %s",
dl_error);
return -1;
}
/* Have the symbol correctly, copy it to make it usable */
memcpy(out_func, &symbol_lookup, func_size);
return 0;
}
/**
* Loads libgamemode and needed functions
*
* Returns 0 on success and -1 on failure
*/
__attribute__((always_inline)) static inline int internal_load_libgamemode(void)
{
/* We start at 1, 0 is a success and -1 is a fail */
if (internal_libgamemode_loaded != 1) {
return internal_libgamemode_loaded;
}
/* Anonymous struct type to define our bindings */
struct binding {
const char *name;
void **functor;
size_t func_size;
bool required;
} bindings[] = {
{ "real_gamemode_request_start",
(void **)&REAL_internal_gamemode_request_start,
sizeof(REAL_internal_gamemode_request_start),
true },
{ "real_gamemode_request_end",
(void **)&REAL_internal_gamemode_request_end,
sizeof(REAL_internal_gamemode_request_end),
true },
{ "real_gamemode_query_status",
(void **)&REAL_internal_gamemode_query_status,
sizeof(REAL_internal_gamemode_query_status),
false },
{ "real_gamemode_error_string",
(void **)&REAL_internal_gamemode_error_string,
sizeof(REAL_internal_gamemode_error_string),
true },
{ "real_gamemode_request_start_for",
(void **)&REAL_internal_gamemode_request_start_for,
sizeof(REAL_internal_gamemode_request_start_for),
false },
{ "real_gamemode_request_end_for",
(void **)&REAL_internal_gamemode_request_end_for,
sizeof(REAL_internal_gamemode_request_end_for),
false },
{ "real_gamemode_query_status_for",
(void **)&REAL_internal_gamemode_query_status_for,
sizeof(REAL_internal_gamemode_query_status_for),
false },
};
void *libgamemode = NULL;
/* Try and load libgamemode */
libgamemode = dlopen("libgamemode.so.0", RTLD_NOW);
if (!libgamemode) {
/* Attempt to load unversioned library for compatibility with older
* versions (as of writing, there are no ABI changes between the two -
* this may need to change if ever ABI-breaking changes are made) */
libgamemode = dlopen("libgamemode.so", RTLD_NOW);
if (!libgamemode) {
snprintf(internal_gamemode_client_error_string,
sizeof(internal_gamemode_client_error_string),
"dlopen failed - %s",
dlerror());
internal_libgamemode_loaded = -1;
return -1;
}
}
/* Attempt to bind all symbols */
for (size_t i = 0; i < sizeof(bindings) / sizeof(bindings[0]); i++) {
struct binding *binder = &bindings[i];
if (internal_bind_libgamemode_symbol(libgamemode,
binder->name,
binder->functor,
binder->func_size,
binder->required)) {
internal_libgamemode_loaded = -1;
return -1;
};
}
/* Success */
internal_libgamemode_loaded = 0;
return 0;
}
/**
* Redirect to the real libgamemode
*/
__attribute__((always_inline)) static inline const char *gamemode_error_string(void)
{
/* If we fail to load the system gamemode, or we have an error string already, return our error
* string instead of diverting to the system version */
if (internal_load_libgamemode() < 0 || internal_gamemode_client_error_string[0] != '\0') {
return internal_gamemode_client_error_string;
}
/* Assert for static analyser that the function is not NULL */
assert(REAL_internal_gamemode_error_string != NULL);
return REAL_internal_gamemode_error_string();
}
/**
* Redirect to the real libgamemode
* Allow automatically requesting game mode
* Also prints errors as they happen.
*/
#ifdef GAMEMODE_AUTO
__attribute__((constructor))
#else
__attribute__((always_inline)) static inline
#endif
int gamemode_request_start(void)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
#ifdef GAMEMODE_AUTO
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
#endif
return -1;
}
/* Assert for static analyser that the function is not NULL */
assert(REAL_internal_gamemode_request_start != NULL);
if (REAL_internal_gamemode_request_start() < 0) {
#ifdef GAMEMODE_AUTO
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
#endif
return -1;
}
return 0;
}
/* Redirect to the real libgamemode */
#ifdef GAMEMODE_AUTO
__attribute__((destructor))
#else
__attribute__((always_inline)) static inline
#endif
int gamemode_request_end(void)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
#ifdef GAMEMODE_AUTO
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
#endif
return -1;
}
/* Assert for static analyser that the function is not NULL */
assert(REAL_internal_gamemode_request_end != NULL);
if (REAL_internal_gamemode_request_end() < 0) {
#ifdef GAMEMODE_AUTO
fprintf(stderr, "gamemodeauto: %s\n", gamemode_error_string());
#endif
return -1;
}
return 0;
}
/* Redirect to the real libgamemode */
__attribute__((always_inline)) static inline int gamemode_query_status(void)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
return -1;
}
if (REAL_internal_gamemode_query_status == NULL) {
snprintf(internal_gamemode_client_error_string,
sizeof(internal_gamemode_client_error_string),
"gamemode_query_status missing (older host?)");
return -1;
}
return REAL_internal_gamemode_query_status();
}
/* Redirect to the real libgamemode */
__attribute__((always_inline)) static inline int gamemode_request_start_for(pid_t pid)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
return -1;
}
if (REAL_internal_gamemode_request_start_for == NULL) {
snprintf(internal_gamemode_client_error_string,
sizeof(internal_gamemode_client_error_string),
"gamemode_request_start_for missing (older host?)");
return -1;
}
return REAL_internal_gamemode_request_start_for(pid);
}
/* Redirect to the real libgamemode */
__attribute__((always_inline)) static inline int gamemode_request_end_for(pid_t pid)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
return -1;
}
if (REAL_internal_gamemode_request_end_for == NULL) {
snprintf(internal_gamemode_client_error_string,
sizeof(internal_gamemode_client_error_string),
"gamemode_request_end_for missing (older host?)");
return -1;
}
return REAL_internal_gamemode_request_end_for(pid);
}
/* Redirect to the real libgamemode */
__attribute__((always_inline)) static inline int gamemode_query_status_for(pid_t pid)
{
/* Need to load gamemode */
if (internal_load_libgamemode() < 0) {
return -1;
}
if (REAL_internal_gamemode_query_status_for == NULL) {
snprintf(internal_gamemode_client_error_string,
sizeof(internal_gamemode_client_error_string),
"gamemode_query_status_for missing (older host?)");
return -1;
}
return REAL_internal_gamemode_query_status_for(pid);
}
#endif // CLIENT_GAMEMODE_H

File diff suppressed because it is too large Load Diff

2
externals/oaknut vendored

@ -1 +1 @@
Subproject commit e6eecc3f9460728be0a8d3f63e66d31c0362f472
Subproject commit 6b1d57ea7ed4882d32a91eeaa6557b0ecb4da152

@ -1 +1 @@
Subproject commit 85c2334e92e215cce34e8e0ed8b2dce4700f4a50
Subproject commit 217e93c664ec6704ec2d8c36fa116c1a4a1e2d40

View File

@ -110,12 +110,16 @@ 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_FORTIFY_SOURCE=2
-Wp,-D_GLIBCXX_ASSERTIONS
-fstack-protector-strong
-fstack-clash-protection
)
if (NOT CMAKE_BUILD_TYPE STREQUAL Debug)
# _FORTIFY_SOURCE can't be used without optimizations.
add_compile_options(-Wp,-D_FORTIFY_SOURCE=2)
endif()
if (CITRA_WARNINGS_AS_ERRORS)
add_compile_options(-Werror)
endif()
@ -124,6 +128,13 @@ else()
add_compile_options("-stdlib=libc++")
endif()
if (CMAKE_CXX_COMPILER_ID STREQUAL GNU)
# GCC may warn when it ignores attributes like maybe_unused,
# which is a problem for older versions (e.g. GCC 11).
add_compile_options("-Wno-attributes")
add_compile_options("-Wno-interference-size")
endif()
if (MINGW)
add_definitions(-DMINGW_HAS_SECURE_API)
if (COMPILE_WITH_DWARF)
@ -148,6 +159,16 @@ else()
endif()
endif()
if(ENABLE_SOFTWARE_RENDERER)
add_compile_definitions(ENABLE_SOFTWARE_RENDERER)
endif()
if(ENABLE_OPENGL)
add_compile_definitions(ENABLE_OPENGL)
endif()
if(ENABLE_VULKAN)
add_compile_definitions(ENABLE_VULKAN)
endif()
add_subdirectory(common)
add_subdirectory(core)
add_subdirectory(video_core)

View File

@ -10,7 +10,7 @@ plugins {
id("org.jetbrains.kotlin.android")
id("de.undercouch.download") version "5.5.0"
id("kotlin-parcelize")
kotlin("plugin.serialization") version "1.8.21"
kotlin("plugin.serialization") version "1.9.22"
id("androidx.navigation.safeargs.kotlin")
}
@ -29,7 +29,7 @@ android {
namespace = "org.citra.citra_emu"
compileSdkVersion = "android-34"
ndkVersion = "25.2.9519653"
ndkVersion = "26.1.10909125"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
@ -40,6 +40,10 @@ android {
jvmTarget = "17"
}
androidResources {
generateLocaleConfig = true
}
packaging {
// This is necessary for libadrenotools custom driver loading
jniLibs.useLegacyPackaging = true
@ -169,27 +173,23 @@ android {
dependencies {
implementation("androidx.recyclerview:recyclerview:1.3.2")
implementation("androidx.activity:activity-ktx:1.8.0")
implementation("androidx.activity:activity-ktx:1.8.2")
implementation("androidx.fragment:fragment-ktx:1.6.2")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.documentfile:documentfile:1.0.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1")
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.slidingpanelayout:slidingpanelayout:1.2.0")
implementation("com.google.android.material:material:1.9.0")
implementation("androidx.core:core-splashscreen:1.0.1")
implementation("androidx.work:work-runtime:2.8.1")
// For loading huge screenshots from the disk.
implementation("com.squareup.picasso:picasso:2.71828")
implementation("androidx.work:work-runtime:2.9.0")
implementation("org.ini4j:ini4j:0.5.4")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.navigation:navigation-fragment-ktx:2.7.5")
implementation("androidx.navigation:navigation-ui-ktx:2.7.5")
implementation("androidx.navigation:navigation-fragment-ktx:2.7.6")
implementation("androidx.navigation:navigation-ui-ktx:2.7.6")
implementation("info.debatty:java-string-similarity:2.0.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("io.coil-kt:coil:2.2.2")
implementation("io.coil-kt:coil:2.5.0")
}
// Download Vulkan Validation Layers from the KhronosGroup GitHub.

View File

@ -42,6 +42,9 @@
android:banner="@mipmap/ic_launcher"
android:requestLegacyExternalStorage="true">
<meta-data android:name="android.game_mode_config"
android:resource="@xml/game_mode_config" />
<activity
android:name="org.citra.citra_emu.ui.main.MainActivity"
android:theme="@style/Theme.Citra.Splash.Main"

View File

@ -9,10 +9,13 @@ import android.app.Application
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import org.citra.citra_emu.utils.DirectoryInitialization
import org.citra.citra_emu.utils.DocumentsTree
import org.citra.citra_emu.utils.GpuDriverHelper
import org.citra.citra_emu.utils.PermissionsHandler
import org.citra.citra_emu.utils.Log
import org.citra.citra_emu.utils.MemoryUtil
class CitraApplication : Application() {
private fun createNotificationChannel() {
@ -53,9 +56,20 @@ class CitraApplication : Application() {
}
NativeLibrary.logDeviceInfo()
logDeviceInfo()
createNotificationChannel()
}
fun logDeviceInfo() {
Log.info("Device Manufacturer - ${Build.MANUFACTURER}")
Log.info("Device Model - ${Build.MODEL}")
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) {
Log.info("SoC Manufacturer - ${Build.SOC_MANUFACTURER}")
Log.info("SoC Model - ${Build.SOC_MODEL}")
}
Log.info("Total System Memory - ${MemoryUtil.getDeviceRAM()}")
}
companion object {
private var application: CitraApplication? = null

View File

@ -413,12 +413,12 @@ object NativeLibrary {
}
fun setEmulationActivity(emulationActivity: EmulationActivity?) {
Log.verbose("[NativeLibrary] Registering EmulationActivity.")
Log.debug("[NativeLibrary] Registering EmulationActivity.")
sEmulationActivity = WeakReference(emulationActivity)
}
fun clearEmulationActivity() {
Log.verbose("[NativeLibrary] Unregistering EmulationActivity.")
Log.debug("[NativeLibrary] Unregistering EmulationActivity.")
sEmulationActivity.clear()
}

View File

@ -20,7 +20,6 @@ import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.NotificationManagerCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
@ -32,13 +31,16 @@ import org.citra.citra_emu.R
import org.citra.citra_emu.camera.StillImageCameraHelper.OnFilePickerResult
import org.citra.citra_emu.contracts.OpenFileResultContract
import org.citra.citra_emu.databinding.ActivityEmulationBinding
import org.citra.citra_emu.display.ScreenAdjustmentUtil
import org.citra.citra_emu.features.hotkeys.HotkeyUtility
import org.citra.citra_emu.features.settings.model.SettingsViewModel
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
import org.citra.citra_emu.fragments.MessageDialogFragment
import org.citra.citra_emu.utils.ControllerMappingHelper
import org.citra.citra_emu.utils.EmulationMenuSettings
import org.citra.citra_emu.utils.FileBrowserHelper
import org.citra.citra_emu.utils.ForegroundService
import org.citra.citra_emu.utils.EmulationLifecycleUtil
import org.citra.citra_emu.utils.EmulationMenuSettings
import org.citra.citra_emu.utils.ThemeUtil
import org.citra.citra_emu.viewmodel.EmulationViewModel
@ -52,6 +54,8 @@ class EmulationActivity : AppCompatActivity() {
private val emulationViewModel: EmulationViewModel by viewModels()
private lateinit var binding: ActivityEmulationBinding
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
private lateinit var hotkeyUtility: HotkeyUtility
override fun onCreate(savedInstanceState: Bundle?) {
ThemeUtil.setTheme(this)
@ -61,6 +65,8 @@ class EmulationActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
binding = ActivityEmulationBinding.inflate(layoutInflater)
screenAdjustmentUtil = ScreenAdjustmentUtil(windowManager, settingsViewModel.settings)
hotkeyUtility = HotkeyUtility(screenAdjustmentUtil)
setContentView(binding.root)
val navHostFragment =
@ -82,6 +88,8 @@ class EmulationActivity : AppCompatActivity() {
// Start a foreground service to prevent the app from getting killed in the background
foregroundService = Intent(this, ForegroundService::class.java)
startForegroundService(foregroundService)
EmulationLifecycleUtil.addShutdownHook(hook = { this.finish() })
}
// On some devices, the system bars will not disappear on first boot or after some
@ -103,6 +111,7 @@ class EmulationActivity : AppCompatActivity() {
}
override fun onDestroy() {
EmulationLifecycleUtil.clear()
stopForegroundService(this)
super.onDestroy()
}
@ -188,6 +197,8 @@ class EmulationActivity : AppCompatActivity() {
onBackPressed()
}
hotkeyUtility.handleHotkey(button)
// Normal key events.
NativeLibrary.ButtonState.PRESSED
}

View File

@ -26,10 +26,9 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.citra.citra_emu.HomeNavigationDirections
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.R
import org.citra.citra_emu.activities.EmulationActivity
import org.citra.citra_emu.adapters.GameAdapter.GameViewHolder
import org.citra.citra_emu.databinding.CardGameBinding
import org.citra.citra_emu.features.cheats.ui.CheatsActivity
import org.citra.citra_emu.features.cheats.ui.CheatsFragmentDirections
import org.citra.citra_emu.model.Game
import org.citra.citra_emu.utils.GameIconUtils
import org.citra.citra_emu.viewmodel.GamesViewModel
@ -100,7 +99,8 @@ class GameAdapter(private val activity: AppCompatActivity) :
.setPositiveButton(android.R.string.ok, null)
.show()
} else {
CheatsActivity.launch(view.context, holder.game.titleId)
val action = CheatsFragmentDirections.actionGlobalCheatsFragment(holder.game.titleId)
view.findNavController().navigate(action)
}
return true
}

View File

@ -1,129 +0,0 @@
// Copyright 2020 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.applets;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.os.Bundle;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.R;
import org.citra.citra_emu.activities.EmulationActivity;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@Keep
public final class MiiSelector {
@Keep
public static class MiiSelectorConfig implements java.io.Serializable {
public boolean enable_cancel_button;
public String title;
public long initially_selected_mii_index;
// List of Miis to display
public String[] mii_names;
}
public static class MiiSelectorData {
public long return_code;
public int index;
private MiiSelectorData(long return_code, int index) {
this.return_code = return_code;
this.index = index;
}
}
public static class MiiSelectorDialogFragment extends DialogFragment {
static MiiSelectorDialogFragment newInstance(MiiSelectorConfig config) {
MiiSelectorDialogFragment frag = new MiiSelectorDialogFragment();
Bundle args = new Bundle();
args.putSerializable("config", config);
frag.setArguments(args);
return frag;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity emulationActivity = Objects.requireNonNull(getActivity());
MiiSelectorConfig config =
Objects.requireNonNull((MiiSelectorConfig) Objects.requireNonNull(getArguments())
.getSerializable("config"));
// Note: we intentionally leave out the Standard Mii in the native code so that
// the string can get translated
ArrayList<String> list = new ArrayList<>();
list.add(emulationActivity.getString(R.string.standard_mii));
list.addAll(Arrays.asList(config.mii_names));
final int initialIndex = config.initially_selected_mii_index < list.size()
? (int) config.initially_selected_mii_index
: 0;
data.index = initialIndex;
MaterialAlertDialogBuilder builder =
new MaterialAlertDialogBuilder(emulationActivity)
.setTitle(config.title.isEmpty()
? emulationActivity.getString(R.string.mii_selector)
: config.title)
.setSingleChoiceItems(list.toArray(new String[]{}), initialIndex,
(dialog, which) -> {
data.index = which;
})
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
data.return_code = 0;
synchronized (finishLock) {
finishLock.notifyAll();
}
});
if (config.enable_cancel_button) {
builder.setNegativeButton(android.R.string.cancel, (dialog, which) -> {
data.return_code = 1;
synchronized (finishLock) {
finishLock.notifyAll();
}
});
}
setCancelable(false);
return builder.create();
}
}
private static MiiSelectorData data;
private static final Object finishLock = new Object();
private static void ExecuteImpl(MiiSelectorConfig config) {
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
data = new MiiSelectorData(0, 0);
MiiSelectorDialogFragment fragment = MiiSelectorDialogFragment.newInstance(config);
fragment.show(emulationActivity.getSupportFragmentManager(), "mii_selector");
}
public static MiiSelectorData Execute(MiiSelectorConfig config) {
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config));
synchronized (finishLock) {
try {
finishLock.wait();
} catch (Exception ignored) {
}
}
return data;
}
}

View File

@ -0,0 +1,47 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.applets
import androidx.annotation.Keep
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.fragments.MiiSelectorDialogFragment
import java.io.Serializable
@Keep
object MiiSelector {
lateinit var data: MiiSelectorData
val finishLock = Object()
private fun ExecuteImpl(config: MiiSelectorConfig) {
val emulationActivity = NativeLibrary.sEmulationActivity.get()
data = MiiSelectorData(0, 0)
val fragment = MiiSelectorDialogFragment.newInstance(config)
fragment.show(emulationActivity!!.supportFragmentManager, "mii_selector")
}
@JvmStatic
fun Execute(config: MiiSelectorConfig): MiiSelectorData {
NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { ExecuteImpl(config) }
synchronized(finishLock) {
try {
finishLock.wait()
} catch (ignored: Exception) {
}
}
return data
}
@Keep
class MiiSelectorConfig : Serializable {
var enableCancelButton = false
var title: String? = null
var initiallySelectedMiiIndex: Long = 0
// List of Miis to display
lateinit var miiNames: Array<String>
}
class MiiSelectorData (var returnCode: Long, var index: Int)
}

View File

@ -1,279 +0,0 @@
// Copyright 2020 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.applets;
import android.app.Activity;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.os.Bundle;
import android.text.InputFilter;
import android.text.Spanned;
import android.util.TypedValue;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.FrameLayout;
import androidx.annotation.ColorInt;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.citra.citra_emu.CitraApplication;
import org.citra.citra_emu.NativeLibrary;
import org.citra.citra_emu.R;
import org.citra.citra_emu.activities.EmulationActivity;
import org.citra.citra_emu.utils.Log;
import java.util.Objects;
@Keep
public final class SoftwareKeyboard {
/// Corresponds to Frontend::ButtonConfig
private interface ButtonConfig {
int Single = 0; /// Ok button
int Dual = 1; /// Cancel | Ok buttons
int Triple = 2; /// Cancel | I Forgot | Ok buttons
int None = 3; /// No button (returned by swkbdInputText in special cases)
}
/// Corresponds to Frontend::ValidationError
public enum ValidationError {
None,
// Button Selection
ButtonOutOfRange,
// Configured Filters
MaxDigitsExceeded,
AtSignNotAllowed,
PercentNotAllowed,
BackslashNotAllowed,
ProfanityNotAllowed,
CallbackFailed,
// Allowed Input Type
FixedLengthRequired,
MaxLengthExceeded,
BlankInputNotAllowed,
EmptyInputNotAllowed,
}
@Keep
public static class KeyboardConfig implements java.io.Serializable {
public int button_config;
public int max_text_length;
public boolean multiline_mode; /// True if the keyboard accepts multiple lines of input
public String hint_text; /// Displayed in the field as a hint before
@Nullable
public String[] button_text; /// Contains the button text that the caller provides
}
/// Corresponds to Frontend::KeyboardData
public static class KeyboardData {
public int button;
public String text;
private KeyboardData(int button, String text) {
this.button = button;
this.text = text;
}
}
private static class Filter implements InputFilter {
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
int dstart, int dend) {
String text = new StringBuilder(dest)
.replace(dstart, dend, source.subSequence(start, end).toString())
.toString();
if (ValidateFilters(text) == ValidationError.None) {
return null; // Accept replacement
}
return dest.subSequence(dstart, dend); // Request the subsequence to be unchanged
}
}
public static class KeyboardDialogFragment extends DialogFragment {
static KeyboardDialogFragment newInstance(KeyboardConfig config) {
KeyboardDialogFragment frag = new KeyboardDialogFragment();
Bundle args = new Bundle();
args.putSerializable("config", config);
frag.setArguments(args);
return frag;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
final Activity emulationActivity = getActivity();
assert emulationActivity != null;
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.leftMargin = params.rightMargin =
CitraApplication.Companion.getAppContext().getResources().getDimensionPixelSize(
R.dimen.dialog_margin);
KeyboardConfig config = Objects.requireNonNull(
(KeyboardConfig) Objects.requireNonNull(getArguments()).getSerializable("config"));
// Set up the input
EditText editText = new EditText(CitraApplication.Companion.getAppContext());
editText.setHint(config.hint_text);
editText.setSingleLine(!config.multiline_mode);
editText.setLayoutParams(params);
editText.setFilters(new InputFilter[]{
new Filter(), new InputFilter.LengthFilter(config.max_text_length)});
TypedValue typedValue = new TypedValue();
Resources.Theme theme = requireContext().getTheme();
theme.resolveAttribute(R.attr.colorOnSurface, typedValue, true);
@ColorInt int color = typedValue.data;
editText.setHintTextColor(color);
editText.setTextColor(color);
FrameLayout container = new FrameLayout(emulationActivity);
container.addView(editText);
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(emulationActivity)
.setTitle(R.string.software_keyboard)
.setView(container);
setCancelable(false);
switch (config.button_config) {
case ButtonConfig.Triple: {
final String text = config.button_text[1].isEmpty()
? emulationActivity.getString(R.string.i_forgot)
: config.button_text[1];
builder.setNeutralButton(text, null);
}
// fallthrough
case ButtonConfig.Dual: {
final String text = config.button_text[0].isEmpty()
? emulationActivity.getString(android.R.string.cancel)
: config.button_text[0];
builder.setNegativeButton(text, null);
}
// fallthrough
case ButtonConfig.Single: {
final String text = config.button_text[2].isEmpty()
? emulationActivity.getString(android.R.string.ok)
: config.button_text[2];
builder.setPositiveButton(text, null);
break;
}
}
final AlertDialog dialog = builder.create();
dialog.create();
if (dialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) {
dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener((view) -> {
data.button = config.button_config;
data.text = editText.getText().toString();
final ValidationError error = ValidateInput(data.text);
if (error != ValidationError.None) {
HandleValidationError(config, error);
return;
}
dialog.dismiss();
synchronized (finishLock) {
finishLock.notifyAll();
}
});
}
if (dialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) {
dialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener((view) -> {
data.button = 1;
dialog.dismiss();
synchronized (finishLock) {
finishLock.notifyAll();
}
});
}
if (dialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) {
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((view) -> {
data.button = 0;
dialog.dismiss();
synchronized (finishLock) {
finishLock.notifyAll();
}
});
}
return dialog;
}
}
private static KeyboardData data;
private static final Object finishLock = new Object();
private static void ExecuteImpl(KeyboardConfig config) {
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
data = new KeyboardData(0, "");
KeyboardDialogFragment fragment = KeyboardDialogFragment.newInstance(config);
fragment.show(emulationActivity.getSupportFragmentManager(), "keyboard");
}
private static void HandleValidationError(KeyboardConfig config, ValidationError error) {
final EmulationActivity emulationActivity = NativeLibrary.sEmulationActivity.get();
String message = "";
switch (error) {
case FixedLengthRequired:
message =
emulationActivity.getString(R.string.fixed_length_required, config.max_text_length);
break;
case MaxLengthExceeded:
message =
emulationActivity.getString(R.string.max_length_exceeded, config.max_text_length);
break;
case BlankInputNotAllowed:
message = emulationActivity.getString(R.string.blank_input_not_allowed);
break;
case EmptyInputNotAllowed:
message = emulationActivity.getString(R.string.empty_input_not_allowed);
break;
}
new MaterialAlertDialogBuilder(emulationActivity)
.setTitle(R.string.software_keyboard)
.setMessage(message)
.setPositiveButton(android.R.string.ok, null)
.show();
}
public static KeyboardData Execute(KeyboardConfig config) {
if (config.button_config == ButtonConfig.None) {
Log.error("Unexpected button config None");
return new KeyboardData(0, "");
}
NativeLibrary.sEmulationActivity.get().runOnUiThread(() -> ExecuteImpl(config));
synchronized (finishLock) {
try {
finishLock.wait();
} catch (Exception ignored) {
}
}
return data;
}
public static void ShowError(String error) {
NativeLibrary.displayAlertMsg(
CitraApplication.Companion.getAppContext().getResources().getString(R.string.software_keyboard),
error, false);
}
private static native ValidationError ValidateFilters(String text);
private static native ValidationError ValidateInput(String text);
}

View File

@ -0,0 +1,152 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.applets
import android.text.InputFilter
import android.text.Spanned
import androidx.annotation.Keep
import org.citra.citra_emu.CitraApplication.Companion.appContext
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
import org.citra.citra_emu.fragments.KeyboardDialogFragment
import org.citra.citra_emu.fragments.MessageDialogFragment
import org.citra.citra_emu.utils.Log
import java.io.Serializable
@Keep
object SoftwareKeyboard {
lateinit var data: KeyboardData
val finishLock = Object()
private fun ExecuteImpl(config: KeyboardConfig) {
val emulationActivity = NativeLibrary.sEmulationActivity.get()
data = KeyboardData(0, "")
KeyboardDialogFragment.newInstance(config)
.show(emulationActivity!!.supportFragmentManager, KeyboardDialogFragment.TAG)
}
fun HandleValidationError(config: KeyboardConfig, error: ValidationError) {
val emulationActivity = NativeLibrary.sEmulationActivity.get()!!
val message: String = when (error) {
ValidationError.FixedLengthRequired -> emulationActivity.getString(
R.string.fixed_length_required,
config.maxTextLength
)
ValidationError.MaxLengthExceeded ->
emulationActivity.getString(R.string.max_length_exceeded, config.maxTextLength)
ValidationError.BlankInputNotAllowed ->
emulationActivity.getString(R.string.blank_input_not_allowed)
ValidationError.EmptyInputNotAllowed ->
emulationActivity.getString(R.string.empty_input_not_allowed)
else -> emulationActivity.getString(R.string.invalid_input)
}
MessageDialogFragment.newInstance(R.string.software_keyboard, message).show(
NativeLibrary.sEmulationActivity.get()!!.supportFragmentManager,
MessageDialogFragment.TAG
)
}
@JvmStatic
fun Execute(config: KeyboardConfig): KeyboardData {
if (config.buttonConfig == ButtonConfig.None) {
Log.error("Unexpected button config None")
return KeyboardData(0, "")
}
NativeLibrary.sEmulationActivity.get()!!.runOnUiThread { ExecuteImpl(config) }
synchronized(finishLock) {
try {
finishLock.wait()
} catch (ignored: Exception) {
}
}
return data
}
@JvmStatic
fun ShowError(error: String) {
NativeLibrary.displayAlertMsg(
appContext.resources.getString(R.string.software_keyboard),
error,
false
)
}
private external fun ValidateFilters(text: String): ValidationError
external fun ValidateInput(text: String): ValidationError
/// Corresponds to Frontend::ButtonConfig
interface ButtonConfig {
companion object {
const val Single = 0 /// Ok button
const val Dual = 1 /// Cancel | Ok buttons
const val Triple = 2 /// Cancel | I Forgot | Ok buttons
const val None = 3 /// No button (returned by swkbdInputText in special cases)
}
}
/// Corresponds to Frontend::ValidationError
enum class ValidationError {
None,
// Button Selection
ButtonOutOfRange,
// Configured Filters
MaxDigitsExceeded,
AtSignNotAllowed,
PercentNotAllowed,
BackslashNotAllowed,
ProfanityNotAllowed,
CallbackFailed,
// Allowed Input Type
FixedLengthRequired,
MaxLengthExceeded,
BlankInputNotAllowed,
EmptyInputNotAllowed
}
@Keep
class KeyboardConfig : Serializable {
var buttonConfig = 0
var maxTextLength = 0
// True if the keyboard accepts multiple lines of input
var multilineMode = false
// Displayed in the field as a hint before
var hintText: String? = null
// Contains the button text that the caller provides
lateinit var buttonText: Array<String>
}
/// Corresponds to Frontend::KeyboardData
class KeyboardData(var button: Int, var text: String)
class Filter : InputFilter {
override fun filter(
source: CharSequence,
start: Int,
end: Int,
dest: Spanned,
dstart: Int,
dend: Int
): CharSequence? {
val text = StringBuilder(dest)
.replace(dstart, dend, source.subSequence(start, end).toString())
.toString()
return if (ValidateFilters(text) == ValidationError.None) {
null // Accept replacement
} else {
dest.subSequence(dstart, dend) // Request the subsequence to be unchanged
}
}
}
}

View File

@ -1,24 +0,0 @@
package org.citra.citra_emu.contracts;
import android.content.Context;
import android.content.Intent;
import android.util.Pair;
import androidx.activity.result.contract.ActivityResultContract;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class OpenFileResultContract extends ActivityResultContract<Boolean, Intent> {
@NonNull
@Override
public Intent createIntent(@NonNull Context context, Boolean allowMultiple) {
return new Intent(Intent.ACTION_OPEN_DOCUMENT)
.setType("application/octet-stream")
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, allowMultiple);
}
@Override
public Intent parseResult(int i, @Nullable Intent intent) {
return intent;
}
}

View File

@ -0,0 +1,19 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.contracts
import android.content.Context
import android.content.Intent
import androidx.activity.result.contract.ActivityResultContract
class OpenFileResultContract : ActivityResultContract<Boolean?, Intent?>() {
override fun createIntent(context: Context, input: Boolean?): Intent {
return Intent(Intent.ACTION_OPEN_DOCUMENT)
.setType("application/octet-stream")
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, input)
}
override fun parseResult(resultCode: Int, intent: Intent?): Intent? = intent
}

View File

@ -0,0 +1,42 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.display
import android.view.WindowManager
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.features.settings.model.BooleanSetting
import org.citra.citra_emu.features.settings.model.IntSetting
import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.features.settings.utils.SettingsFile
import org.citra.citra_emu.utils.EmulationMenuSettings
class ScreenAdjustmentUtil(private val windowManager: WindowManager,
private val settings: Settings) {
fun swapScreen() {
val isEnabled = !EmulationMenuSettings.swapScreens
EmulationMenuSettings.swapScreens = isEnabled
NativeLibrary.swapScreens(
isEnabled,
windowManager.defaultDisplay.rotation
)
BooleanSetting.SWAP_SCREEN.boolean = isEnabled
settings.saveSetting(BooleanSetting.SWAP_SCREEN, SettingsFile.FILE_NAME_CONFIG)
}
fun cycleLayouts() {
val nextLayout = (EmulationMenuSettings.landscapeScreenLayout + 1) % ScreenLayout.entries.size
changeScreenOrientation(ScreenLayout.from(nextLayout))
}
fun changeScreenOrientation(layoutOption: ScreenLayout) {
EmulationMenuSettings.landscapeScreenLayout = layoutOption.int
NativeLibrary.notifyOrientationChange(
EmulationMenuSettings.landscapeScreenLayout,
windowManager.defaultDisplay.rotation
)
IntSetting.SCREEN_LAYOUT.int = layoutOption.int
settings.saveSetting(IntSetting.SCREEN_LAYOUT, SettingsFile.FILE_NAME_CONFIG)
}
}

View File

@ -0,0 +1,22 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.display
enum class ScreenLayout(val int: Int) {
// These must match what is defined in src/common/settings.h
DEFAULT(0),
SINGLE_SCREEN(1),
LARGE_SCREEN(2),
SIDE_SCREEN(3),
HYBRID_SCREEN(4),
MOBILE_PORTRAIT(5),
MOBILE_LANDSCAPE(6);
companion object {
fun from(int: Int): ScreenLayout {
return entries.firstOrNull { it.int == int } ?: DEFAULT
}
}
}

View File

@ -1,57 +0,0 @@
package org.citra.citra_emu.features.cheats.model;
import androidx.annotation.Keep;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class Cheat {
@Keep
private final long mPointer;
private Runnable mEnabledChangedCallback = null;
@Keep
private Cheat(long pointer) {
mPointer = pointer;
}
@Override
protected native void finalize();
@NonNull
public native String getName();
@NonNull
public native String getNotes();
@NonNull
public native String getCode();
public native boolean getEnabled();
public void setEnabled(boolean enabled) {
setEnabledImpl(enabled);
onEnabledChanged();
}
private native void setEnabledImpl(boolean enabled);
public void setEnabledChangedCallback(@Nullable Runnable callback) {
mEnabledChangedCallback = callback;
}
private void onEnabledChanged() {
if (mEnabledChangedCallback != null) {
mEnabledChangedCallback.run();
}
}
/**
* If the code is valid, returns 0. Otherwise, returns the 1-based index
* for the line containing the error.
*/
public static native int isValidGatewayCode(@NonNull String code);
public static native Cheat createGatewayCode(@NonNull String name, @NonNull String notes,
@NonNull String code);
}

View File

@ -0,0 +1,48 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.cheats.model
import androidx.annotation.Keep
@Keep
class Cheat(@field:Keep private val mPointer: Long) {
private var enabledChangedCallback: Runnable? = null
protected external fun finalize()
external fun getName(): String
external fun getNotes(): String
external fun getCode(): String
external fun getEnabled(): Boolean
fun setEnabled(enabled: Boolean) {
setEnabledImpl(enabled)
onEnabledChanged()
}
private external fun setEnabledImpl(enabled: Boolean)
fun setEnabledChangedCallback(callback: Runnable) {
enabledChangedCallback = callback
}
private fun onEnabledChanged() {
enabledChangedCallback?.run()
}
companion object {
/**
* If the code is valid, returns 0. Otherwise, returns the 1-based index
* for the line containing the error.
*/
@JvmStatic
external fun isValidGatewayCode(code: String): Int
@JvmStatic
external fun createGatewayCode(name: String, notes: String, code: String): Cheat
}
}

View File

@ -1,28 +0,0 @@
package org.citra.citra_emu.features.cheats.model;
import androidx.annotation.Keep;
public class CheatEngine {
@Keep
private final long mPointer;
@Keep
public CheatEngine(long titleId) {
mPointer = initialize(titleId);
}
private static native long initialize(long titleId);
@Override
protected native void finalize();
public native Cheat[] getCheats();
public native void addCheat(Cheat cheat);
public native void removeCheat(int index);
public native void updateCheat(int index, Cheat newCheat);
public native void saveCheatFile();
}

View File

@ -0,0 +1,19 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.cheats.model
import androidx.annotation.Keep
@Keep
object CheatEngine {
external fun loadCheatFile(titleId: Long)
external fun saveCheatFile(titleId: Long)
external fun getCheats(): Array<Cheat>
external fun addCheat(cheat: Cheat?)
external fun removeCheat(index: Int)
external fun updateCheat(index: Int, newCheat: Cheat?)
}

View File

@ -1,187 +0,0 @@
package org.citra.citra_emu.features.cheats.model;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
public class CheatsViewModel extends ViewModel {
private int mSelectedCheatPosition = -1;
private final MutableLiveData<Cheat> mSelectedCheat = new MutableLiveData<>(null);
private final MutableLiveData<Boolean> mIsAdding = new MutableLiveData<>(false);
private final MutableLiveData<Boolean> mIsEditing = new MutableLiveData<>(false);
private final MutableLiveData<Integer> mCheatAddedEvent = new MutableLiveData<>(null);
private final MutableLiveData<Integer> mCheatChangedEvent = new MutableLiveData<>(null);
private final MutableLiveData<Integer> mCheatDeletedEvent = new MutableLiveData<>(null);
private final MutableLiveData<Boolean> mOpenDetailsViewEvent = new MutableLiveData<>(false);
private CheatEngine mCheatEngine;
private Cheat[] mCheats;
private boolean mCheatsNeedSaving = false;
public void initialize(long titleId) {
mCheatEngine = new CheatEngine(titleId);
load();
}
private void load() {
mCheats = mCheatEngine.getCheats();
for (int i = 0; i < mCheats.length; i++) {
int position = i;
mCheats[i].setEnabledChangedCallback(() -> {
mCheatsNeedSaving = true;
notifyCheatUpdated(position);
});
}
}
public void saveIfNeeded() {
if (mCheatsNeedSaving) {
mCheatEngine.saveCheatFile();
mCheatsNeedSaving = false;
}
}
public Cheat[] getCheats() {
return mCheats;
}
public LiveData<Cheat> getSelectedCheat() {
return mSelectedCheat;
}
public void setSelectedCheat(Cheat cheat, int position) {
if (mIsEditing.getValue()) {
setIsEditing(false);
}
mSelectedCheat.setValue(cheat);
mSelectedCheatPosition = position;
}
public LiveData<Boolean> getIsAdding() {
return mIsAdding;
}
public LiveData<Boolean> getIsEditing() {
return mIsEditing;
}
public void setIsEditing(boolean isEditing) {
mIsEditing.setValue(isEditing);
if (mIsAdding.getValue() && !isEditing) {
mIsAdding.setValue(false);
setSelectedCheat(null, -1);
}
}
/**
* When a cheat is added, the integer stored in the returned LiveData
* changes to the position of that cheat, then changes back to null.
*/
public LiveData<Integer> getCheatAddedEvent() {
return mCheatAddedEvent;
}
private void notifyCheatAdded(int position) {
mCheatAddedEvent.setValue(position);
mCheatAddedEvent.setValue(null);
}
public void startAddingCheat() {
mSelectedCheat.setValue(null);
mSelectedCheatPosition = -1;
mIsAdding.setValue(true);
mIsEditing.setValue(true);
}
public void finishAddingCheat(Cheat cheat) {
if (!mIsAdding.getValue()) {
throw new IllegalStateException();
}
mIsAdding.setValue(false);
mIsEditing.setValue(false);
int position = mCheats.length;
mCheatEngine.addCheat(cheat);
mCheatsNeedSaving = true;
load();
notifyCheatAdded(position);
setSelectedCheat(mCheats[position], position);
}
/**
* When a cheat is edited, the integer stored in the returned LiveData
* changes to the position of that cheat, then changes back to null.
*/
public LiveData<Integer> getCheatUpdatedEvent() {
return mCheatChangedEvent;
}
/**
* Notifies that an edit has been made to the contents of the cheat at the given position.
*/
private void notifyCheatUpdated(int position) {
mCheatChangedEvent.setValue(position);
mCheatChangedEvent.setValue(null);
}
public void updateSelectedCheat(Cheat newCheat) {
mCheatEngine.updateCheat(mSelectedCheatPosition, newCheat);
mCheatsNeedSaving = true;
load();
notifyCheatUpdated(mSelectedCheatPosition);
setSelectedCheat(mCheats[mSelectedCheatPosition], mSelectedCheatPosition);
}
/**
* When a cheat is deleted, the integer stored in the returned LiveData
* changes to the position of that cheat, then changes back to null.
*/
public LiveData<Integer> getCheatDeletedEvent() {
return mCheatDeletedEvent;
}
/**
* Notifies that the cheat at the given position has been deleted.
*/
private void notifyCheatDeleted(int position) {
mCheatDeletedEvent.setValue(position);
mCheatDeletedEvent.setValue(null);
}
public void deleteSelectedCheat() {
int position = mSelectedCheatPosition;
setSelectedCheat(null, -1);
mCheatEngine.removeCheat(position);
mCheatsNeedSaving = true;
load();
notifyCheatDeleted(position);
}
public LiveData<Boolean> getOpenDetailsViewEvent() {
return mOpenDetailsViewEvent;
}
public void openDetailsView() {
mOpenDetailsViewEvent.setValue(true);
mOpenDetailsViewEvent.setValue(false);
}
}

View File

@ -0,0 +1,170 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.cheats.model
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
class CheatsViewModel : ViewModel() {
val selectedCheat get() = _selectedCheat.asStateFlow()
private val _selectedCheat = MutableStateFlow<Cheat?>(null)
val isAdding get() = _isAdding.asStateFlow()
private val _isAdding = MutableStateFlow(false)
val isEditing get() = _isEditing.asStateFlow()
private val _isEditing = MutableStateFlow(false)
/**
* When a cheat is added, the integer stored in the returned StateFlow
* changes to the position of that cheat, then changes back to null.
*/
val cheatAddedEvent get() = _cheatAddedEvent.asStateFlow()
private val _cheatAddedEvent = MutableStateFlow<Int?>(null)
val cheatChangedEvent get() = _cheatChangedEvent.asStateFlow()
private val _cheatChangedEvent = MutableStateFlow<Int?>(null)
/**
* When a cheat is deleted, the integer stored in the returned StateFlow
* changes to the position of that cheat, then changes back to null.
*/
val cheatDeletedEvent get() = _cheatDeletedEvent.asStateFlow()
private val _cheatDeletedEvent = MutableStateFlow<Int?>(null)
val openDetailsViewEvent get() = _openDetailsViewEvent.asStateFlow()
private val _openDetailsViewEvent = MutableStateFlow(false)
val closeDetailsViewEvent get() = _closeDetailsViewEvent.asStateFlow()
private val _closeDetailsViewEvent = MutableStateFlow(false)
val listViewFocusChange get() = _listViewFocusChange.asStateFlow()
private val _listViewFocusChange = MutableStateFlow(false)
val detailsViewFocusChange get() = _detailsViewFocusChange.asStateFlow()
private val _detailsViewFocusChange = MutableStateFlow(false)
private var titleId: Long = 0
lateinit var cheats: Array<Cheat>
private var cheatsNeedSaving = false
private var selectedCheatPosition = -1
fun initialize(titleId_: Long) {
titleId = titleId_;
load()
}
private fun load() {
CheatEngine.loadCheatFile(titleId)
cheats = CheatEngine.getCheats()
for (i in cheats.indices) {
cheats[i].setEnabledChangedCallback {
cheatsNeedSaving = true
notifyCheatUpdated(i)
}
}
}
fun saveIfNeeded() {
if (cheatsNeedSaving) {
CheatEngine.saveCheatFile(titleId)
cheatsNeedSaving = false
}
}
fun setSelectedCheat(cheat: Cheat?, position: Int) {
if (isEditing.value) {
setIsEditing(false)
}
_selectedCheat.value = cheat
selectedCheatPosition = position
}
fun setIsEditing(value: Boolean) {
_isEditing.value = value
if (isAdding.value && !value) {
_isAdding.value = false
setSelectedCheat(null, -1)
}
}
private fun notifyCheatAdded(position: Int) {
_cheatAddedEvent.value = position
_cheatAddedEvent.value = null
}
fun startAddingCheat() {
_selectedCheat.value = null
selectedCheatPosition = -1
_isAdding.value = true
_isEditing.value = true
}
fun finishAddingCheat(cheat: Cheat?) {
check(isAdding.value)
_isAdding.value = false
_isEditing.value = false
val position = cheats.size
CheatEngine.addCheat(cheat)
cheatsNeedSaving = true
load()
notifyCheatAdded(position)
setSelectedCheat(cheats[position], position)
}
/**
* Notifies that an edit has been made to the contents of the cheat at the given position.
*/
private fun notifyCheatUpdated(position: Int) {
_cheatChangedEvent.value = position
_cheatChangedEvent.value = null
}
fun updateSelectedCheat(newCheat: Cheat?) {
CheatEngine.updateCheat(selectedCheatPosition, newCheat)
cheatsNeedSaving = true
load()
notifyCheatUpdated(selectedCheatPosition)
setSelectedCheat(cheats[selectedCheatPosition], selectedCheatPosition)
}
/**
* Notifies that the cheat at the given position has been deleted.
*/
private fun notifyCheatDeleted(position: Int) {
_cheatDeletedEvent.value = position
_cheatDeletedEvent.value = null
}
fun deleteSelectedCheat() {
val position = selectedCheatPosition
setSelectedCheat(null, -1)
CheatEngine.removeCheat(position)
cheatsNeedSaving = true
load()
notifyCheatDeleted(position)
}
fun openDetailsView() {
_openDetailsViewEvent.value = true
_openDetailsViewEvent.value = false
}
fun closeDetailsView() {
_closeDetailsViewEvent.value = true
_closeDetailsViewEvent.value = false
}
fun onListViewFocusChanged(changed: Boolean) {
_listViewFocusChange.value = changed
_listViewFocusChange.value = false
}
fun onDetailsViewFocusChanged(changed: Boolean) {
_detailsViewFocusChange.value = changed
_detailsViewFocusChange.value = false
}
}

View File

@ -1,175 +0,0 @@
package org.citra.citra_emu.features.cheats.ui;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ScrollView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.cheats.model.Cheat;
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
public class CheatDetailsFragment extends Fragment {
private View mRoot;
private ScrollView mScrollView;
private TextView mLabelName;
private EditText mEditName;
private EditText mEditNotes;
private EditText mEditCode;
private Button mButtonDelete;
private Button mButtonEdit;
private Button mButtonCancel;
private Button mButtonOk;
private CheatsViewModel mViewModel;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_cheat_details, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
mRoot = view.findViewById(R.id.root);
mScrollView = view.findViewById(R.id.scroll_view);
mLabelName = view.findViewById(R.id.label_name);
mEditName = view.findViewById(R.id.edit_name);
mEditNotes = view.findViewById(R.id.edit_notes);
mEditCode = view.findViewById(R.id.edit_code);
mButtonDelete = view.findViewById(R.id.button_delete);
mButtonEdit = view.findViewById(R.id.button_edit);
mButtonCancel = view.findViewById(R.id.button_cancel);
mButtonOk = view.findViewById(R.id.button_ok);
CheatsActivity activity = (CheatsActivity) requireActivity();
mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
mViewModel.getSelectedCheat().observe(getViewLifecycleOwner(),
this::onSelectedCheatUpdated);
mViewModel.getIsEditing().observe(getViewLifecycleOwner(), this::onIsEditingUpdated);
mButtonDelete.setOnClickListener(this::onDeleteClicked);
mButtonEdit.setOnClickListener(this::onEditClicked);
mButtonCancel.setOnClickListener(this::onCancelClicked);
mButtonOk.setOnClickListener(this::onOkClicked);
// On a portrait phone screen (or other narrow screen), only one of the two panes are shown
// at the same time. If the user is navigating using a d-pad and moves focus to an element
// in the currently hidden pane, we need to manually show that pane.
CheatsActivity.setOnFocusChangeListenerRecursively(view,
(v, hasFocus) -> activity.onDetailsViewFocusChange(hasFocus));
}
private void clearEditErrors() {
mEditName.setError(null);
mEditCode.setError(null);
}
private void onDeleteClicked(View view) {
String name = mEditName.getText().toString();
new MaterialAlertDialogBuilder(requireContext())
.setMessage(getString(R.string.cheats_delete_confirmation, name))
.setPositiveButton(android.R.string.yes,
(dialog, i) -> mViewModel.deleteSelectedCheat())
.setNegativeButton(android.R.string.no, null)
.show();
}
private void onEditClicked(View view) {
mViewModel.setIsEditing(true);
mButtonOk.requestFocus();
}
private void onCancelClicked(View view) {
mViewModel.setIsEditing(false);
onSelectedCheatUpdated(mViewModel.getSelectedCheat().getValue());
mButtonDelete.requestFocus();
}
private void onOkClicked(View view) {
clearEditErrors();
String name = mEditName.getText().toString();
String notes = mEditNotes.getText().toString();
String code = mEditCode.getText().toString();
if (name.isEmpty()) {
mEditName.setError(getString(R.string.cheats_error_no_name));
mScrollView.smoothScrollTo(0, mLabelName.getTop());
return;
} else if (code.isEmpty()) {
mEditCode.setError(getString(R.string.cheats_error_no_code_lines));
mScrollView.smoothScrollTo(0, mEditCode.getBottom());
return;
}
int validityResult = Cheat.isValidGatewayCode(code);
if (validityResult != 0) {
mEditCode.setError(getString(R.string.cheats_error_on_line, validityResult));
mScrollView.smoothScrollTo(0, mEditCode.getBottom());
return;
}
Cheat newCheat = Cheat.createGatewayCode(name, notes, code);
if (mViewModel.getIsAdding().getValue()) {
mViewModel.finishAddingCheat(newCheat);
} else {
mViewModel.updateSelectedCheat(newCheat);
}
mButtonEdit.requestFocus();
}
private void onSelectedCheatUpdated(@Nullable Cheat cheat) {
clearEditErrors();
boolean isEditing = mViewModel.getIsEditing().getValue();
mRoot.setVisibility(isEditing || cheat != null ? View.VISIBLE : View.GONE);
// If the fragment was recreated while editing a cheat, it's vital that we
// don't repopulate the fields, otherwise the user's changes will be lost
if (!isEditing) {
if (cheat == null) {
mEditName.setText("");
mEditNotes.setText("");
mEditCode.setText("");
} else {
mEditName.setText(cheat.getName());
mEditNotes.setText(cheat.getNotes());
mEditCode.setText(cheat.getCode());
}
}
}
private void onIsEditingUpdated(boolean isEditing) {
if (isEditing) {
mRoot.setVisibility(View.VISIBLE);
}
mEditName.setEnabled(isEditing);
mEditNotes.setEnabled(isEditing);
mEditCode.setEnabled(isEditing);
mButtonDelete.setVisibility(isEditing ? View.GONE : View.VISIBLE);
mButtonEdit.setVisibility(isEditing ? View.GONE : View.VISIBLE);
mButtonCancel.setVisibility(isEditing ? View.VISIBLE : View.GONE);
mButtonOk.setVisibility(isEditing ? View.VISIBLE : View.GONE);
}
}

View File

@ -0,0 +1,193 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.cheats.ui
import android.annotation.SuppressLint
import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.citra.citra_emu.R
import org.citra.citra_emu.databinding.FragmentCheatDetailsBinding
import org.citra.citra_emu.features.cheats.model.Cheat
import org.citra.citra_emu.features.cheats.model.CheatsViewModel
class CheatDetailsFragment : Fragment() {
private val cheatsViewModel: CheatsViewModel by activityViewModels()
private var _binding: FragmentCheatDetailsBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCheatDetailsBinding.inflate(layoutInflater)
return binding.root
}
// This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
cheatsViewModel.selectedCheat.collect { onSelectedCheatUpdated(it) }
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
cheatsViewModel.isEditing.collect { onIsEditingUpdated(it) }
}
}
}
binding.buttonDelete.setOnClickListener { onDeleteClicked() }
binding.buttonEdit.setOnClickListener { onEditClicked() }
binding.buttonCancel.setOnClickListener { onCancelClicked() }
binding.buttonOk.setOnClickListener { onOkClicked() }
// On a portrait phone screen (or other narrow screen), only one of the two panes are shown
// at the same time. If the user is navigating using a d-pad and moves focus to an element
// in the currently hidden pane, we need to manually show that pane.
CheatsActivity.setOnFocusChangeListenerRecursively(view) { _, hasFocus ->
cheatsViewModel.onDetailsViewFocusChanged(hasFocus)
}
binding.toolbarCheatDetails.setNavigationOnClickListener {
cheatsViewModel.closeDetailsView()
}
setInsets()
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
private fun clearEditErrors() {
binding.editName.error = null
binding.editCode.error = null
}
private fun onDeleteClicked() {
val name = binding.editNameInput.text.toString()
MaterialAlertDialogBuilder(requireContext())
.setMessage(getString(R.string.cheats_delete_confirmation, name))
.setPositiveButton(
android.R.string.ok
) { _: DialogInterface?, _: Int -> cheatsViewModel.deleteSelectedCheat() }
.setNegativeButton(android.R.string.cancel, null)
.show()
}
private fun onEditClicked() {
cheatsViewModel.setIsEditing(true)
binding.buttonOk.requestFocus()
}
private fun onCancelClicked() {
cheatsViewModel.setIsEditing(false)
onSelectedCheatUpdated(cheatsViewModel.selectedCheat.value)
binding.buttonDelete.requestFocus()
cheatsViewModel.closeDetailsView()
}
private fun onOkClicked() {
clearEditErrors()
val name = binding.editNameInput.text.toString()
val notes = binding.editNotesInput.text.toString()
val code = binding.editCodeInput.text.toString()
if (name.isEmpty()) {
binding.editName.error = getString(R.string.cheats_error_no_name)
binding.scrollView.smoothScrollTo(0, binding.editNameInput.top)
return
} else if (code.isEmpty()) {
binding.editCode.error = getString(R.string.cheats_error_no_code_lines)
binding.scrollView.smoothScrollTo(0, binding.editCodeInput.bottom)
return
}
val validityResult = Cheat.isValidGatewayCode(code)
if (validityResult != 0) {
binding.editCode.error = getString(R.string.cheats_error_on_line, validityResult)
binding.scrollView.smoothScrollTo(0, binding.editCodeInput.bottom)
return
}
val newCheat = Cheat.createGatewayCode(name, notes, code)
if (cheatsViewModel.isAdding.value == true) {
cheatsViewModel.finishAddingCheat(newCheat)
} else {
cheatsViewModel.updateSelectedCheat(newCheat)
}
binding.buttonEdit.requestFocus()
}
private fun onSelectedCheatUpdated(cheat: Cheat?) {
clearEditErrors()
val isEditing: Boolean = cheatsViewModel.isEditing.value == true
// If the fragment was recreated while editing a cheat, it's vital that we
// don't repopulate the fields, otherwise the user's changes will be lost
if (!isEditing) {
if (cheat == null) {
binding.editNameInput.setText("")
binding.editNotesInput.setText("")
binding.editCodeInput.setText("")
} else {
binding.editNameInput.setText(cheat.getName())
binding.editNotesInput.setText(cheat.getNotes())
binding.editCodeInput.setText(cheat.getCode())
}
}
}
private fun onIsEditingUpdated(isEditing: Boolean) {
if (isEditing) {
binding.root.visibility = View.VISIBLE
}
binding.editNameInput.isEnabled = isEditing
binding.editNotesInput.isEnabled = isEditing
binding.editCodeInput.isEnabled = isEditing
binding.buttonDelete.visibility = if (isEditing) View.GONE else View.VISIBLE
binding.buttonEdit.visibility = if (isEditing) View.GONE else View.VISIBLE
binding.buttonCancel.visibility = if (isEditing) View.VISIBLE else View.GONE
binding.buttonOk.visibility = if (isEditing) View.VISIBLE else View.GONE
}
private fun setInsets() =
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _: View?, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right
val mlpAppBar = binding.toolbarCheatDetails.layoutParams as ViewGroup.MarginLayoutParams
mlpAppBar.leftMargin = leftInsets
mlpAppBar.rightMargin = rightInsets
binding.toolbarCheatDetails.layoutParams = mlpAppBar
binding.scrollView.updatePadding(left = leftInsets, right = rightInsets)
binding.buttonContainer.updatePadding(left = leftInsets, right = rightInsets)
windowInsets
}
}

View File

@ -1,71 +0,0 @@
package org.citra.citra_emu.features.cheats.ui;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
import org.citra.citra_emu.ui.DividerItemDecoration;
public class CheatListFragment extends Fragment {
private RecyclerView mRecyclerView;
private FloatingActionButton mFab;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_cheat_list, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
mRecyclerView = view.findViewById(R.id.cheat_list);
mFab = view.findViewById(R.id.fab);
CheatsActivity activity = (CheatsActivity) requireActivity();
CheatsViewModel viewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
mRecyclerView.setAdapter(new CheatsAdapter(activity, viewModel));
mRecyclerView.setLayoutManager(new LinearLayoutManager(activity));
mRecyclerView.addItemDecoration(new DividerItemDecoration(activity, null));
mFab.setOnClickListener(v -> {
viewModel.startAddingCheat();
viewModel.openDetailsView();
});
setInsets();
}
private void setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(mRecyclerView, (v, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(0, 0, 0, insets.bottom + getResources().getDimensionPixelSize(R.dimen.spacing_fab_list));
ViewGroup.MarginLayoutParams mlpFab =
(ViewGroup.MarginLayoutParams) mFab.getLayoutParams();
int fabPadding = getResources().getDimensionPixelSize(R.dimen.spacing_large);
mlpFab.leftMargin = insets.left + fabPadding;
mlpFab.bottomMargin = insets.bottom + fabPadding;
mlpFab.rightMargin = insets.right + fabPadding;
mFab.setLayoutParams(mlpFab);
return windowInsets;
});
}
}

View File

@ -0,0 +1,143 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.cheats.ui
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.divider.MaterialDividerItemDecoration
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.citra.citra_emu.R
import org.citra.citra_emu.databinding.FragmentCheatListBinding
import org.citra.citra_emu.features.cheats.model.CheatsViewModel
import org.citra.citra_emu.ui.main.MainActivity
class CheatListFragment : Fragment() {
private var _binding: FragmentCheatListBinding? = null
private val binding get() = _binding!!
private val cheatsViewModel: CheatsViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCheatListBinding.inflate(layoutInflater)
return binding.root
}
// This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.cheatList.adapter = CheatsAdapter(requireActivity(), cheatsViewModel)
binding.cheatList.layoutManager = LinearLayoutManager(requireContext())
binding.cheatList.addItemDecoration(
MaterialDividerItemDecoration(
requireContext(),
MaterialDividerItemDecoration.VERTICAL
)
)
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
cheatsViewModel.cheatAddedEvent.collect { position: Int? ->
position?.let {
binding.cheatList.apply {
post { (adapter as CheatsAdapter).notifyItemInserted(it) }
}
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
cheatsViewModel.cheatChangedEvent.collect { position: Int? ->
position?.let {
binding.cheatList.apply {
post { (adapter as CheatsAdapter).notifyItemChanged(it) }
}
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
cheatsViewModel.cheatDeletedEvent.collect { position: Int? ->
position?.let {
binding.cheatList.apply {
post { (adapter as CheatsAdapter).notifyItemRemoved(it) }
}
}
}
}
}
}
binding.fab.setOnClickListener {
cheatsViewModel.startAddingCheat()
cheatsViewModel.openDetailsView()
}
binding.toolbarCheatList.setNavigationOnClickListener {
if (requireActivity() is MainActivity) {
view.findNavController().popBackStack()
} else {
requireActivity().finish()
}
}
setInsets()
}
private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(
binding.root
) { _: View, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val cutoutInsets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
val leftInsets = barInsets.left + cutoutInsets.left
val rightInsets = barInsets.right + cutoutInsets.right
val mlpAppBar = binding.toolbarCheatList.layoutParams as MarginLayoutParams
mlpAppBar.leftMargin = leftInsets
mlpAppBar.rightMargin = rightInsets
binding.toolbarCheatList.layoutParams = mlpAppBar
binding.cheatList.updatePadding(
left = leftInsets,
right = rightInsets,
bottom = barInsets.bottom +
resources.getDimensionPixelSize(R.dimen.spacing_fab_list)
)
val mlpFab = binding.fab.layoutParams as MarginLayoutParams
val fabPadding = resources.getDimensionPixelSize(R.dimen.spacing_large)
mlpFab.leftMargin = leftInsets + fabPadding
mlpFab.bottomMargin = barInsets.bottom + fabPadding
mlpFab.rightMargin = rightInsets + fabPadding
binding.fab.layoutParams = mlpFab
windowInsets
}
}
}

View File

@ -1,56 +0,0 @@
package org.citra.citra_emu.features.cheats.ui;
import android.view.View;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.RecyclerView;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.cheats.model.Cheat;
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
public class CheatViewHolder extends RecyclerView.ViewHolder
implements View.OnClickListener, CompoundButton.OnCheckedChangeListener {
private final View mRoot;
private final TextView mName;
private final CheckBox mCheckbox;
private CheatsViewModel mViewModel;
private Cheat mCheat;
private int mPosition;
public CheatViewHolder(@NonNull View itemView) {
super(itemView);
mRoot = itemView.findViewById(R.id.root);
mName = itemView.findViewById(R.id.text_name);
mCheckbox = itemView.findViewById(R.id.checkbox);
}
public void bind(CheatsActivity activity, Cheat cheat, int position) {
mCheckbox.setOnCheckedChangeListener(null);
mViewModel = new ViewModelProvider(activity).get(CheatsViewModel.class);
mCheat = cheat;
mPosition = position;
mName.setText(mCheat.getName());
mCheckbox.setChecked(mCheat.getEnabled());
mRoot.setOnClickListener(this);
mCheckbox.setOnCheckedChangeListener(this);
}
public void onClick(View root) {
mViewModel.setSelectedCheat(mCheat, mPosition);
mViewModel.openDetailsView();
}
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
mCheat.setEnabled(isChecked);
}
}

View File

@ -1,235 +0,0 @@
package org.citra.citra_emu.features.cheats.ui;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsAnimationCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.slidingpanelayout.widget.SlidingPaneLayout;
import com.google.android.material.appbar.AppBarLayout;
import com.google.android.material.appbar.MaterialToolbar;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.cheats.model.Cheat;
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
import org.citra.citra_emu.ui.TwoPaneOnBackPressedCallback;
import org.citra.citra_emu.utils.InsetsHelper;
import org.citra.citra_emu.utils.ThemeUtil;
import java.util.List;
public class CheatsActivity extends AppCompatActivity
implements SlidingPaneLayout.PanelSlideListener {
private static String ARG_TITLE_ID = "title_id";
private CheatsViewModel mViewModel;
private SlidingPaneLayout mSlidingPaneLayout;
private View mCheatList;
private View mCheatDetails;
private View mCheatListLastFocus;
private View mCheatDetailsLastFocus;
public static void launch(Context context, long titleId) {
Intent intent = new Intent(context, CheatsActivity.class);
intent.putExtra(ARG_TITLE_ID, titleId);
context.startActivity(intent);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
ThemeUtil.INSTANCE.setTheme(this);
super.onCreate(savedInstanceState);
WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
long titleId = getIntent().getLongExtra(ARG_TITLE_ID, -1);
mViewModel = new ViewModelProvider(this).get(CheatsViewModel.class);
mViewModel.initialize(titleId);
setContentView(R.layout.activity_cheats);
mSlidingPaneLayout = findViewById(R.id.sliding_pane_layout);
mCheatList = findViewById(R.id.cheat_list_container);
mCheatDetails = findViewById(R.id.cheat_details_container);
mCheatListLastFocus = mCheatList;
mCheatDetailsLastFocus = mCheatDetails;
mSlidingPaneLayout.addPanelSlideListener(this);
getOnBackPressedDispatcher().addCallback(this,
new TwoPaneOnBackPressedCallback(mSlidingPaneLayout));
mViewModel.getSelectedCheat().observe(this, this::onSelectedCheatChanged);
mViewModel.getIsEditing().observe(this, this::onIsEditingChanged);
onSelectedCheatChanged(mViewModel.getSelectedCheat().getValue());
mViewModel.getOpenDetailsViewEvent().observe(this, this::openDetailsView);
// Show "Up" button in the action bar for navigation
MaterialToolbar toolbar = findViewById(R.id.toolbar_cheats);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setInsets();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_settings, menu);
return true;
}
@Override
protected void onStop() {
super.onStop();
mViewModel.saveIfNeeded();
}
@Override
public void onPanelSlide(@NonNull View panel, float slideOffset) {
}
@Override
public void onPanelOpened(@NonNull View panel) {
boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
mCheatDetailsLastFocus.requestFocus(rtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
}
@Override
public void onPanelClosed(@NonNull View panel) {
boolean rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL;
mCheatListLastFocus.requestFocus(rtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
}
private void onIsEditingChanged(boolean isEditing) {
if (isEditing) {
mSlidingPaneLayout.setLockMode(SlidingPaneLayout.LOCK_MODE_UNLOCKED);
}
}
private void onSelectedCheatChanged(Cheat selectedCheat) {
boolean cheatSelected = selectedCheat != null || mViewModel.getIsEditing().getValue();
if (!cheatSelected && mSlidingPaneLayout.isOpen()) {
mSlidingPaneLayout.close();
}
mSlidingPaneLayout.setLockMode(cheatSelected ?
SlidingPaneLayout.LOCK_MODE_UNLOCKED : SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED);
}
public void onListViewFocusChange(boolean hasFocus) {
if (hasFocus) {
mCheatListLastFocus = mCheatList.findFocus();
if (mCheatListLastFocus == null)
throw new NullPointerException();
mSlidingPaneLayout.close();
}
}
public void onDetailsViewFocusChange(boolean hasFocus) {
if (hasFocus) {
mCheatDetailsLastFocus = mCheatDetails.findFocus();
if (mCheatDetailsLastFocus == null)
throw new NullPointerException();
mSlidingPaneLayout.open();
}
}
@Override
public boolean onSupportNavigateUp() {
onBackPressed();
return true;
}
private void openDetailsView(boolean open) {
if (open) {
mSlidingPaneLayout.open();
}
}
public static void setOnFocusChangeListenerRecursively(@NonNull View view, View.OnFocusChangeListener listener) {
view.setOnFocusChangeListener(listener);
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
for (int i = 0; i < viewGroup.getChildCount(); i++) {
View child = viewGroup.getChildAt(i);
setOnFocusChangeListenerRecursively(child, listener);
}
}
}
private void setInsets() {
AppBarLayout appBarLayout = findViewById(R.id.appbar_cheats);
ViewCompat.setOnApplyWindowInsetsListener(mSlidingPaneLayout, (v, windowInsets) -> {
Insets barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
Insets keyboardInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime());
InsetsHelper.insetAppBar(barInsets, appBarLayout);
mSlidingPaneLayout.setPadding(barInsets.left, 0, barInsets.right, 0);
// Set keyboard insets if the system supports smooth keyboard animations
ViewGroup.MarginLayoutParams mlpDetails =
(ViewGroup.MarginLayoutParams) mCheatDetails.getLayoutParams();
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.R) {
if (keyboardInsets.bottom > 0) {
mlpDetails.bottomMargin = keyboardInsets.bottom;
} else {
mlpDetails.bottomMargin = barInsets.bottom;
}
} else {
if (mlpDetails.bottomMargin == 0) {
mlpDetails.bottomMargin = barInsets.bottom;
}
}
mCheatDetails.setLayoutParams(mlpDetails);
return windowInsets;
});
// Update the layout for every frame that the keyboard animates in
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
ViewCompat.setWindowInsetsAnimationCallback(mCheatDetails,
new WindowInsetsAnimationCompat.Callback(
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP) {
int keyboardInsets = 0;
int barInsets = 0;
@NonNull
@Override
public WindowInsetsCompat onProgress(@NonNull WindowInsetsCompat insets,
@NonNull List<WindowInsetsAnimationCompat> runningAnimations) {
ViewGroup.MarginLayoutParams mlpDetails =
(ViewGroup.MarginLayoutParams) mCheatDetails.getLayoutParams();
keyboardInsets = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom;
barInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom;
mlpDetails.bottomMargin = Math.max(keyboardInsets, barInsets);
mCheatDetails.setLayoutParams(mlpDetails);
return insets;
}
});
}
}
}

View File

@ -0,0 +1,63 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.cheats.ui
import android.os.Bundle
import android.view.View
import android.view.View.OnFocusChangeListener
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.WindowCompat
import androidx.navigation.fragment.NavHostFragment
import com.google.android.material.color.MaterialColors
import org.citra.citra_emu.R
import org.citra.citra_emu.databinding.ActivityCheatsBinding
import org.citra.citra_emu.utils.InsetsHelper
import org.citra.citra_emu.utils.ThemeUtil
class CheatsActivity : AppCompatActivity() {
private lateinit var binding: ActivityCheatsBinding
override fun onCreate(savedInstanceState: Bundle?) {
ThemeUtil.setTheme(this)
super.onCreate(savedInstanceState)
binding = ActivityCheatsBinding.inflate(layoutInflater)
setContentView(binding.root)
WindowCompat.setDecorFitsSystemWindows(window, false)
if (InsetsHelper.getSystemGestureType(applicationContext) !=
InsetsHelper.GESTURE_NAVIGATION
) {
binding.navigationBarShade.setBackgroundColor(
ThemeUtil.getColorWithOpacity(
MaterialColors.getColor(
binding.navigationBarShade,
com.google.android.material.R.attr.colorSurface
),
ThemeUtil.SYSTEM_BAR_ALPHA
)
)
}
val navHostFragment =
supportFragmentManager.findFragmentById(R.id.fragment_container) as NavHostFragment
val navController = navHostFragment.navController
navController.setGraph(R.navigation.cheats_navigation, intent.extras)
}
companion object {
fun setOnFocusChangeListenerRecursively(view: View, listener: OnFocusChangeListener?) {
view.onFocusChangeListener = listener
if (view is ViewGroup) {
for (i in 0 until view.childCount) {
val child = view.getChildAt(i)
setOnFocusChangeListenerRecursively(child, listener)
}
}
}
}
}

View File

@ -1,72 +0,0 @@
package org.citra.citra_emu.features.cheats.ui;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.citra.citra_emu.R;
import org.citra.citra_emu.features.cheats.model.Cheat;
import org.citra.citra_emu.features.cheats.model.CheatsViewModel;
public class CheatsAdapter extends RecyclerView.Adapter<CheatViewHolder> {
private final CheatsActivity mActivity;
private final CheatsViewModel mViewModel;
public CheatsAdapter(CheatsActivity activity, CheatsViewModel viewModel) {
mActivity = activity;
mViewModel = viewModel;
mViewModel.getCheatAddedEvent().observe(activity, (position) -> {
if (position != null) {
notifyItemInserted(position);
}
});
mViewModel.getCheatUpdatedEvent().observe(activity, (position) -> {
if (position != null) {
notifyItemChanged(position);
}
});
mViewModel.getCheatDeletedEvent().observe(activity, (position) -> {
if (position != null) {
notifyItemRemoved(position);
}
});
}
@NonNull
@Override
public CheatViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View cheatView = inflater.inflate(R.layout.list_item_cheat, parent, false);
addViewListeners(cheatView);
return new CheatViewHolder(cheatView);
}
@Override
public void onBindViewHolder(@NonNull CheatViewHolder holder, int position) {
holder.bind(mActivity, getItemAt(position), position);
}
@Override
public int getItemCount() {
return mViewModel.getCheats().length;
}
private void addViewListeners(View view) {
// On a portrait phone screen (or other narrow screen), only one of the two panes are shown
// at the same time. If the user is navigating using a d-pad and moves focus to an element
// in the currently hidden pane, we need to manually show that pane.
CheatsActivity.setOnFocusChangeListenerRecursively(view,
(v, hasFocus) -> mActivity.onListViewFocusChange(hasFocus));
}
private Cheat getItemAt(int position) {
return mViewModel.getCheats()[position];
}
}

View File

@ -0,0 +1,69 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.cheats.ui
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CompoundButton
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.RecyclerView
import org.citra.citra_emu.databinding.ListItemCheatBinding
import org.citra.citra_emu.features.cheats.model.Cheat
import org.citra.citra_emu.features.cheats.model.CheatsViewModel
class CheatsAdapter(
private val activity: FragmentActivity,
private val viewModel: CheatsViewModel
) : RecyclerView.Adapter<CheatsAdapter.CheatViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CheatViewHolder {
val binding =
ListItemCheatBinding.inflate(LayoutInflater.from(parent.context), parent, false)
addViewListeners(binding.root)
return CheatViewHolder(binding)
}
override fun onBindViewHolder(holder: CheatViewHolder, position: Int) =
holder.bind(activity, viewModel.cheats[position], position)
override fun getItemCount(): Int = viewModel.cheats.size
private fun addViewListeners(view: View) {
// On a portrait phone screen (or other narrow screen), only one of the two panes are shown
// at the same time. If the user is navigating using a d-pad and moves focus to an element
// in the currently hidden pane, we need to manually show that pane.
CheatsActivity.setOnFocusChangeListenerRecursively(view) { _, hasFocus ->
viewModel.onListViewFocusChanged(hasFocus)
}
}
inner class CheatViewHolder(private val binding: ListItemCheatBinding) :
RecyclerView.ViewHolder(binding.root), View.OnClickListener,
CompoundButton.OnCheckedChangeListener {
private lateinit var viewModel: CheatsViewModel
private lateinit var cheat: Cheat
private var position = 0
fun bind(activity: FragmentActivity, cheat: Cheat, position: Int) {
viewModel = ViewModelProvider(activity)[CheatsViewModel::class.java]
this.cheat = cheat
this.position = position
binding.textName.text = this.cheat.getName()
binding.cheatSwitch.isChecked = this.cheat.getEnabled()
binding.cheatContainer.setOnClickListener(this)
binding.cheatSwitch.setOnCheckedChangeListener(this)
}
override fun onClick(root: View) {
viewModel.setSelectedCheat(cheat, position)
viewModel.openDetailsView()
}
override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) {
cheat.setEnabled(isChecked)
}
}
}

View File

@ -0,0 +1,244 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.cheats.ui
import android.annotation.SuppressLint
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.OnBackPressedCallback
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat
import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import androidx.slidingpanelayout.widget.SlidingPaneLayout
import com.google.android.material.transition.MaterialSharedAxis
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch
import org.citra.citra_emu.databinding.FragmentCheatsBinding
import org.citra.citra_emu.features.cheats.model.Cheat
import org.citra.citra_emu.features.cheats.model.CheatsViewModel
import org.citra.citra_emu.ui.TwoPaneOnBackPressedCallback
import org.citra.citra_emu.ui.main.MainActivity
import org.citra.citra_emu.viewmodel.HomeViewModel
class CheatsFragment : Fragment(), SlidingPaneLayout.PanelSlideListener {
private var cheatListLastFocus: View? = null
private var cheatDetailsLastFocus: View? = null
private var _binding: FragmentCheatsBinding? = null
private val binding get() = _binding!!
private val cheatsViewModel: CheatsViewModel by activityViewModels()
private val homeViewModel: HomeViewModel by activityViewModels()
private val args by navArgs<CheatsFragmentArgs>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentCheatsBinding.inflate(inflater)
return binding.root
}
// This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = false, animated = true)
homeViewModel.setStatusBarShadeVisibility(visible = false)
cheatsViewModel.initialize(args.titleId)
cheatListLastFocus = binding.cheatListContainer
cheatDetailsLastFocus = binding.cheatDetailsContainer
binding.slidingPaneLayout.addPanelSlideListener(this)
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
TwoPaneOnBackPressedCallback(binding.slidingPaneLayout)
)
requireActivity().onBackPressedDispatcher.addCallback(
viewLifecycleOwner,
object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (binding.slidingPaneLayout.isOpen) {
binding.slidingPaneLayout.close()
} else {
if (requireActivity() is MainActivity) {
view.findNavController().popBackStack()
} else {
requireActivity().finish()
}
}
}
}
)
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
cheatsViewModel.selectedCheat.collect { onSelectedCheatChanged(it) }
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
cheatsViewModel.isEditing.collect { onIsEditingChanged(it) }
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
cheatsViewModel.openDetailsViewEvent.collect { openDetailsView(it) }
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
cheatsViewModel.closeDetailsViewEvent.collect { closeDetailsView(it) }
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
cheatsViewModel.listViewFocusChange.collect { onListViewFocusChange(it) }
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
cheatsViewModel.detailsViewFocusChange.collect { onDetailsViewFocusChange(it) }
}
}
}
setInsets()
}
override fun onStop() {
super.onStop()
cheatsViewModel.saveIfNeeded()
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
override fun onPanelSlide(panel: View, slideOffset: Float) {}
override fun onPanelOpened(panel: View) {
val rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL
cheatDetailsLastFocus!!.requestFocus(if (rtl) View.FOCUS_LEFT else View.FOCUS_RIGHT)
}
override fun onPanelClosed(panel: View) {
val rtl = ViewCompat.getLayoutDirection(panel) == ViewCompat.LAYOUT_DIRECTION_RTL
cheatListLastFocus!!.requestFocus(if (rtl) View.FOCUS_RIGHT else View.FOCUS_LEFT)
}
private fun onIsEditingChanged(isEditing: Boolean) {
if (isEditing) {
binding.slidingPaneLayout.lockMode = SlidingPaneLayout.LOCK_MODE_UNLOCKED
}
}
private fun onSelectedCheatChanged(selectedCheat: Cheat?) {
val cheatSelected = selectedCheat != null || cheatsViewModel.isEditing.value!!
if (!cheatSelected && binding.slidingPaneLayout.isOpen) {
binding.slidingPaneLayout.close()
}
binding.slidingPaneLayout.lockMode =
if (cheatSelected) SlidingPaneLayout.LOCK_MODE_UNLOCKED else SlidingPaneLayout.LOCK_MODE_LOCKED_CLOSED
}
fun onListViewFocusChange(hasFocus: Boolean) {
if (hasFocus) {
cheatListLastFocus = binding.cheatListContainer.findFocus()
if (cheatListLastFocus == null) throw NullPointerException()
binding.slidingPaneLayout.close()
}
}
fun onDetailsViewFocusChange(hasFocus: Boolean) {
if (hasFocus) {
cheatDetailsLastFocus = binding.cheatDetailsContainer.findFocus()
if (cheatDetailsLastFocus == null) {
throw NullPointerException()
}
binding.slidingPaneLayout.open()
}
}
private fun openDetailsView(open: Boolean) {
if (open) {
binding.slidingPaneLayout.open()
}
}
private fun closeDetailsView(close: Boolean) {
if (close) {
binding.slidingPaneLayout.close()
}
}
private fun setInsets() {
ViewCompat.setOnApplyWindowInsetsListener(
binding.slidingPaneLayout
) { _: View?, windowInsets: WindowInsetsCompat ->
val barInsets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
val keyboardInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
// Set keyboard insets if the system supports smooth keyboard animations
val mlpDetails = binding.cheatDetailsContainer.layoutParams as ViewGroup.MarginLayoutParams
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
if (keyboardInsets.bottom > 0) {
mlpDetails.bottomMargin = keyboardInsets.bottom
} else {
mlpDetails.bottomMargin = barInsets.bottom
}
} else {
if (mlpDetails.bottomMargin == 0) {
mlpDetails.bottomMargin = barInsets.bottom
}
}
binding.cheatDetailsContainer.layoutParams = mlpDetails
windowInsets
}
// Update the layout for every frame that the keyboard animates in
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
ViewCompat.setWindowInsetsAnimationCallback(
binding.cheatDetailsContainer,
object : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) {
var keyboardInsets = 0
var barInsets = 0
override fun onProgress(
insets: WindowInsetsCompat,
runningAnimations: List<WindowInsetsAnimationCompat>
): WindowInsetsCompat {
val mlpDetails =
binding.cheatDetailsContainer.layoutParams as ViewGroup.MarginLayoutParams
keyboardInsets = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom
barInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom
mlpDetails.bottomMargin = keyboardInsets.coerceAtLeast(barInsets)
binding.cheatDetailsContainer.layoutParams = mlpDetails
return insets
}
})
}
}
}

View File

@ -0,0 +1,12 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.hotkeys
enum class Hotkey(val button: Int) {
SWAP_SCREEN(10001),
CYCLE_LAYOUT(10002),
CLOSE_GAME(10003),
PAUSE_OR_RESUME(10004);
}

View File

@ -0,0 +1,27 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.features.hotkeys
import org.citra.citra_emu.utils.EmulationLifecycleUtil
import org.citra.citra_emu.display.ScreenAdjustmentUtil
class HotkeyUtility(private val screenAdjustmentUtil: ScreenAdjustmentUtil) {
val hotkeyButtons = Hotkey.entries.map { it.button }
fun handleHotkey(bindedButton: Int): Boolean {
if(hotkeyButtons.contains(bindedButton)) {
when (bindedButton) {
Hotkey.SWAP_SCREEN.button -> screenAdjustmentUtil.swapScreen()
Hotkey.CYCLE_LAYOUT.button -> screenAdjustmentUtil.cycleLayouts()
Hotkey.CLOSE_GAME.button -> EmulationLifecycleUtil.closeGame()
Hotkey.PAUSE_OR_RESUME.button -> EmulationLifecycleUtil.pauseOrResume()
else -> {}
}
return true
}
return false
}
}

View File

@ -2,9 +2,7 @@
// 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
package org.citra.citra_emu.features.settings.model
interface AbstractShortSetting : AbstractSetting {
var short: Short

View File

@ -12,7 +12,8 @@ enum class BooleanSetting(
SPIRV_SHADER_GEN("spirv_shader_gen", Settings.SECTION_RENDERER, true),
ASYNC_SHADERS("async_shader_compilation", Settings.SECTION_RENDERER, false),
PLUGIN_LOADER("plugin_loader", Settings.SECTION_SYSTEM, false),
ALLOW_PLUGIN_LOADER("allow_plugin_loader", Settings.SECTION_SYSTEM, true);
ALLOW_PLUGIN_LOADER("allow_plugin_loader", Settings.SECTION_SYSTEM, true),
SWAP_SCREEN("swap_screen", Settings.SECTION_LAYOUT, false);
override var boolean: Boolean = defaultValue

View File

@ -22,8 +22,10 @@ enum class IntSetting(
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),
AUDIO_INPUT_TYPE("output_type", Settings.SECTION_AUDIO, 0),
NEW_3DS("is_new_3ds", Settings.SECTION_SYSTEM, 1),
LLE_APPLETS("lle_applets", Settings.SECTION_SYSTEM, 0),
CPU_CLOCK_SPEED("cpu_clock_percentage", Settings.SECTION_CORE, 100),
LINEAR_FILTERING("filter_mode", Settings.SECTION_RENDERER, 1),
SHADERS_ACCURATE_MUL("shaders_accurate_mul", Settings.SECTION_RENDERER, 0),
@ -60,6 +62,7 @@ enum class IntSetting(
EMULATED_REGION,
INIT_CLOCK,
NEW_3DS,
LLE_APPLETS,
GRAPHICS_API,
VSYNC,
DEBUG_RENDERER,

View File

@ -94,6 +94,10 @@ class Settings {
}
}
fun saveSetting(setting: AbstractSetting, filename: String) {
SettingsFile.saveFile(filename, setting)
}
companion object {
const val SECTION_CORE = "Core"
const val SECTION_SYSTEM = "System"
@ -128,6 +132,11 @@ class Settings {
const val KEY_DPAD_AXIS_VERTICAL = "dpad_axis_vertical"
const val KEY_DPAD_AXIS_HORIZONTAL = "dpad_axis_horizontal"
const val HOTKEY_SCREEN_SWAP = "hotkey_screen_swap"
const val HOTKEY_CYCLE_LAYOUT = "hotkey_toggle_layout"
const val HOTKEY_CLOSE_GAME = "hotkey_close_game"
const val HOTKEY_PAUSE_OR_RESUME = "hotkey_pause_or_resume_game"
val buttonKeys = listOf(
KEY_BUTTON_A,
KEY_BUTTON_B,
@ -174,6 +183,18 @@ class Settings {
R.string.button_zl,
R.string.button_zr
)
val hotKeys = listOf(
HOTKEY_SCREEN_SWAP,
HOTKEY_CYCLE_LAYOUT,
HOTKEY_CLOSE_GAME,
HOTKEY_PAUSE_OR_RESUME
)
val hotkeyTitles = listOf(
R.string.emulation_swap_screens,
R.string.emulation_cycle_landscape_layouts,
R.string.emulation_close_game,
R.string.emulation_toggle_pause
)
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
const val PREF_MATERIAL_YOU = "MaterialYouTheme"

View File

@ -6,14 +6,15 @@ package org.citra.citra_emu.features.settings.model.view
import android.content.Context
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import android.view.InputDevice
import android.view.InputDevice.MotionRange
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.features.hotkeys.Hotkey
import org.citra.citra_emu.features.settings.model.AbstractSetting
import org.citra.citra_emu.features.settings.model.Settings
@ -127,6 +128,11 @@ 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_SCREEN_SWAP -> Hotkey.SWAP_SCREEN.button
Settings.HOTKEY_CYCLE_LAYOUT -> Hotkey.CYCLE_LAYOUT.button
Settings.HOTKEY_CLOSE_GAME -> Hotkey.CLOSE_GAME.button
Settings.HOTKEY_PAUSE_OR_RESUME -> Hotkey.PAUSE_OR_RESUME.button
else -> -1
}

View File

@ -6,6 +6,7 @@ package org.citra.citra_emu.features.settings.model.view
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.AbstractShortSetting
class SingleChoiceSetting(
setting: AbstractSetting?,

View File

@ -5,6 +5,7 @@
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.AbstractShortSetting
import org.citra.citra_emu.features.settings.model.AbstractStringSetting
class StringSingleChoiceSetting(

View File

@ -224,7 +224,7 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView {
setUsername("CITRA")
setBirthday(3, 25)
setSystemLanguage(1)
setSoundOutputMode(2)
setSoundOutputMode(1)
setCountryCode(49)
setPlayCoins(42)
}

View File

@ -37,7 +37,7 @@ 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.ScaledFloatSetting
import org.citra.citra_emu.features.settings.model.view.AbstractShortSetting
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

View File

@ -23,7 +23,7 @@ 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.Settings
import org.citra.citra_emu.features.settings.model.StringSetting
import org.citra.citra_emu.features.settings.model.view.AbstractShortSetting
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.HeaderSetting
import org.citra.citra_emu.features.settings.model.view.InputBindingSetting
@ -38,8 +38,8 @@ import org.citra.citra_emu.features.settings.model.view.SwitchSetting
import org.citra.citra_emu.features.settings.utils.SettingsFile
import org.citra.citra_emu.fragments.ResetSettingsDialogFragment
import org.citra.citra_emu.utils.BirthdayMonth
import org.citra.citra_emu.utils.SystemSaveGame
import org.citra.citra_emu.utils.Log
import org.citra.citra_emu.utils.SystemSaveGame
import org.citra.citra_emu.utils.ThemeUtil
class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView) {
@ -620,6 +620,12 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
val button = getInputObject(key)
add(InputBindingSetting(button, Settings.triggerTitles[i]))
}
add(HeaderSetting(R.string.controller_hotkeys))
Settings.hotKeys.forEachIndexed { i: Int, key: String ->
val button = getInputObject(key)
add(InputBindingSetting(button, Settings.hotkeyTitles[i]))
}
}
}
@ -681,8 +687,8 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
1,
10,
"x",
IntSetting.GRAPHICS_API.key,
IntSetting.GRAPHICS_API.defaultValue.toFloat()
IntSetting.RESOLUTION_FACTOR.key,
IntSetting.RESOLUTION_FACTOR.defaultValue.toFloat()
)
)
add(
@ -874,7 +880,7 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
override val section = null
override val isRuntimeEditable = false
override val valueAsString = int.toString()
override val defaultValue = 2
override val defaultValue = 1
}
add(
SingleChoiceSetting(
@ -901,6 +907,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
IntSetting.NEW_3DS.defaultValue
)
)
add(
SwitchSetting(
IntSetting.LLE_APPLETS,
R.string.lle_applets,
0,
IntSetting.LLE_APPLETS.key,
IntSetting.LLE_APPLETS.defaultValue
)
)
add(
SliderSetting(
IntSetting.CPU_CLOCK_SPEED,

View File

@ -8,7 +8,6 @@ import android.content.Context
import android.net.Uri
import androidx.documentfile.provider.DocumentFile
import org.citra.citra_emu.CitraApplication
import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.R
import org.citra.citra_emu.features.settings.model.AbstractSetting
import org.citra.citra_emu.features.settings.model.BooleanSetting
@ -23,9 +22,11 @@ import org.citra.citra_emu.utils.BiMap
import org.citra.citra_emu.utils.DirectoryInitialization.userDirectory
import org.citra.citra_emu.utils.Log
import org.ini4j.Wini
import java.io.*
import java.lang.NumberFormatException
import java.util.*
import java.io.BufferedReader
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStreamReader
import java.util.TreeMap
/**
@ -146,6 +147,26 @@ object SettingsFile {
}
}
fun saveFile(
fileName: String,
setting: AbstractSetting
) {
val ini = getSettingsFile(fileName)
try {
val context: Context = CitraApplication.appContext
val inputStream = context.contentResolver.openInputStream(ini.uri)
val writer = Wini(inputStream)
writer.put(setting.section, setting.key, setting.valueAsString)
inputStream!!.close()
val outputStream = context.contentResolver.openOutputStream(ini.uri, "wt")
writer.store(outputStream)
outputStream!!.flush()
outputStream.close()
} catch (e: Exception) {
Log.error("[SettingsFile] File not found: $fileName.ini: ${e.message}")
}
}
private fun mapSectionNameFromIni(generalSectionName: String): String? {
return if (sectionsMap.getForward(generalSectionName) != null) {
sectionsMap.getForward(generalSectionName)

View File

@ -15,7 +15,6 @@ import android.os.Looper
import android.os.SystemClock
import android.view.Choreographer
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.MotionEvent
import android.view.Surface
import android.view.SurfaceHolder
@ -33,6 +32,7 @@ import androidx.drawerlayout.widget.DrawerLayout
import androidx.drawerlayout.widget.DrawerLayout.DrawerListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
@ -51,6 +51,9 @@ import org.citra.citra_emu.activities.EmulationActivity
import org.citra.citra_emu.databinding.DialogCheckboxBinding
import org.citra.citra_emu.databinding.DialogSliderBinding
import org.citra.citra_emu.databinding.FragmentEmulationBinding
import org.citra.citra_emu.display.ScreenAdjustmentUtil
import org.citra.citra_emu.display.ScreenLayout
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
@ -60,10 +63,10 @@ import org.citra.citra_emu.utils.EmulationMenuSettings
import org.citra.citra_emu.utils.FileUtil
import org.citra.citra_emu.utils.GameHelper
import org.citra.citra_emu.utils.GameIconUtils
import org.citra.citra_emu.utils.EmulationLifecycleUtil
import org.citra.citra_emu.utils.Log
import org.citra.citra_emu.utils.ViewUtils
import org.citra.citra_emu.viewmodel.EmulationViewModel
import java.lang.NullPointerException
class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.FrameCallback {
private val preferences: SharedPreferences
@ -80,8 +83,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
private val args by navArgs<EmulationFragmentArgs>()
private lateinit var game: Game
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
private val emulationViewModel: EmulationViewModel by activityViewModels()
private val settingsViewModel: SettingsViewModel by viewModels()
override fun onAttach(context: Context) {
super.onAttach(context)
@ -137,11 +142,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
retainInstance = true
emulationState = EmulationState(game.path)
emulationActivity = requireActivity() as EmulationActivity
screenAdjustmentUtil = ScreenAdjustmentUtil(emulationActivity.windowManager, settingsViewModel.settings)
EmulationLifecycleUtil.addShutdownHook(hook = { emulationState.stop() })
EmulationLifecycleUtil.addPauseResumeHook(hook = { togglePause() })
}
/**
* Initialize the UI and start emulation in here.
*/
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -261,12 +266,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
}
R.id.menu_swap_screens -> {
val isEnabled = !EmulationMenuSettings.swapScreens
EmulationMenuSettings.swapScreens = isEnabled
NativeLibrary.swapScreens(
isEnabled,
requireActivity().windowManager.defaultDisplay.rotation
)
screenAdjustmentUtil.swapScreen()
true
}
@ -318,8 +318,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
.setTitle(R.string.emulation_close_game)
.setMessage(R.string.emulation_close_game_message)
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
emulationState.stop()
requireActivity().finish()
EmulationLifecycleUtil.closeGame()
}
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int ->
NativeLibrary.unPauseEmulation()
@ -413,6 +412,14 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
setInsets()
}
private fun togglePause() {
if(emulationState.isPaused) {
emulationState.unpause()
} else {
emulationState.pause()
}
}
override fun onResume() {
super.onResume()
Choreographer.getInstance().postFrameCallback(this)
@ -669,15 +676,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.menuInflater.inflate(R.menu.menu_landscape_screen_layout, popupMenu.menu)
val layoutOptionMenuItem = when (EmulationMenuSettings.landscapeScreenLayout) {
EmulationMenuSettings.LayoutOption_SingleScreen ->
ScreenLayout.SINGLE_SCREEN.int ->
R.id.menu_screen_layout_single
EmulationMenuSettings.LayoutOption_SideScreen ->
ScreenLayout.SIDE_SCREEN.int ->
R.id.menu_screen_layout_sidebyside
EmulationMenuSettings.LayoutOption_MobilePortrait ->
ScreenLayout.MOBILE_PORTRAIT.int ->
R.id.menu_screen_layout_portrait
ScreenLayout.HYBRID_SCREEN.int ->
R.id.menu_screen_layout_hybrid
else -> R.id.menu_screen_layout_landscape
}
popupMenu.menu.findItem(layoutOptionMenuItem).setChecked(true)
@ -685,22 +695,27 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_screen_layout_landscape -> {
changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobileLandscape, it)
screenAdjustmentUtil.changeScreenOrientation(ScreenLayout.MOBILE_LANDSCAPE)
true
}
R.id.menu_screen_layout_portrait -> {
changeScreenOrientation(EmulationMenuSettings.LayoutOption_MobilePortrait, it)
screenAdjustmentUtil.changeScreenOrientation(ScreenLayout.MOBILE_PORTRAIT)
true
}
R.id.menu_screen_layout_single -> {
changeScreenOrientation(EmulationMenuSettings.LayoutOption_SingleScreen, it)
screenAdjustmentUtil.changeScreenOrientation(ScreenLayout.SINGLE_SCREEN)
true
}
R.id.menu_screen_layout_sidebyside -> {
changeScreenOrientation(EmulationMenuSettings.LayoutOption_SideScreen, it)
screenAdjustmentUtil.changeScreenOrientation(ScreenLayout.SIDE_SCREEN)
true
}
R.id.menu_screen_layout_hybrid -> {
screenAdjustmentUtil.changeScreenOrientation(ScreenLayout.HYBRID_SCREEN)
true
}
@ -711,15 +726,6 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.show()
}
private fun changeScreenOrientation(layoutOption: Int, item: MenuItem) {
item.setChecked(true)
NativeLibrary.notifyOrientationChange(
layoutOption,
requireActivity().windowManager.defaultDisplay.rotation
)
EmulationMenuSettings.landscapeScreenLayout = layoutOption
}
private fun editControlsPlacement() {
if (binding.surfaceInputOverlay.isInEditMode) {
binding.doneControlConfig.visibility = View.GONE

View File

@ -0,0 +1,115 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.fragments
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import android.text.InputFilter
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.citra.citra_emu.R
import org.citra.citra_emu.applets.SoftwareKeyboard
import org.citra.citra_emu.databinding.DialogSoftwareKeyboardBinding
import org.citra.citra_emu.utils.SerializableHelper.serializable
class KeyboardDialogFragment : DialogFragment() {
private lateinit var config: SoftwareKeyboard.KeyboardConfig
private var _binding: DialogSoftwareKeyboardBinding? = null
private val binding get() = _binding!!
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
_binding = DialogSoftwareKeyboardBinding.inflate(layoutInflater)
config = requireArguments().serializable<SoftwareKeyboard.KeyboardConfig>(CONFIG)!!
binding.apply {
editText.hint = config.hintText
editTextInput.isSingleLine = !config.multilineMode
editTextInput.filters =
arrayOf(SoftwareKeyboard.Filter(), InputFilter.LengthFilter(config.maxTextLength))
}
val builder = MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.software_keyboard)
.setView(binding.root)
isCancelable = false
when (config.buttonConfig) {
SoftwareKeyboard.ButtonConfig.Triple -> {
val negativeText =
config.buttonText[0].ifEmpty { getString(android.R.string.cancel) }
val neutralText = config.buttonText[1].ifEmpty { getString(R.string.i_forgot) }
val positiveText = config.buttonText[2].ifEmpty { getString(android.R.string.ok) }
builder.setNegativeButton(negativeText, null)
.setNeutralButton(neutralText, null)
.setPositiveButton(positiveText, null)
}
SoftwareKeyboard.ButtonConfig.Dual -> {
val negativeText =
config.buttonText[0].ifEmpty { getString(android.R.string.cancel) }
val positiveText = config.buttonText[2].ifEmpty { getString(android.R.string.ok) }
builder.setNegativeButton(negativeText, null)
.setPositiveButton(positiveText, null)
}
SoftwareKeyboard.ButtonConfig.Single -> {
val positiveText = config.buttonText[2].ifEmpty { getString(android.R.string.ok) }
builder.setPositiveButton(positiveText, null)
}
}
// This overrides the default alert dialog behavior to prevent dismissing the keyboard
// dialog while we show an error message
val alertDialog = builder.create()
alertDialog.create()
if (alertDialog.getButton(DialogInterface.BUTTON_POSITIVE) != null) {
alertDialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener {
SoftwareKeyboard.data.button = config.buttonConfig
SoftwareKeyboard.data.text = binding.editTextInput.text.toString()
val error = SoftwareKeyboard.ValidateInput(SoftwareKeyboard.data.text)
if (error != SoftwareKeyboard.ValidationError.None) {
SoftwareKeyboard.HandleValidationError(config, error)
return@setOnClickListener
}
dismiss()
synchronized(SoftwareKeyboard.finishLock) { SoftwareKeyboard.finishLock.notifyAll() }
}
}
if (alertDialog.getButton(DialogInterface.BUTTON_NEUTRAL) != null) {
alertDialog.getButton(DialogInterface.BUTTON_NEUTRAL).setOnClickListener {
SoftwareKeyboard.data.button = 1
dismiss()
synchronized(SoftwareKeyboard.finishLock) { SoftwareKeyboard.finishLock.notifyAll() }
}
}
if (alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE) != null) {
alertDialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener {
SoftwareKeyboard.data.button = 0
dismiss()
synchronized(SoftwareKeyboard.finishLock) { SoftwareKeyboard.finishLock.notifyAll() }
}
}
return alertDialog
}
companion object {
const val TAG = "KeyboardDialogFragment"
const val CONFIG = "config"
fun newInstance(config: SoftwareKeyboard.KeyboardConfig): KeyboardDialogFragment {
val frag = KeyboardDialogFragment()
val args = Bundle()
args.putSerializable(CONFIG, config)
frag.arguments = args
return frag
}
}
}

View File

@ -0,0 +1,60 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.fragments
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.citra.citra_emu.R
import org.citra.citra_emu.applets.MiiSelector
import org.citra.citra_emu.utils.SerializableHelper.serializable
class MiiSelectorDialogFragment : DialogFragment() {
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val config = requireArguments().serializable<MiiSelector.MiiSelectorConfig>(CONFIG)!!
// Note: we intentionally leave out the Standard Mii in the native code so that
// the string can get translated
val list = mutableListOf<String>()
list.add(getString(R.string.standard_mii))
list.addAll(config.miiNames)
val initialIndex =
if (config.initiallySelectedMiiIndex < list.size) config.initiallySelectedMiiIndex.toInt() else 0
MiiSelector.data.index = initialIndex
val builder = MaterialAlertDialogBuilder(requireActivity())
.setTitle(if (config.title!!.isEmpty()) getString(R.string.mii_selector) else config.title)
.setSingleChoiceItems(list.toTypedArray(), initialIndex) { _: DialogInterface?, which: Int ->
MiiSelector.data.index = which
}
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
MiiSelector.data.returnCode = 0
synchronized(MiiSelector.finishLock) { MiiSelector.finishLock.notifyAll() }
}
if (config.enableCancelButton) {
builder.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int ->
MiiSelector.data.returnCode = 1
synchronized(MiiSelector.finishLock) { MiiSelector.finishLock.notifyAll() }
}
}
isCancelable = false
return builder.create()
}
companion object {
const val TAG = "MiiSelectorDialogFragment"
const val CONFIG = "config"
fun newInstance(config: MiiSelector.MiiSelectorConfig): MiiSelectorDialogFragment {
val frag = MiiSelectorDialogFragment()
val args = Bundle()
args.putSerializable(CONFIG, config)
frag.arguments = args
return frag
}
}
}

View File

@ -1,36 +0,0 @@
package org.citra.citra_emu.model;
import android.net.Uri;
import android.provider.DocumentsContract;
/**
* A struct that is much more "cheaper" than DocumentFile.
* Only contains the information we needed.
*/
public class CheapDocument {
private final String filename;
private final Uri uri;
private final String mimeType;
public CheapDocument(String filename, String mimeType, Uri uri) {
this.filename = filename;
this.mimeType = mimeType;
this.uri = uri;
}
public String getFilename() {
return filename;
}
public Uri getUri() {
return uri;
}
public String getMimeType() {
return mimeType;
}
public boolean isDirectory() {
return mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR);
}
}

View File

@ -0,0 +1,17 @@
// Copyright 2023 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
package org.citra.citra_emu.model
import android.net.Uri
import android.provider.DocumentsContract
/**
* A struct that is much more "cheaper" than DocumentFile.
* Only contains the information we needed.
*/
class CheapDocument(val filename: String, val mimeType: String, val uri: Uri) {
val isDirectory: Boolean
get() = mimeType == DocumentsContract.Document.MIME_TYPE_DIR
}

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