Compare commits

...

210 Commits

Author SHA1 Message Date
7645aa3839 Android #177 2023-12-31 00:57:21 +00:00
6d509702bf Merge PR 12518 2023-12-31 00:57:21 +00:00
0fb1090e30 Merge PR 12513 2023-12-31 00:57:21 +00:00
93f4696e2a Merge PR 12501 2023-12-31 00:57:21 +00:00
4d2090a9a9 Merge PR 12466 2023-12-31 00:57:21 +00:00
c8f0c34202 Merge PR 12454 2023-12-31 00:57:20 +00:00
09bfc852dc Merge pull request #12509 from liamwhite/ktrace
k_capabilities: simplify KTrace map skip logic
2023-12-30 14:59:09 -05:00
ace74bd066 Merge pull request #12486 from t895/debug-ci
actions: android: Build relWithDebInfo on main repo
2023-12-30 14:59:04 -05:00
f6ee53af14 Merge pull request #12527 from DCNick3/log-more-sdl-errors
Report more SDL errors
2023-12-30 14:58:58 -05:00
6c6cb5745f Merge pull request #12521 from ReillyBrogan/fix-wayland-appid
Fix Wayland appId
2023-12-30 14:58:52 -05:00
3262c0f747 Merge pull request #12520 from t895/settings-tomfoolery
android: Small settings tweaks
2023-12-30 14:58:45 -05:00
9323a1f9b2 yuzu_cmd: Report more SDL errors 2023-12-30 13:32:33 +03:00
f02a8d0ae9 Merge pull request #12479 from GPUCode/linear-bcn
video_core: Fix buffer_row_length for linear compressed textures
2023-12-30 06:56:08 +01:00
8517d7cb44 Merge pull request #12487 from liamwhite/clip3
shader_recompiler: use default value for clip distances array
2023-12-30 06:54:44 +01:00
cb4b4f3d6e Fix Wayland appId
On compliant Wayland compositors windows are matched to their .desktop files by comparing the appId window property to the name of the .desktop file without the .deskop extension.

Qt5/6 by default set this property to the basename of the binary (IE `yuzu`) which does not match the expected value `org.yuzu_emu.yuzu`. We can fix this and fix window associations on compliant compositors (like Plasma) by using the `setDesktopFileName()` function which will set the appId window property. This is a no-op on X11 so is safe to be ran without guards.
2023-12-29 20:32:08 -06:00
21e7f86697 android: Expose anisotropic filtering setting 2023-12-29 17:43:36 -05:00
347b3bd18d android: Center switch setting title when no description is present 2023-12-29 17:28:01 -05:00
755c45777f android: Pair fastmem toggle to CPU Debug toggle
Hides fastmem toggle when CPU Debugging is disabled
2023-12-29 17:27:36 -05:00
d677052e8c actions: android: Build relWithDebInfo on main repo 2023-12-28 20:53:16 -05:00
95bfc542aa k_capabilities: simplify KTrace map skip logic 2023-12-28 01:22:57 -05:00
d0c60605ab shader_recompiler: use default value for clip distances array 2023-12-26 19:32:47 -05:00
6697b665ca shader_recompiler: respect clip distance limits in indexed store 2023-12-26 19:10:25 -05:00
12178c694a Merge pull request #12455 from liamwhite/end-wait
kernel: use simple mutex for object list container
2023-12-26 11:46:19 -05:00
de1e5584b3 Merge pull request #12465 from liamwhite/proper-handle-table
service: fetch objects from the client handle table
2023-12-26 11:46:11 -05:00
1559984f77 Merge pull request #12471 from FearlessTobi/port-7146
Port citra-emu/citra#7146: "assert/logging: Stop the logging thread and flush the backends before crashing"
2023-12-26 11:46:04 -05:00
467ac4fdfe Merge pull request #12472 from FearlessTobi/port-7239
Port citra-emu/citra#7239: "common: Miscellaneous cleanups"
2023-12-26 11:45:57 -05:00
69b7100dac Merge pull request #12449 from liamwhite/debug-utils
renderer_vulkan: skip SetObjectNameEXT on unsupported driver
2023-12-26 11:45:39 -05:00
14dc41d4b3 Merge pull request #12448 from liamwhite/format-assert
renderer_vulkan: demote format assert to error log
2023-12-26 11:45:33 -05:00
ad049f13aa Merge pull request #12415 from ameerj/ogl-draw-auto
gl_rasterizer: Implement DrawTransformFeedback macro
2023-12-26 11:45:25 -05:00
20e0407235 video_core: Fix buffer_row_length computation for linear compressed textures 2023-12-26 16:33:03 +02:00
4f569fd568 assert/logging: Stop the logging thread and flush the backends before crashing
Co-Authored-By: SachinVin <26602104+SachinVin@users.noreply.github.com>
2023-12-26 10:35:14 +01:00
553dac2ae0 ring_buffer: Use feature macro
Co-Authored-By: GPUCode <47210458+GPUCode@users.noreply.github.com>
2023-12-25 14:10:40 +01:00
96abe0d7d3 main: Remove unused enum
Co-Authored-By: GPUCode <47210458+GPUCode@users.noreply.github.com>
2023-12-25 14:10:05 +01:00
47e44a6693 am/jit: reference memory instance from context 2023-12-24 19:36:42 -05:00
cf8c7d4ed3 kernel: remove unecessary process member from handle table 2023-12-24 19:23:03 -05:00
5165ed9efd service: fetch objects from the client handle table 2023-12-24 19:20:43 -05:00
05e3db3ac9 Merge pull request #12394 from liamwhite/per-process-memory
general: properly support multiple memory instances
2023-12-24 16:23:14 +01:00
e3491a9ee8 kernel: use simple mutex for object list container 2023-12-23 16:26:07 -05:00
6a1ddc5028 renderer_vulkan: skip SetObjectNameEXT on unsupported driver 2023-12-23 11:08:02 -05:00
b1d4804c07 renderer_vulkan: demote format assert to error log 2023-12-23 11:04:02 -05:00
c57ae803a6 kernel: fix resource limit imbalance 2023-12-22 21:52:49 -05:00
db7b2bc8f1 kernel: restrict nce to applications 2023-12-22 21:52:49 -05:00
31bf57a310 general: properly support multiple memory instances 2023-12-22 21:52:49 -05:00
cae675343c k_server_session: remove scratch buffer usage in favor of direct copy 2023-12-22 21:52:49 -05:00
35501ba41c k_server_session: process for guest servers 2023-12-22 21:52:49 -05:00
419055e484 kernel: instantiate memory separately for each guest process 2023-12-22 21:52:49 -05:00
91290b9be4 Merge pull request #12412 from ameerj/gl-query-prims
OpenGL: Add GL_PRIMITIVES_GENERATED and GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN queries
2023-12-22 11:42:05 -05:00
820f113d9e Merge pull request #12435 from liamwhite/type-check
shader_recompiler: ensure derivatives for textureGrad are f32
2023-12-22 17:41:13 +01:00
373a1ff2ce Merge pull request #12410 from liamwhite/more-mali-null
renderer_vulkan: don't pass null view when nullDescriptor is not supported
2023-12-22 17:40:47 +01:00
4d6b6ba76c Merge pull request #12432 from liamwhite/float-write
shader_recompiler: use float image operations on load/store when required
2023-12-22 17:40:26 +01:00
4aa713e861 shader_recompiler: ensure derivatives for textureGrad are f32 2023-12-21 19:06:33 -05:00
9e9aed41be shader_recompiler: use float image operations on load/store when required 2023-12-21 14:34:46 -05:00
3d268b8480 Merge pull request #12424 from t895/vsync-per-game-qt
qt: settings: Fix per-game vsync combobox
2023-12-21 10:53:06 -05:00
ad7445d4cc Merge pull request #12425 from german77/temp-fix
service: hid: Fix crash on InitializeVibrationDevice
2023-12-21 10:50:22 -05:00
3a30271219 Merge pull request #12426 from t895/reload-text-fix
android: Fix "No games found" text appearing on load
2023-12-21 10:50:11 -05:00
bb5196aaae qt: settings: Fix per-game vsync combobox 2023-12-21 01:15:05 -05:00
d3070cafa7 android: Fix "No games found" text appearing on load 2023-12-21 00:49:22 -05:00
5cd3b6f58c service: hid: Fix crash on InitializeVibrationDevice 2023-12-20 22:52:36 -06:00
bedc758fe7 Merge pull request #12414 from jbeich/vk274
externals: update Vulkan-Headers to v1.3.274
2023-12-20 12:46:50 -05:00
76701185ad Merge pull request #12400 from ameerj/vk-query-prefix-fix
vk_query_cache: Fix prefix sum max_accumulation_limit logic
2023-12-20 12:46:41 -05:00
f1cb14eb54 Merge pull request #12417 from liamwhite/arm64-gcc-fix
nce: hide shadowing warnings from dynarmic headers
2023-12-20 18:46:08 +01:00
f4f4a469a9 Merge pull request #12409 from liamwhite/bits-and-bytes
nce: fix read size in simd immediate emulation
2023-12-20 18:45:44 +01:00
9e5b4052ed Merge pull request #12403 from liamwhite/clipdistance
shader_recompiler: use minimal clip distance array
2023-12-20 18:45:20 +01:00
234867b84d Merge pull request #12390 from liamwhite/binding-insanity
renderer_vulkan: work around turnip binding bug in a610
2023-12-20 18:44:47 +01:00
61e8c5f798 gl_rasterizer: Less spammy log for unimplemented resets 2023-12-20 11:51:44 -05:00
4b60aec190 nce: hide shadowing warnings from dynarmic headers 2023-12-20 11:07:50 -05:00
bbc0ed118d gl_rasterizer: Implement DrawTransformFeedback macro 2023-12-19 19:54:57 -05:00
ecfba79d98 externals: update Vulkan-Headers to v1.3.274 2023-12-20 01:13:09 +01:00
310834aea2 vulkan_common: unbreak build with Vulkan-Headers 1.3.274
src/video_core/vulkan_common/vulkan_wrapper.cpp:293:13: error: enumeration value 'VK_ERROR_INVALID_VIDEO_STD_PARAMETERS_KHR' not handled in switch [-Werror,-Wswitch]
    switch (result) {
            ^~~~~~
2023-12-20 01:12:41 +01:00
6a1fa9bb17 Merge pull request #12411 from ameerj/gl-nv-tfb-fixups
gl_buffer_cache: Reintroduce NV_vertex_buffer_unified_memory
2023-12-19 18:36:50 -05:00
db8a601cf8 OpenGL: Add GL_PRIMITIVES_GENERATED and GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN queries 2023-12-19 17:32:31 -05:00
1bb76201e6 gl_rasterizer: Silence spammy logs 2023-12-19 17:13:23 -05:00
372bca5945 gl_buffer_cache: Reintroduce NV_vertex_buffer_unified_memory
Workaround Nvidia drivers complaining when a buffer is bound as both a vertex buffer and transform feedback buffer
2023-12-19 17:13:23 -05:00
93c19a40bf nce: increase handler stack size 2023-12-19 15:24:13 -05:00
d0a75580da renderer_vulkan: don't pass null view when nullDescriptor is not supported 2023-12-19 15:13:10 -05:00
345ec25532 Merge pull request #12408 from german77/lang
yuzu: Read/Save category Paths
2023-12-19 14:40:10 -05:00
a94721fde0 nce: fix read size in simd immediate emulation 2023-12-19 12:51:19 -05:00
816c7a8d1f yuzu: Read/Save category Paths 2023-12-19 11:34:53 -06:00
efe52db690 Merge pull request #12382 from liamwhite/image-limit
renderer_vulkan: allow up to 7 swapchain images
2023-12-19 16:15:40 +01:00
d61df0f400 Merge pull request #12387 from liamwhite/oboe
android: add oboe audio sink
2023-12-19 16:15:07 +01:00
b14547b8b6 Merge pull request #12392 from liamwhite/mode
fs: implement OpenDirectoryMode
2023-12-19 16:14:29 +01:00
97ad3e7530 Merge pull request #12391 from yuzu-emu/revert-12344-its-free-real-estate
Revert "video_core: use interval map for page count tracking"
2023-12-19 16:14:09 +01:00
0589a32f75 Merge pull request #12304 from liamwhite/flinger-wtf
nvnflinger: mark buffer as acquired when acquired
2023-12-19 16:12:56 +01:00
617dc0f822 Merge pull request #12402 from german77/lang
yuzu: Make language persistent and remove symbols_path
2023-12-18 23:10:59 -05:00
fcfa8b680b shader_recompiler: use minimal clip distance array 2023-12-18 22:25:14 -05:00
94244437de shader_recompiler: ignore clip distances beyond driver support level 2023-12-18 22:25:14 -05:00
53956a2990 yuzu: Make language persistent and remove symbols_path 2023-12-18 20:28:55 -06:00
a7731abb72 oboe_sink: specify additional required parameters 2023-12-18 17:27:32 -05:00
50fd029eaa Merge pull request #12349 from Kelebek1/return_system_channels_active
Have GetActiveChannelCount return the system channels instead of host device channels
2023-12-18 15:06:16 -05:00
a2b567dfd6 vk_query_cache: Fix prefix sum max_accumulation_limit logic 2023-12-18 12:37:55 -05:00
b770f6a985 fs: implement OpenDirectoryMode 2023-12-18 00:12:38 -05:00
797e8fdbc3 oboe_sink: set low latency performance mode 2023-12-17 21:05:00 -05:00
b8c5027686 Merge pull request #12389 from liamwhite/string-copy
path_util: copy output for GetParentPath
2023-12-17 19:01:04 -05:00
65e646eeba Revert "video_core: use interval map for page count tracking" 2023-12-17 18:59:49 -05:00
fba3fa705d renderer_vulkan: work around turnip binding bug in a610 2023-12-17 15:45:09 -05:00
09e8fb75ce path_util: copy output for GetParentPath 2023-12-17 14:25:42 -05:00
6ca530a721 android: add oboe to audio configuration 2023-12-17 11:44:49 -05:00
e01c535178 oboe_sink: implement channel count querying 2023-12-17 10:10:14 -05:00
7239547ead android: add oboe audio sink 2023-12-17 01:42:59 -05:00
7fc06260d1 renderer_vulkan: allow up to 7 swapchain images 2023-12-16 18:59:44 -05:00
e357896674 Merge pull request #12378 from liamwhite/offsetof
set: add cstddef for offsetof macro
2023-12-16 13:58:13 -05:00
225f4f40cb Merge pull request #12377 from ameerj/tfb-batch-oopsie
gl_buffer_cache: Fix tfb binding typo
2023-12-16 13:58:06 -05:00
927be75616 Merge pull request #12345 from liamwhite/a-flock-of-seagulls
renderer_vulkan: cap async presentation frame count
2023-12-16 13:58:00 -05:00
00965e6c34 Merge pull request #12335 from t895/per-game-settings
android: Game Properties
2023-12-16 13:57:54 -05:00
4bf1f217ae Merge pull request #12331 from liamwhite/layer-confusion
vi: fix confusion between closing and destroying layers
2023-12-16 13:57:42 -05:00
fcc85abe27 nvnflinger: mark buffer as acquired when acquired 2023-12-16 13:40:04 -05:00
6851e93296 audio: skip coefficient normalization for downmix 2023-12-16 13:05:55 -05:00
67660972c9 set: add cstddef for offsetof macro 2023-12-16 12:57:37 -05:00
ffbba74c91 Have GetActiveChannelCount return the system channels instead of host device channels 2023-12-16 12:49:28 -05:00
2b0cf73bf0 gl_buffer_cache: Fix tfb binding typo 2023-12-16 12:48:21 -05:00
a093f3d47a Merge pull request #12184 from Kelebek1/system_settings
Make system settings persistent across boots
2023-12-16 11:47:52 -05:00
4f600f746a Merge pull request #12237 from liamwhite/nce-sigtrap
nce: implement instruction emulation for misaligned memory accesses
2023-12-16 11:47:35 -05:00
360418f1a1 Merge pull request #12290 from liamwhite/deferred-path-split
Improve path splitting speed
2023-12-16 11:47:29 -05:00
3bc7575c47 Merge pull request #12344 from liamwhite/its-free-real-estate
video_core: use interval map for page count tracking
2023-12-16 11:47:21 -05:00
fde8dc1652 Merge pull request #12358 from liamwhite/optimized-alloc
common: use memory holepunching when clearing memory
2023-12-16 11:47:03 -05:00
b8f83aa4bf Merge pull request #12359 from german77/real_shared
service: hid: Allow to create multiple instances of shared memory
2023-12-16 11:46:51 -05:00
85b1e17df6 ssl: fix output byte buffer size issue (#12372) 2023-12-16 17:42:33 +01:00
4144c517a5 Make system settings persistent across boots 2023-12-16 06:01:54 +00:00
8ad5f2c506 common: use memory holepunching when clearing memory 2023-12-14 23:44:33 -05:00
2a3f84aaf2 video_core: lock interval map update 2023-12-14 22:10:21 -05:00
030e6b3980 video_core: use interval map for page count tracking 2023-12-14 21:54:36 -05:00
e8ad603cd9 core: Make sure npad is initialized 2023-12-14 20:04:38 -06:00
b560ade663 renderer_vulkan: bound async presentation queue growth 2023-12-14 15:54:56 -05:00
d10464de30 core: hid: Clean up headers 2023-12-14 09:36:33 -06:00
64f68e9635 service: hid: Allow to create multiple instances of shared memory 2023-12-13 23:24:28 -06:00
462ba1b360 Merge pull request #12354 from liamwhite/mackage-panager
ci: fix homebrew installation issue in actions runner images
2023-12-13 23:15:43 -05:00
4a86a55174 ci: fix homebrew installation issue in actions runner images 2023-12-13 14:03:51 -05:00
86d26914a2 android: Rework InstallableProperty view with icon 2023-12-12 17:25:37 -05:00
6ae4177b25 android: Prevent editing non-savable settings in per-game settings 2023-12-12 17:25:37 -05:00
f6bf8b3ed3 android: Pre-select custom config in game launch dialog 2023-12-12 17:25:37 -05:00
345fb6b226 android: Use confirmation dialog when deleting shader cache 2023-12-12 17:25:37 -05:00
87a9dc9489 android: Always use custom settings when launched from intent 2023-12-12 17:25:37 -05:00
6c6e8b8de0 settings: Allow vsync to be changed per-game 2023-12-12 17:25:37 -05:00
5acffe75df android: Adjust variable name format for native config 2023-12-12 17:25:37 -05:00
ac222ceba2 android: Add game dir entries to FilesystemProvider
Allows us to correctly parse update metadata
2023-12-12 17:25:37 -05:00
f9d4827102 android: Fix games list loading thread safety
Previously we relied on a stateflow for reloading state. Now we use an atomic boolean.
2023-12-12 17:25:37 -05:00
7ea7c72dde android: Collect latest information for games list 2023-12-12 17:25:37 -05:00
809230f634 android: Remove global save import/exporter UI
The original implementation exposed here was fundamentally broken where it would not export or import all of your saves depending on your user profile configuration
2023-12-12 17:25:37 -05:00
698c854d5b android: Compare all properties between games in DiffCallback 2023-12-12 17:25:37 -05:00
ca5b135ddf android: Expose MemoryUtil size formatting function 2023-12-12 17:25:37 -05:00
dbddc627d4 android: Add JNI initialization information for Game class
Unused in this PR, but will be useful later
2023-12-12 17:25:37 -05:00
62fc386bb4 settings: Allow CPU Debug and Fastmem to be changed per-game 2023-12-12 17:25:37 -05:00
f2eb3c579f android: Add per-game drivers 2023-12-12 17:25:37 -05:00
2fce812026 android: Add per-game settings 2023-12-12 17:25:36 -05:00
e975f3cde9 android: Add Game properties
This commit has the UI for viewing a game's properties on long-press and some links to useful tools like
- Game info
- Shortcut to settings (global in this commit)
- Addon manager with installer
- Save data manager
- Option to clear all save data
- Option to clear shader cache
2023-12-12 17:25:36 -05:00
6b5fb2063f frontend_common: Fix settings reload bug
This clears the touch_from_button_maps array before we read new data into it because this read duplicate data on a reload otherwise.
2023-12-12 17:25:36 -05:00
70c3d36536 android: Refactor settings to expose more options
In AbstractSetting, this removes the category, androidDefault, and valueAsString properties as they are no longer needed and have replacements. isSwitchable, global, and getValueAsString are all exposed and give better options for working with global/per-game settings.
2023-12-12 17:25:36 -05:00
d590cfb9d0 Merge pull request #12342 from FearlessTobi/fix-msvc
vcpkg: Update fmt to 10.1.1
2023-12-12 15:32:09 -05:00
ded419ef2b Merge pull request #12343 from FearlessTobi/fix-typo
configure_debug: Fix small typo
2023-12-12 15:32:00 -05:00
4c3f898789 configure_debug: Fix small typo 2023-12-12 20:38:54 +01:00
46c259bb20 vcpkg: Update fmt to 10.1.1
Should fix compiling on the latest version of MSVC.
2023-12-12 19:27:20 +01:00
adc3079613 vi: fix confusion between closing and destroying layers 2023-12-12 12:14:23 -05:00
15bebf1695 Merge pull request #12328 from german77/profile_manager
core: Use single instance of profile manager
2023-12-12 11:06:37 -05:00
5c840334b8 Merge pull request #12333 from german77/aruid_free
service: hid: Improve CreateAppletResource implementation and free resources
2023-12-12 11:06:24 -05:00
a05c242429 nce: adjust initialization for repeated calls 2023-12-11 23:02:01 -05:00
bd59934350 nce: implement instruction emulation for misaligned memory accesses 2023-12-10 18:23:42 -05:00
11b123ba01 Merge pull request #12322 from liamwhite/savedata-absurdity
fs: don't enumerate hidden savedata size file
2023-12-10 18:17:11 -05:00
24e7ace876 Merge pull request #12327 from liamwhite/tipc
sm:: fix tipc deserialization
2023-12-10 18:17:04 -05:00
62586c1676 Merge pull request #12317 from liamwhite/sc-fix
kernel: fix single core
2023-12-10 18:16:58 -05:00
108737fcc6 Merge pull request #12321 from liamwhite/ro2
ro: add separate ro service
2023-12-10 18:16:50 -05:00
abfebe5cc4 service: hid: Improve CreateAppletResource implementation and free resources 2023-12-10 16:17:51 -06:00
a22a025c5b core: Use single instance of profile manager 2023-12-10 11:29:43 -06:00
a529ef4c09 sm:: fix tipc deserialization 2023-12-09 22:56:21 -05:00
875568bb3e Merge pull request #12296 from liamwhite/client-session
kernel: implement remaining IPC syscalls
2023-12-09 19:03:56 -05:00
988e557ec8 Merge pull request #12299 from liamwhite/light-ipc
kernel: implement light IPC
2023-12-09 19:03:50 -05:00
6d2af32f29 Merge pull request #12323 from liamwhite/buffer-format
fbshare: set external format correctly
2023-12-09 19:00:52 -05:00
8f9d5c3143 Merge pull request #12325 from GPUCode/better-nce-message
settings: Clearer NCE error messages
2023-12-09 19:00:44 -05:00
dc0fb56f3a settings: Clearer NCE error messages 2023-12-09 23:37:14 +02:00
7ba4a8f4a3 ro: add separate ro service 2023-12-09 15:50:34 -05:00
8ef1fdafa2 fbshare: set external format correctly 2023-12-09 14:51:15 -05:00
d597383ab2 fs: don't enumerate hidden savedata size file 2023-12-09 14:05:16 -05:00
5feda37688 service: populate pid and handle table from client 2023-12-09 13:45:25 -05:00
34e4012998 service: use interface factory in server manager 2023-12-09 13:45:25 -05:00
c1924951ad Merge pull request #12289 from german77/aruid
service: hid: Introduce proper AppletResource emulation
2023-12-09 13:41:06 -05:00
5646e313a0 Merge pull request #12320 from liamwhite/debug-fix
debug: fix reading of module names
2023-12-09 13:38:39 -05:00
f447996080 Merge pull request #12319 from t895/refresh-list-bug
android: Listen for directory selection in viewmodel
2023-12-09 13:38:34 -05:00
42b34a0dc5 Merge pull request #12318 from t895/nce-default
settings: Enable NCE by default on capable systems
2023-12-09 13:38:27 -05:00
fe5e4bd846 debug: fix reading of module names 2023-12-09 11:18:10 -05:00
a53cd2854e android: Listen for directory selection in viewmodel
Fixes broken game directory selection setup flow
2023-12-08 22:02:04 -05:00
1d731dd1ff kernel: fix single core 2023-12-08 20:31:18 -05:00
8225ac004e settings: Enable NCE by default on capable systems 2023-12-08 20:25:07 -05:00
52e6b8a2d3 Merge pull request #12274 from liamwhite/srgb-nonsense
renderer_vulkan: do not recreate swapchain for srgb
2023-12-08 12:26:13 -05:00
13131e602f Merge pull request #12208 from liamwhite/romfs
romfs: optimize parsing and building
2023-12-08 12:25:58 -05:00
7761f29892 Merge pull request #11214 from lat9nq/ff-deprecated
codec: Update to use av frame flags
2023-12-07 23:13:13 -05:00
e92b10f971 dist: add udev rule to enable user hidraw access (#12292)
* dist: add udev rule to enable user hidraw access

* dist: amend install instructions for udev rules

* dist: change udev prefix prefix from 99 to 72

* dist: fix header typo for udev rule

* Update dist/72-yuzu-input.rules

shoutout to @liamwhite

Co-authored-by: liamwhite <liamwhite@users.noreply.github.com>

* Update dist/72-yuzu-input.rules

shout out to @liamwhite

Co-authored-by: liamwhite <liamwhite@users.noreply.github.com>

* Update dist/72-yuzu-input.rules

shout out to @liamwhite

Co-authored-by: liamwhite <liamwhite@users.noreply.github.com>

* Update dist/72-yuzu-input.rules

shout out to @liamwhite

Co-authored-by: liamwhite <liamwhite@users.noreply.github.com>

* Update dist/72-yuzu-input.rules

Co-authored-by: liamwhite <liamwhite@users.noreply.github.com>

* dist: add spdx header to udev rules

* dist: change udev modes to 0660

* Update dist/72-yuzu-input.rules

Co-authored-by: liamwhite <liamwhite@users.noreply.github.com>

* Update dist/72-yuzu-input.rules

Co-authored-by: liamwhite <liamwhite@users.noreply.github.com>

---------

Co-authored-by: HurricanePootis <hurricanepootis@protonmail.com>
Co-authored-by: liamwhite <liamwhite@users.noreply.github.com>
2023-12-07 23:12:56 -05:00
9268f265a1 kernel: implement light IPC 2023-12-07 09:13:43 -05:00
e445ef9d60 service: hid: Introduce proper AppletResource emulation 2023-12-06 20:24:04 -06:00
40bb176c39 kernel: implement remaining IPC syscalls 2023-12-06 17:33:00 -05:00
8a79dd2d6c Merge pull request #12236 from liamwhite/cpu-refactor
core: refactor emulated cpu core activation
2023-12-06 14:19:17 +01:00
d5de9402ee Improve path splitting speed 2023-12-05 23:17:19 -05:00
4cd3f9f4f9 codec: Update to use av frame flags
Resolves Clang -Wdeprecated-declarations warning from interlaced_frame
2023-12-05 21:10:38 -05:00
167efb2d2b Merge pull request #12271 from liamwhite/pretext-fix
nce: fix pre-text patch for single modules
2023-12-05 07:51:53 -05:00
8e0e066c3f Merge pull request #12283 from t895/language-troubles
frontend_common: Use optional for language default
2023-12-04 22:45:08 -05:00
f0ee3e29cb arm: fix context save of vector regs 2023-12-04 22:19:11 -05:00
5d4da07943 frontend_common: Use optional for language default 2023-12-04 19:49:01 -05:00
45c87c7e6e core: refactor emulated cpu core activation 2023-12-04 10:37:16 -05:00
90e87c40e8 Merge pull request #12235 from liamwhite/flip-clip
renderer_vulkan: adjust window origin and swizzle independently
2023-12-03 21:59:11 -05:00
6b7dc587cf texture_cache: fix max_element comparison function 2023-12-03 18:40:19 -05:00
f05cb69d4f renderer_opengl: remove srgb conversion logic 2023-12-03 17:08:25 -05:00
382cf087a0 renderer_vulkan: do not recreate swapchain for srgb 2023-12-03 16:43:54 -05:00
0751488727 fsmitm_romfsbuild: optimize for data locality 2023-12-03 16:29:57 -05:00
4bc932261b romfs: cache file and directory metadata tables 2023-12-03 16:29:57 -05:00
5fb1a83e4c Merge pull request #12094 from ameerj/gl-buffer-cache-batch-vtx
gl_buffer_cache: Batch vertex/tfb buffer binding
2023-12-03 16:27:22 -05:00
6da8301773 Merge pull request #12196 from ameerj/glsl-cbuf-sizes
GLSL: Use known cbuf sizes when possible
2023-12-03 16:27:07 -05:00
fedeff7a89 Merge pull request #12263 from liamwhite/null-romfs
file_sys: handle null romfs
2023-12-03 16:26:14 -05:00
9de99839bd nce: fix pre-text patch for single modules 2023-12-02 16:14:52 -05:00
45b6161582 file_sys: handle null romfs 2023-12-01 23:39:48 -05:00
e7dd968ac4 renderer_vulkan: adjust window origin and swizzle independently 2023-11-30 12:33:26 -05:00
db1d32485e GLSL: Prefer known used cbuf sizes 2023-11-26 23:25:29 -05:00
a595ed499d gl_buffer_cache: Batch vertex/tfb buffer binding 2023-11-19 17:17:16 -05:00
431 changed files with 21725 additions and 8939 deletions

View File

@ -6,7 +6,12 @@
export NDK_CCACHE="$(which ccache)"
ccache -s
BUILD_FLAVOR=mainline
BUILD_FLAVOR="mainline"
BUILD_TYPE="release"
if [ "${GITHUB_REPOSITORY}" == "yuzu-emu/yuzu" ]; then
BUILD_TYPE="relWithDebInfo"
fi
if [ ! -z "${ANDROID_KEYSTORE_B64}" ]; then
export ANDROID_KEYSTORE_FILE="${GITHUB_WORKSPACE}/ks.jks"
@ -15,7 +20,7 @@ fi
cd src/android
chmod +x ./gradlew
./gradlew "assemble${BUILD_FLAVOR}Release" "bundle${BUILD_FLAVOR}Release"
./gradlew "assemble${BUILD_FLAVOR}${BUILD_TYPE}" "bundle${BUILD_FLAVOR}${BUILD_TYPE}"
ccache -s

View File

@ -7,9 +7,16 @@
REV_NAME="yuzu-${GITDATE}-${GITREV}"
BUILD_FLAVOR=mainline
BUILD_FLAVOR="mainline"
cp src/android/app/build/outputs/apk/"${BUILD_FLAVOR}/release/app-${BUILD_FLAVOR}-release.apk" \
BUILD_TYPE_LOWER="release"
BUILD_TYPE_UPPER="Release"
if [ "${GITHUB_REPOSITORY}" == "yuzu-emu/yuzu" ]; then
BUILD_TYPE_LOWER="relWithDebInfo"
BUILD_TYPE_UPPER="RelWithDebInfo"
fi
cp src/android/app/build/outputs/apk/"${BUILD_FLAVOR}/${BUILD_TYPE_LOWER}/app-${BUILD_FLAVOR}-${BUILD_TYPE_LOWER}.apk" \
"artifacts/${REV_NAME}.apk"
cp src/android/app/build/outputs/bundle/"${BUILD_FLAVOR}Release"/"app-${BUILD_FLAVOR}-release.aab" \
cp src/android/app/build/outputs/bundle/"${BUILD_FLAVOR}${BUILD_TYPE_UPPER}"/"app-${BUILD_FLAVOR}-${BUILD_TYPE_LOWER}.aab" \
"artifacts/${REV_NAME}.aab"

View File

@ -3,4 +3,4 @@
[codespell]
skip = ./.git,./build,./dist,./Doxyfile,./externals,./LICENSES,./src/android/app/src/main/res
ignore-words-list = aci,allright,ba,canonicalizations,deques,froms,hda,inout,lod,masia,nam,nax,nce,nd,optin,pullrequests,pullrequest,te,transfered,unstall,uscaled,vas,zink
ignore-words-list = aci,allright,ba,canonicalizations,deques,fpr,froms,hda,inout,lod,masia,nam,nax,nce,nd,optin,pullrequests,pullrequest,te,transfered,unstall,uscaled,vas,zink

View File

@ -79,7 +79,8 @@ jobs:
fetch-depth: 0
- name: Install dependencies
run: |
brew install autoconf automake boost@1.83 ccache ffmpeg fmt glslang hidapi libtool libusb lz4 ninja nlohmann-json openssl pkg-config qt@5 sdl2 speexdsp zlib zlib zstd
# workaround for https://github.com/actions/setup-python/issues/577
brew install autoconf automake boost@1.83 ccache ffmpeg fmt glslang hidapi libtool libusb lz4 ninja nlohmann-json openssl pkg-config qt@5 sdl2 speexdsp zlib zlib zstd || brew link --overwrite python@3.12
- name: Build
run: |
mkdir build

View File

@ -142,6 +142,9 @@ if (YUZU_USE_BUNDLED_VCPKG)
if (ENABLE_WEB_SERVICE)
list(APPEND VCPKG_MANIFEST_FEATURES "web-service")
endif()
if (ANDROID)
list(APPEND VCPKG_MANIFEST_FEATURES "android")
endif()
include(${CMAKE_SOURCE_DIR}/externals/vcpkg/scripts/buildsystems/vcpkg.cmake)
elseif(NOT "$ENV{VCPKG_TOOLCHAIN_FILE}" STREQUAL "")
@ -302,7 +305,7 @@ find_package(ZLIB 1.2 REQUIRED)
find_package(zstd 1.5 REQUIRED)
if (NOT YUZU_USE_EXTERNAL_VULKAN_HEADERS)
find_package(Vulkan 1.3.256 REQUIRED)
find_package(Vulkan 1.3.274 REQUIRED)
endif()
if (ENABLE_LIBUSB)

View File

@ -1,3 +1,16 @@
| Pull Request | Commit | Title | Author | Merged? |
|----|----|----|----|----|
| [12454](https://github.com/yuzu-emu/yuzu//pull/12454) | [`3a4e7d45f`](https://github.com/yuzu-emu/yuzu//pull/12454/files) | core_timing: minor refactors | [liamwhite](https://github.com/liamwhite/) | Yes |
| [12466](https://github.com/yuzu-emu/yuzu//pull/12466) | [`adb2af0a2`](https://github.com/yuzu-emu/yuzu//pull/12466/files) | core: track separate heap allocation for linux | [liamwhite](https://github.com/liamwhite/) | Yes |
| [12501](https://github.com/yuzu-emu/yuzu//pull/12501) | [`d1c99c5d5`](https://github.com/yuzu-emu/yuzu//pull/12501/files) | ips_layer: prevent out of bounds access with offset exceeding module size | [liamwhite](https://github.com/liamwhite/) | Yes |
| [12513](https://github.com/yuzu-emu/yuzu//pull/12513) | [`558192abf`](https://github.com/yuzu-emu/yuzu//pull/12513/files) | jit: use code memory handles correctly | [liamwhite](https://github.com/liamwhite/) | Yes |
| [12518](https://github.com/yuzu-emu/yuzu//pull/12518) | [`aa4d15594`](https://github.com/yuzu-emu/yuzu//pull/12518/files) | android: Migrate remaining settings to ini | [t895](https://github.com/t895/) | Yes |
End of merge log. You can find the original README.md below the break.
-----
<!--
SPDX-FileCopyrightText: 2018 yuzu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later

19
dist/72-yuzu-input.rules vendored Normal file
View File

@ -0,0 +1,19 @@
# SPDX-FileCopyrightText: 2023 yuzu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
# Allow systemd-logind to manage user access to hidraw with this file
# On most systems, this file should be installed to /etc/udev/rules.d/72-yuzu-input.rules
# Consult your distro if this is not the case
# Switch Pro Controller (USB/Bluetooth)
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="2009", MODE="0660", TAG+="uaccess"
KERNEL=="hidraw*", KERNELS=="*057e:2009*", MODE="0660", TAG+="uaccess"
# Joy-Con L (Bluetooth)
KERNEL=="hidraw*", KERNELS=="*057e:2006*", MODE="0660", TAG+="uaccess"
# Joy-Con R (Bluetooth)
KERNEL=="hidraw*", KERNELS=="*057e:2007*", MODE="0660", TAG+="uaccess"
# Joy-Con Charging Grip (USB)
KERNEL=="hidraw*", ATTRS{idVendor}=="057e", ATTRS{idProduct}=="200e", MODE="0660", TAG+="uaccess"

View File

@ -10,7 +10,7 @@ plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-parcelize")
kotlin("plugin.serialization") version "1.8.21"
kotlin("plugin.serialization") version "1.9.20"
id("androidx.navigation.safeargs.kotlin")
id("org.jlleitschuh.gradle.ktlint") version "11.4.0"
}
@ -174,7 +174,8 @@ android {
"-DANDROID_ARM_NEON=true", // cryptopp requires Neon to work
"-DYUZU_USE_BUNDLED_VCPKG=ON",
"-DYUZU_USE_BUNDLED_FFMPEG=ON",
"-DYUZU_ENABLE_LTO=ON"
"-DYUZU_ENABLE_LTO=ON",
"-DCMAKE_EXPORT_COMPILE_COMMANDS=ON"
)
abiFilters("arm64-v8a", "x86_64")

View File

@ -230,8 +230,6 @@ object NativeLibrary {
*/
external fun onTouchReleased(finger_id: Int)
external fun initGameIni(gameID: String?)
external fun setAppDirectory(directory: String)
/**
@ -241,6 +239,8 @@ object NativeLibrary {
*/
external fun installFileToNand(filename: String, extension: String): Int
external fun doesUpdateMatchProgram(programId: String, updatePath: String): Boolean
external fun initializeGpuDriver(
hookLibDir: String?,
customDriverDir: String?,
@ -252,18 +252,11 @@ object NativeLibrary {
external fun initializeSystem(reload: Boolean)
external fun defaultCPUCore(): Int
/**
* Begins emulation.
*/
external fun run(path: String?)
/**
* Begins emulation from the specified savestate.
*/
external fun run(path: String?, savestatePath: String?, deleteSavestate: Boolean)
// Surface Handling
external fun surfaceChanged(surf: Surface?)
@ -304,10 +297,9 @@ object NativeLibrary {
*/
external fun getCpuBackend(): String
/**
* Notifies the core emulation that the orientation has changed.
*/
external fun notifyOrientationChange(layout_option: Int, rotation: Int)
external fun applySettings()
external fun logSettings()
enum class CoreError {
ErrorSystemFiles,
@ -538,6 +530,35 @@ object NativeLibrary {
*/
external fun isFirmwareAvailable(): Boolean
/**
* Checks the PatchManager for any addons that are available
*
* @param path Path to game file. Can be a [Uri].
* @param programId String representation of a game's program ID
* @return Array of pairs where the first value is the name of an addon and the second is the version
*/
external fun getAddonsForFile(path: String, programId: String): Array<Pair<String, String>>?
/**
* Gets the save location for a specific game
*
* @param programId String representation of a game's program ID
* @return Save data path that may not exist yet
*/
external fun getSavePath(programId: String): String
/**
* Adds a file to the manual filesystem provider in our EmulationSession instance
* @param path Path to the file we're adding. Can be a string representation of a [Uri] or
* a normal path
*/
external fun addFileToFilesystemProvider(path: String)
/**
* Clears all files added to the manual filesystem provider in our EmulationSession instance
*/
external fun clearFilesystemProvider()
/**
* Button type for use in onTouchEvent
*/

View File

@ -49,6 +49,7 @@ import org.yuzu.yuzu_emu.utils.ForegroundService
import org.yuzu.yuzu_emu.utils.InputHandler
import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.MemoryUtil
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.NfcReader
import org.yuzu.yuzu_emu.utils.ThemeHelper
import java.text.NumberFormat
@ -170,9 +171,14 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
stopMotionSensorListener()
}
override fun onStop() {
super.onStop()
NativeConfig.saveGlobalConfig()
}
override fun onUserLeaveHint() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
if (BooleanSetting.PICTURE_IN_PICTURE.boolean && !isInPictureInPictureMode) {
if (BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && !isInPictureInPictureMode) {
val pictureInPictureParamsBuilder = PictureInPictureParams.Builder()
.getPictureInPictureActionsBuilder().getPictureInPictureAspectBuilder()
enterPictureInPictureMode(pictureInPictureParamsBuilder.build())
@ -284,7 +290,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
private fun PictureInPictureParams.Builder.getPictureInPictureAspectBuilder():
PictureInPictureParams.Builder {
val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.int) {
val aspectRatio = when (IntSetting.RENDERER_ASPECT_RATIO.getInt()) {
0 -> Rational(16, 9)
1 -> Rational(4, 3)
2 -> Rational(21, 9)
@ -331,7 +337,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
pictureInPictureActions.add(pauseRemoteAction)
}
if (BooleanSetting.AUDIO_MUTED.boolean) {
if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
val unmuteIcon = Icon.createWithResource(
this@EmulationActivity,
R.drawable.ic_pip_unmute
@ -376,7 +382,7 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
val isEmulationActive = emulationViewModel.emulationStarted.value &&
!emulationViewModel.isEmulationStopping.value
pictureInPictureParamsBuilder.setAutoEnterEnabled(
BooleanSetting.PICTURE_IN_PICTURE.boolean && isEmulationActive
BooleanSetting.PICTURE_IN_PICTURE.getBoolean() && isEmulationActive
)
}
setPictureInPictureParams(pictureInPictureParamsBuilder.build())
@ -390,9 +396,13 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
if (!NativeLibrary.isPaused()) NativeLibrary.pauseEmulation()
}
if (intent.action == actionUnmute) {
if (BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(false)
if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
BooleanSetting.AUDIO_MUTED.setBoolean(false)
}
} else if (intent.action == actionMute) {
if (!BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(true)
if (!BooleanSetting.AUDIO_MUTED.getBoolean()) {
BooleanSetting.AUDIO_MUTED.setBoolean(true)
}
}
buildPictureInPictureParams()
}
@ -423,7 +433,9 @@ class EmulationActivity : AppCompatActivity(), SensorEventListener {
} catch (ignored: Exception) {
}
// Always resume audio, since there is no UI button
if (BooleanSetting.AUDIO_MUTED.boolean) BooleanSetting.AUDIO_MUTED.setBoolean(false)
if (BooleanSetting.AUDIO_MUTED.getBoolean()) {
BooleanSetting.AUDIO_MUTED.setBoolean(false)
}
}
}

View File

@ -0,0 +1,52 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import org.yuzu.yuzu_emu.databinding.ListItemAddonBinding
import org.yuzu.yuzu_emu.model.Addon
class AddonAdapter : ListAdapter<Addon, AddonAdapter.AddonViewHolder>(
AsyncDifferConfig.Builder(DiffCallback()).build()
) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AddonViewHolder {
ListItemAddonBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.also { return AddonViewHolder(it) }
}
override fun getItemCount(): Int = currentList.size
override fun onBindViewHolder(holder: AddonViewHolder, position: Int) =
holder.bind(currentList[position])
inner class AddonViewHolder(val binding: ListItemAddonBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(addon: Addon) {
binding.root.setOnClickListener {
binding.addonSwitch.isChecked = !binding.addonSwitch.isChecked
}
binding.title.text = addon.title
binding.version.text = addon.version
binding.addonSwitch.setOnCheckedChangeListener { _, checked ->
addon.enabled = checked
}
binding.addonSwitch.isChecked = addon.enabled
}
}
private class DiffCallback : DiffUtil.ItemCallback<Addon>() {
override fun areItemsTheSame(oldItem: Addon, newItem: Addon): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Addon, newItem: Addon): Boolean {
return oldItem == newItem
}
}
}

View File

@ -15,7 +15,7 @@ import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.databinding.CardAppletOptionBinding
import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
import org.yuzu.yuzu_emu.model.Applet
import org.yuzu.yuzu_emu.model.AppletInfo
import org.yuzu.yuzu_emu.model.Game
@ -28,7 +28,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) :
parent: ViewGroup,
viewType: Int
): AppletAdapter.AppletViewHolder {
CardAppletOptionBinding.inflate(LayoutInflater.from(parent.context), parent, false)
CardSimpleOutlinedBinding.inflate(LayoutInflater.from(parent.context), parent, false)
.apply { root.setOnClickListener(this@AppletAdapter) }
.also { return AppletViewHolder(it) }
}
@ -65,7 +65,7 @@ class AppletAdapter(val activity: FragmentActivity, var applets: List<Applet>) :
view.findNavController().navigate(action)
}
inner class AppletViewHolder(val binding: CardAppletOptionBinding) :
inner class AppletViewHolder(val binding: CardSimpleOutlinedBinding) :
RecyclerView.ViewHolder(binding.root) {
lateinit var applet: Applet

View File

@ -42,7 +42,7 @@ class DriverAdapter(private val driverViewModel: DriverViewModel) :
if (driverViewModel.selectedDriver > position) {
driverViewModel.setSelectedDriverIndex(driverViewModel.selectedDriver - 1)
}
if (GpuDriverHelper.customDriverData == driverData.second) {
if (GpuDriverHelper.customDriverSettingData == driverData.second) {
driverViewModel.setSelectedDriverIndex(0)
}
driverViewModel.driversToDelete.add(driverData.first)

View File

@ -44,19 +44,20 @@ import org.yuzu.yuzu_emu.utils.GameIconUtils
class GameAdapter(private val activity: AppCompatActivity) :
ListAdapter<Game, GameViewHolder>(AsyncDifferConfig.Builder(DiffCallback()).build()),
View.OnClickListener {
View.OnClickListener,
View.OnLongClickListener {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder {
// Create a new view.
val binding = CardGameBinding.inflate(LayoutInflater.from(parent.context), parent, false)
binding.cardGame.setOnClickListener(this)
binding.cardGame.setOnLongClickListener(this)
// Use that view to create a ViewHolder.
return GameViewHolder(binding)
}
override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
override fun onBindViewHolder(holder: GameViewHolder, position: Int) =
holder.bind(currentList[position])
}
override fun getItemCount(): Int = currentList.size
@ -125,10 +126,17 @@ class GameAdapter(private val activity: AppCompatActivity) :
}
}
val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game)
val action = HomeNavigationDirections.actionGlobalEmulationActivity(holder.game, true)
view.findNavController().navigate(action)
}
override fun onLongClick(view: View): Boolean {
val holder = view.tag as GameViewHolder
val action = HomeNavigationDirections.actionGlobalPerGamePropertiesFragment(holder.game)
view.findNavController().navigate(action)
return true
}
inner class GameViewHolder(val binding: CardGameBinding) :
RecyclerView.ViewHolder(binding.root) {
lateinit var game: Game
@ -157,7 +165,7 @@ class GameAdapter(private val activity: AppCompatActivity) :
private class DiffCallback : DiffUtil.ItemCallback<Game>() {
override fun areItemsTheSame(oldItem: Game, newItem: Game): Boolean {
return oldItem.programId == newItem.programId
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Game, newItem: Game): Boolean {

View File

@ -0,0 +1,140 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.adapters
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.databinding.CardInstallableIconBinding
import org.yuzu.yuzu_emu.databinding.CardSimpleOutlinedBinding
import org.yuzu.yuzu_emu.model.GameProperty
import org.yuzu.yuzu_emu.model.InstallableProperty
import org.yuzu.yuzu_emu.model.SubmenuProperty
class GamePropertiesAdapter(
private val viewLifecycle: LifecycleOwner,
private var properties: List<GameProperty>
) :
RecyclerView.Adapter<GamePropertiesAdapter.GamePropertyViewHolder>() {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): GamePropertyViewHolder {
val inflater = LayoutInflater.from(parent.context)
return when (viewType) {
PropertyType.Submenu.ordinal -> {
SubmenuPropertyViewHolder(
CardSimpleOutlinedBinding.inflate(
inflater,
parent,
false
)
)
}
else -> InstallablePropertyViewHolder(
CardInstallableIconBinding.inflate(
inflater,
parent,
false
)
)
}
}
override fun getItemCount(): Int = properties.size
override fun onBindViewHolder(holder: GamePropertyViewHolder, position: Int) =
holder.bind(properties[position])
override fun getItemViewType(position: Int): Int {
return when (properties[position]) {
is SubmenuProperty -> PropertyType.Submenu.ordinal
else -> PropertyType.Installable.ordinal
}
}
sealed class GamePropertyViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
abstract fun bind(property: GameProperty)
}
inner class SubmenuPropertyViewHolder(val binding: CardSimpleOutlinedBinding) :
GamePropertyViewHolder(binding.root) {
override fun bind(property: GameProperty) {
val submenuProperty = property as SubmenuProperty
binding.root.setOnClickListener {
submenuProperty.action.invoke()
}
binding.title.setText(submenuProperty.titleId)
binding.description.setText(submenuProperty.descriptionId)
binding.icon.setImageDrawable(
ResourcesCompat.getDrawable(
binding.icon.context.resources,
submenuProperty.iconId,
binding.icon.context.theme
)
)
binding.details.postDelayed({
binding.details.isSelected = true
binding.details.ellipsize = TextUtils.TruncateAt.MARQUEE
}, 3000)
if (submenuProperty.details != null) {
binding.details.visibility = View.VISIBLE
binding.details.text = submenuProperty.details.invoke()
} else if (submenuProperty.detailsFlow != null) {
binding.details.visibility = View.VISIBLE
viewLifecycle.lifecycleScope.launch {
viewLifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
submenuProperty.detailsFlow.collect { binding.details.text = it }
}
}
} else {
binding.details.visibility = View.GONE
}
}
}
inner class InstallablePropertyViewHolder(val binding: CardInstallableIconBinding) :
GamePropertyViewHolder(binding.root) {
override fun bind(property: GameProperty) {
val installableProperty = property as InstallableProperty
binding.title.setText(installableProperty.titleId)
binding.description.setText(installableProperty.descriptionId)
binding.icon.setImageDrawable(
ResourcesCompat.getDrawable(
binding.icon.context.resources,
installableProperty.iconId,
binding.icon.context.theme
)
)
if (installableProperty.install != null) {
binding.buttonInstall.visibility = View.VISIBLE
binding.buttonInstall.setOnClickListener { installableProperty.install.invoke() }
}
if (installableProperty.export != null) {
binding.buttonExport.visibility = View.VISIBLE
binding.buttonExport.setOnClickListener { installableProperty.export.invoke() }
}
}
}
enum class PropertyType {
Submenu,
Installable
}
}

View File

@ -4,7 +4,6 @@
package org.yuzu.yuzu_emu.features.settings.model
interface AbstractBooleanSetting : AbstractSetting {
val boolean: Boolean
fun getBoolean(needsGlobal: Boolean = false): Boolean
fun setBoolean(value: Boolean)
}

View File

@ -4,7 +4,6 @@
package org.yuzu.yuzu_emu.features.settings.model
interface AbstractByteSetting : AbstractSetting {
val byte: Byte
fun getByte(needsGlobal: Boolean = false): Byte
fun setByte(value: Byte)
}

View File

@ -4,7 +4,6 @@
package org.yuzu.yuzu_emu.features.settings.model
interface AbstractFloatSetting : AbstractSetting {
val float: Float
fun getFloat(needsGlobal: Boolean = false): Float
fun setFloat(value: Float)
}

View File

@ -4,7 +4,6 @@
package org.yuzu.yuzu_emu.features.settings.model
interface AbstractIntSetting : AbstractSetting {
val int: Int
fun getInt(needsGlobal: Boolean = false): Int
fun setInt(value: Int)
}

View File

@ -4,7 +4,6 @@
package org.yuzu.yuzu_emu.features.settings.model
interface AbstractLongSetting : AbstractSetting {
val long: Long
fun getLong(needsGlobal: Boolean = false): Long
fun setLong(value: Long)
}

View File

@ -7,12 +7,7 @@ import org.yuzu.yuzu_emu.utils.NativeConfig
interface AbstractSetting {
val key: String
val category: Settings.Category
val defaultValue: Any
val androidDefault: Any?
get() = null
val valueAsString: String
get() = ""
val isRuntimeModifiable: Boolean
get() = NativeConfig.getIsRuntimeModifiable(key)
@ -20,5 +15,17 @@ interface AbstractSetting {
val pairedSettingKey: String
get() = NativeConfig.getPairedSettingKey(key)
val isSwitchable: Boolean
get() = NativeConfig.getIsSwitchable(key)
var global: Boolean
get() = NativeConfig.usingGlobal(key)
set(value) = NativeConfig.setGlobal(key, value)
val isSaveable: Boolean
get() = NativeConfig.getIsSaveable(key)
fun getValueAsString(needsGlobal: Boolean = false): String
fun reset()
}

View File

@ -4,7 +4,6 @@
package org.yuzu.yuzu_emu.features.settings.model
interface AbstractShortSetting : AbstractSetting {
val short: Short
fun getShort(needsGlobal: Boolean = false): Short
fun setShort(value: Short)
}

View File

@ -4,7 +4,6 @@
package org.yuzu.yuzu_emu.features.settings.model
interface AbstractStringSetting : AbstractSetting {
val string: String
fun getString(needsGlobal: Boolean = false): String
fun setString(value: String)
}

View File

@ -5,36 +5,41 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class BooleanSetting(
override val key: String,
override val category: Settings.Category,
override val androidDefault: Boolean? = null
) : AbstractBooleanSetting {
AUDIO_MUTED("audio_muted", Settings.Category.Audio),
CPU_DEBUG_MODE("cpu_debug_mode", Settings.Category.Cpu),
FASTMEM("cpuopt_fastmem", Settings.Category.Cpu),
FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives", Settings.Category.Cpu),
RENDERER_USE_SPEED_LIMIT("use_speed_limit", Settings.Category.Core),
USE_DOCKED_MODE("use_docked_mode", Settings.Category.System, false),
RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache", Settings.Category.Renderer),
RENDERER_FORCE_MAX_CLOCK("force_max_clock", Settings.Category.Renderer),
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders", Settings.Category.Renderer),
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing", Settings.Category.Renderer, false),
RENDERER_DEBUG("debug", Settings.Category.Renderer),
PICTURE_IN_PICTURE("picture_in_picture", Settings.Category.Android),
USE_CUSTOM_RTC("custom_rtc_enabled", Settings.Category.System);
enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
AUDIO_MUTED("audio_muted"),
CPU_DEBUG_MODE("cpu_debug_mode"),
FASTMEM("cpuopt_fastmem"),
FASTMEM_EXCLUSIVES("cpuopt_fastmem_exclusives"),
RENDERER_USE_SPEED_LIMIT("use_speed_limit"),
USE_DOCKED_MODE("use_docked_mode"),
RENDERER_USE_DISK_SHADER_CACHE("use_disk_shader_cache"),
RENDERER_FORCE_MAX_CLOCK("force_max_clock"),
RENDERER_ASYNCHRONOUS_SHADERS("use_asynchronous_shaders"),
RENDERER_REACTIVE_FLUSHING("use_reactive_flushing"),
RENDERER_DEBUG("debug"),
PICTURE_IN_PICTURE("picture_in_picture"),
USE_CUSTOM_RTC("custom_rtc_enabled"),
BLACK_BACKGROUNDS("black_backgrounds"),
JOYSTICK_REL_CENTER("joystick_rel_center"),
DPAD_SLIDE("dpad_slide"),
HAPTIC_FEEDBACK("haptic_feedback"),
SHOW_PERFORMANCE_OVERLAY("show_performance_overlay"),
SHOW_INPUT_OVERLAY("show_input_overlay"),
TOUCHSCREEN("touchscreen");
override val boolean: Boolean
get() = NativeConfig.getBoolean(key, false)
override fun getBoolean(needsGlobal: Boolean): Boolean =
NativeConfig.getBoolean(key, needsGlobal)
override fun setBoolean(value: Boolean) = NativeConfig.setBoolean(key, value)
override val defaultValue: Boolean by lazy {
androidDefault ?: NativeConfig.getBoolean(key, true)
override fun setBoolean(value: Boolean) {
if (NativeConfig.isPerGameConfigLoaded()) {
global = false
}
NativeConfig.setBoolean(key, value)
}
override val valueAsString: String
get() = if (boolean) "1" else "0"
override val defaultValue: Boolean by lazy { NativeConfig.getDefaultToString(key).toBoolean() }
override fun getValueAsString(needsGlobal: Boolean): String = getBoolean(needsGlobal).toString()
override fun reset() = NativeConfig.setBoolean(key, defaultValue)
}

View File

@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class ByteSetting(
override val key: String,
override val category: Settings.Category
) : AbstractByteSetting {
AUDIO_VOLUME("volume", Settings.Category.Audio);
enum class ByteSetting(override val key: String) : AbstractByteSetting {
AUDIO_VOLUME("volume");
override val byte: Byte
get() = NativeConfig.getByte(key, false)
override fun getByte(needsGlobal: Boolean): Byte = NativeConfig.getByte(key, needsGlobal)
override fun setByte(value: Byte) = NativeConfig.setByte(key, value)
override fun setByte(value: Byte) {
if (NativeConfig.isPerGameConfigLoaded()) {
global = false
}
NativeConfig.setByte(key, value)
}
override val defaultValue: Byte by lazy { NativeConfig.getByte(key, true) }
override val defaultValue: Byte by lazy { NativeConfig.getDefaultToString(key).toByte() }
override val valueAsString: String
get() = byte.toString()
override fun getValueAsString(needsGlobal: Boolean): String = getByte(needsGlobal).toString()
override fun reset() = NativeConfig.setByte(key, defaultValue)
}

View File

@ -5,22 +5,22 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class FloatSetting(
override val key: String,
override val category: Settings.Category
) : AbstractFloatSetting {
enum class FloatSetting(override val key: String) : AbstractFloatSetting {
// No float settings currently exist
EMPTY_SETTING("", Settings.Category.UiGeneral);
EMPTY_SETTING("");
override val float: Float
get() = NativeConfig.getFloat(key, false)
override fun getFloat(needsGlobal: Boolean): Float = NativeConfig.getFloat(key, false)
override fun setFloat(value: Float) = NativeConfig.setFloat(key, value)
override fun setFloat(value: Float) {
if (NativeConfig.isPerGameConfigLoaded()) {
global = false
}
NativeConfig.setFloat(key, value)
}
override val defaultValue: Float by lazy { NativeConfig.getFloat(key, true) }
override val defaultValue: Float by lazy { NativeConfig.getDefaultToString(key).toFloat() }
override val valueAsString: String
get() = float.toString()
override fun getValueAsString(needsGlobal: Boolean): String = getFloat(needsGlobal).toString()
override fun reset() = NativeConfig.setFloat(key, defaultValue)
}

View File

@ -5,36 +5,38 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class IntSetting(
override val key: String,
override val category: Settings.Category,
override val androidDefault: Int? = null
) : AbstractIntSetting {
CPU_BACKEND("cpu_backend", Settings.Category.Cpu),
CPU_ACCURACY("cpu_accuracy", Settings.Category.Cpu),
REGION_INDEX("region_index", Settings.Category.System),
LANGUAGE_INDEX("language_index", Settings.Category.System),
RENDERER_BACKEND("backend", Settings.Category.Renderer),
RENDERER_ACCURACY("gpu_accuracy", Settings.Category.Renderer, 0),
RENDERER_RESOLUTION("resolution_setup", Settings.Category.Renderer),
RENDERER_VSYNC("use_vsync", Settings.Category.Renderer),
RENDERER_SCALING_FILTER("scaling_filter", Settings.Category.Renderer),
RENDERER_ANTI_ALIASING("anti_aliasing", Settings.Category.Renderer),
RENDERER_SCREEN_LAYOUT("screen_layout", Settings.Category.Android),
RENDERER_ASPECT_RATIO("aspect_ratio", Settings.Category.Renderer),
AUDIO_OUTPUT_ENGINE("output_engine", Settings.Category.Audio);
enum class IntSetting(override val key: String) : AbstractIntSetting {
CPU_BACKEND("cpu_backend"),
CPU_ACCURACY("cpu_accuracy"),
REGION_INDEX("region_index"),
LANGUAGE_INDEX("language_index"),
RENDERER_BACKEND("backend"),
RENDERER_ACCURACY("gpu_accuracy"),
RENDERER_RESOLUTION("resolution_setup"),
RENDERER_VSYNC("use_vsync"),
RENDERER_SCALING_FILTER("scaling_filter"),
RENDERER_ANTI_ALIASING("anti_aliasing"),
RENDERER_SCREEN_LAYOUT("screen_layout"),
RENDERER_ASPECT_RATIO("aspect_ratio"),
AUDIO_OUTPUT_ENGINE("output_engine"),
MAX_ANISOTROPY("max_anisotropy"),
THEME("theme"),
THEME_MODE("theme_mode"),
OVERLAY_SCALE("control_scale"),
OVERLAY_OPACITY("control_opacity");
override val int: Int
get() = NativeConfig.getInt(key, false)
override fun getInt(needsGlobal: Boolean): Int = NativeConfig.getInt(key, needsGlobal)
override fun setInt(value: Int) = NativeConfig.setInt(key, value)
override val defaultValue: Int by lazy {
androidDefault ?: NativeConfig.getInt(key, true)
override fun setInt(value: Int) {
if (NativeConfig.isPerGameConfigLoaded()) {
global = false
}
NativeConfig.setInt(key, value)
}
override val valueAsString: String
get() = int.toString()
override val defaultValue: Int by lazy { NativeConfig.getDefaultToString(key).toInt() }
override fun getValueAsString(needsGlobal: Boolean): String = getInt(needsGlobal).toString()
override fun reset() = NativeConfig.setInt(key, defaultValue)
}

View File

@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class LongSetting(
override val key: String,
override val category: Settings.Category
) : AbstractLongSetting {
CUSTOM_RTC("custom_rtc", Settings.Category.System);
enum class LongSetting(override val key: String) : AbstractLongSetting {
CUSTOM_RTC("custom_rtc");
override val long: Long
get() = NativeConfig.getLong(key, false)
override fun getLong(needsGlobal: Boolean): Long = NativeConfig.getLong(key, needsGlobal)
override fun setLong(value: Long) = NativeConfig.setLong(key, value)
override fun setLong(value: Long) {
if (NativeConfig.isPerGameConfigLoaded()) {
global = false
}
NativeConfig.setLong(key, value)
}
override val defaultValue: Long by lazy { NativeConfig.getLong(key, true) }
override val defaultValue: Long by lazy { NativeConfig.getDefaultToString(key).toLong() }
override val valueAsString: String
get() = long.toString()
override fun getValueAsString(needsGlobal: Boolean): String = getLong(needsGlobal).toString()
override fun reset() = NativeConfig.setLong(key, defaultValue)
}

View File

@ -6,78 +6,19 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.R
object Settings {
enum class Category {
Android,
Audio,
Core,
Cpu,
CpuDebug,
CpuUnsafe,
Renderer,
RendererAdvanced,
RendererDebug,
System,
SystemAudio,
DataStorage,
Debugging,
DebuggingGraphics,
Miscellaneous,
Network,
WebService,
AddOns,
Controls,
Ui,
UiGeneral,
UiLayout,
UiGameList,
Screenshots,
Shortcuts,
Multiplayer,
Services,
Paths,
MaxEnum
}
val settingsList = listOf<AbstractSetting>(
*BooleanSetting.values(),
*ByteSetting.values(),
*ShortSetting.values(),
*IntSetting.values(),
*FloatSetting.values(),
*LongSetting.values(),
*StringSetting.values()
)
const val SECTION_GENERAL = "General"
const val SECTION_SYSTEM = "System"
const val SECTION_RENDERER = "Renderer"
const val SECTION_AUDIO = "Audio"
const val SECTION_CPU = "Cpu"
const val SECTION_THEME = "Theme"
const val SECTION_DEBUG = "Debug"
enum class MenuTag(val titleId: Int) {
SECTION_ROOT(R.string.advanced_settings),
SECTION_SYSTEM(R.string.preferences_system),
SECTION_RENDERER(R.string.preferences_graphics),
SECTION_AUDIO(R.string.preferences_audio),
SECTION_CPU(R.string.cpu),
SECTION_THEME(R.string.preferences_theme),
SECTION_DEBUG(R.string.preferences_debug);
}
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
const val PREF_MEMORY_WARNING_SHOWN = "MemoryWarningShown"
const val PREF_OVERLAY_VERSION = "OverlayVersion"
const val PREF_LANDSCAPE_OVERLAY_VERSION = "LandscapeOverlayVersion"
const val PREF_PORTRAIT_OVERLAY_VERSION = "PortraitOverlayVersion"
const val PREF_FOLDABLE_OVERLAY_VERSION = "FoldableOverlayVersion"
val overlayLayoutPrefs = listOf(
PREF_LANDSCAPE_OVERLAY_VERSION,
PREF_PORTRAIT_OVERLAY_VERSION,
PREF_FOLDABLE_OVERLAY_VERSION
)
// Deprecated input overlay preference keys
const val PREF_CONTROL_SCALE = "controlScale"
const val PREF_CONTROL_OPACITY = "controlOpacity"
const val PREF_TOUCH_ENABLED = "isTouchEnabled"
@ -98,23 +39,12 @@ object Settings {
const val PREF_BUTTON_STICK_R = "buttonToggle14"
const val PREF_BUTTON_HOME = "buttonToggle15"
const val PREF_BUTTON_SCREENSHOT = "buttonToggle16"
const val PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER = "EmulationMenuSettings_JoystickRelCenter"
const val PREF_MENU_SETTINGS_DPAD_SLIDE = "EmulationMenuSettings_DpadSlideEnable"
const val PREF_MENU_SETTINGS_HAPTICS = "EmulationMenuSettings_Haptics"
const val PREF_MENU_SETTINGS_SHOW_FPS = "EmulationMenuSettings_ShowFps"
const val PREF_MENU_SETTINGS_SHOW_OVERLAY = "EmulationMenuSettings_ShowOverlay"
const val PREF_FIRST_APP_LAUNCH = "FirstApplicationLaunch"
const val PREF_THEME = "Theme"
const val PREF_THEME_MODE = "ThemeMode"
const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds"
val overlayPreferences = listOf(
PREF_OVERLAY_VERSION,
PREF_CONTROL_SCALE,
PREF_CONTROL_OPACITY,
PREF_TOUCH_ENABLED,
PREF_BUTTON_A,
PREF_BUTTON_B,
PREF_BUTTON_X,
@ -134,6 +64,21 @@ object Settings {
PREF_BUTTON_STICK_R
)
// Deprecated layout preference keys
const val PREF_LANDSCAPE_SUFFIX = "_Landscape"
const val PREF_PORTRAIT_SUFFIX = "_Portrait"
const val PREF_FOLDABLE_SUFFIX = "_Foldable"
val overlayLayoutSuffixes = listOf(
PREF_LANDSCAPE_SUFFIX,
PREF_PORTRAIT_SUFFIX,
PREF_FOLDABLE_SUFFIX
)
// Deprecated theme preference keys
const val PREF_THEME = "Theme"
const val PREF_THEME_MODE = "ThemeMode"
const val PREF_BLACK_BACKGROUNDS = "BlackBackgrounds"
const val LayoutOption_Unspecified = 0
const val LayoutOption_MobilePortrait = 4
const val LayoutOption_MobileLandscape = 5

View File

@ -5,21 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class ShortSetting(
override val key: String,
override val category: Settings.Category
) : AbstractShortSetting {
RENDERER_SPEED_LIMIT("speed_limit", Settings.Category.Core);
enum class ShortSetting(override val key: String) : AbstractShortSetting {
RENDERER_SPEED_LIMIT("speed_limit");
override val short: Short
get() = NativeConfig.getShort(key, false)
override fun getShort(needsGlobal: Boolean): Short = NativeConfig.getShort(key, needsGlobal)
override fun setShort(value: Short) = NativeConfig.setShort(key, value)
override fun setShort(value: Short) {
if (NativeConfig.isPerGameConfigLoaded()) {
global = false
}
NativeConfig.setShort(key, value)
}
override val defaultValue: Short by lazy { NativeConfig.getShort(key, true) }
override val defaultValue: Short by lazy { NativeConfig.getDefaultToString(key).toShort() }
override val valueAsString: String
get() = short.toString()
override fun getValueAsString(needsGlobal: Boolean): String = getShort(needsGlobal).toString()
override fun reset() = NativeConfig.setShort(key, defaultValue)
}

View File

@ -5,22 +5,21 @@ package org.yuzu.yuzu_emu.features.settings.model
import org.yuzu.yuzu_emu.utils.NativeConfig
enum class StringSetting(
override val key: String,
override val category: Settings.Category
) : AbstractStringSetting {
// No string settings currently exist
EMPTY_SETTING("", Settings.Category.UiGeneral);
enum class StringSetting(override val key: String) : AbstractStringSetting {
DRIVER_PATH("driver_path");
override val string: String
get() = NativeConfig.getString(key, false)
override fun getString(needsGlobal: Boolean): String = NativeConfig.getString(key, needsGlobal)
override fun setString(value: String) = NativeConfig.setString(key, value)
override fun setString(value: String) {
if (NativeConfig.isPerGameConfigLoaded()) {
global = false
}
NativeConfig.setString(key, value)
}
override val defaultValue: String by lazy { NativeConfig.getString(key, true) }
override val defaultValue: String by lazy { NativeConfig.getDefaultToString(key) }
override val valueAsString: String
get() = string
override fun getValueAsString(needsGlobal: Boolean): String = getString(needsGlobal)
override fun reset() = NativeConfig.setString(key, defaultValue)
}

View File

@ -12,7 +12,6 @@ class DateTimeSetting(
) : SettingsItem(longSetting, titleId, descriptionId) {
override val type = TYPE_DATETIME_SETTING
var value: Long
get() = longSetting.long
set(value) = (setting as AbstractLongSetting).setLong(value)
fun getValue(needsGlobal: Boolean = false): Long = longSetting.getLong(needsGlobal)
fun setValue(value: Long) = (setting as AbstractLongSetting).setLong(value)
}

View File

@ -11,8 +11,8 @@ import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.ByteSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.LongSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.ShortSetting
import org.yuzu.yuzu_emu.utils.NativeConfig
/**
* ViewModel abstraction for an Item in the RecyclerView powering SettingsFragments.
@ -30,10 +30,26 @@ abstract class SettingsItem(
val isEditable: Boolean
get() {
// Can't edit settings that aren't saveable in per-game config even if they are switchable
if (NativeConfig.isPerGameConfigLoaded() && !setting.isSaveable) {
return false
}
if (!NativeLibrary.isRunning()) return true
// Prevent editing settings that were modified in per-game config while editing global
// config
if (!NativeConfig.isPerGameConfigLoaded() && !setting.global) {
return false
}
return setting.isRuntimeModifiable
}
val needsRuntimeGlobal: Boolean
get() = NativeLibrary.isRunning() && !setting.global &&
!NativeConfig.isPerGameConfigLoaded()
companion object {
const val TYPE_HEADER = 0
const val TYPE_SWITCH = 1
@ -48,8 +64,9 @@ abstract class SettingsItem(
val emptySetting = object : AbstractSetting {
override val key: String = ""
override val category: Settings.Category = Settings.Category.Ui
override val defaultValue: Any = false
override val isSaveable = true
override fun getValueAsString(needsGlobal: Boolean): String = ""
override fun reset() {}
}
@ -226,6 +243,15 @@ abstract class SettingsItem(
R.string.renderer_reactive_flushing_description
)
)
put(
SingleChoiceSetting(
IntSetting.MAX_ANISOTROPY,
R.string.anisotropic_filtering,
R.string.anisotropic_filtering_description,
R.array.anisoEntries,
R.array.anisoValues
)
)
put(
SingleChoiceSetting(
IntSetting.AUDIO_OUTPUT_ENGINE,
@ -270,9 +296,9 @@ abstract class SettingsItem(
)
val fastmem = object : AbstractBooleanSetting {
override val boolean: Boolean
get() =
BooleanSetting.FASTMEM.boolean && BooleanSetting.FASTMEM_EXCLUSIVES.boolean
override fun getBoolean(needsGlobal: Boolean): Boolean =
BooleanSetting.FASTMEM.getBoolean() &&
BooleanSetting.FASTMEM_EXCLUSIVES.getBoolean()
override fun setBoolean(value: Boolean) {
BooleanSetting.FASTMEM.setBoolean(value)
@ -280,9 +306,25 @@ abstract class SettingsItem(
}
override val key: String = FASTMEM_COMBINED
override val category = Settings.Category.Cpu
override val isRuntimeModifiable: Boolean = false
override val pairedSettingKey = BooleanSetting.CPU_DEBUG_MODE.key
override val defaultValue: Boolean = true
override val isSwitchable: Boolean = true
override var global: Boolean
get() {
return BooleanSetting.FASTMEM.global &&
BooleanSetting.FASTMEM_EXCLUSIVES.global
}
set(value) {
BooleanSetting.FASTMEM.global = value
BooleanSetting.FASTMEM_EXCLUSIVES.global = value
}
override val isSaveable = true
override fun getValueAsString(needsGlobal: Boolean): String =
getBoolean().toString()
override fun reset() = setBoolean(defaultValue)
}
put(SwitchSetting(fastmem, R.string.fastmem, 0))

View File

@ -15,16 +15,11 @@ class SingleChoiceSetting(
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SINGLE_CHOICE
var selectedValue: Int
get() {
return when (setting) {
is AbstractIntSetting -> setting.int
else -> -1
}
}
set(value) {
when (setting) {
is AbstractIntSetting -> setting.setInt(value)
}
fun getSelectedValue(needsGlobal: Boolean = false) =
when (setting) {
is AbstractIntSetting -> setting.getInt(needsGlobal)
else -> -1
}
fun setSelectedValue(value: Int) = (setting as AbstractIntSetting).setInt(value)
}

View File

@ -20,22 +20,20 @@ class SliderSetting(
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SLIDER
var selectedValue: Int
get() {
return when (setting) {
is AbstractByteSetting -> setting.byte.toInt()
is AbstractShortSetting -> setting.short.toInt()
is AbstractIntSetting -> setting.int
is AbstractFloatSetting -> setting.float.roundToInt()
else -> -1
}
fun getSelectedValue(needsGlobal: Boolean = false) =
when (setting) {
is AbstractByteSetting -> setting.getByte(needsGlobal).toInt()
is AbstractShortSetting -> setting.getShort(needsGlobal).toInt()
is AbstractIntSetting -> setting.getInt(needsGlobal)
is AbstractFloatSetting -> setting.getFloat(needsGlobal).roundToInt()
else -> -1
}
set(value) {
when (setting) {
is AbstractByteSetting -> setting.setByte(value.toByte())
is AbstractShortSetting -> setting.setShort(value.toShort())
is AbstractIntSetting -> setting.setInt(value)
is AbstractFloatSetting -> setting.setFloat(value.toFloat())
}
fun setSelectedValue(value: Int) =
when (setting) {
is AbstractByteSetting -> setting.setByte(value.toByte())
is AbstractShortSetting -> setting.setShort(value.toShort())
is AbstractFloatSetting -> setting.setFloat(value.toFloat())
else -> (setting as AbstractIntSetting).setInt(value)
}
}

View File

@ -17,14 +17,13 @@ class StringSingleChoiceSetting(
fun getValueAt(index: Int): String =
if (index >= 0 && index < values.size) values[index] else ""
var selectedValue: String
get() = stringSetting.string
set(value) = stringSetting.setString(value)
fun getSelectedValue(needsGlobal: Boolean = false) = stringSetting.getString(needsGlobal)
fun setSelectedValue(value: String) = stringSetting.setString(value)
val selectValueIndex: Int
get() {
for (i in values.indices) {
if (values[i] == selectedValue) {
if (values[i] == getSelectedValue()) {
return i
}
}

View File

@ -14,18 +14,18 @@ class SwitchSetting(
) : SettingsItem(setting, titleId, descriptionId) {
override val type = TYPE_SWITCH
var checked: Boolean
get() {
return when (setting) {
is AbstractIntSetting -> setting.int == 1
is AbstractBooleanSetting -> setting.boolean
else -> false
}
fun getIsChecked(needsGlobal: Boolean = false): Boolean {
return when (setting) {
is AbstractIntSetting -> setting.getInt(needsGlobal) == 1
is AbstractBooleanSetting -> setting.getBoolean(needsGlobal)
else -> false
}
set(value) {
when (setting) {
is AbstractIntSetting -> setting.setInt(if (value) 1 else 0)
is AbstractBooleanSetting -> setting.setBoolean(value)
}
}
fun setChecked(value: Boolean) {
when (setting) {
is AbstractIntSetting -> setting.setInt(if (value) 1 else 0)
is AbstractBooleanSetting -> setting.setBoolean(value)
}
}
}

View File

@ -19,10 +19,9 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.navArgs
import com.google.android.material.color.MaterialColors
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.NativeLibrary
import java.io.IOException
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.ActivitySettingsBinding
@ -46,6 +45,9 @@ class SettingsActivity : AppCompatActivity() {
binding = ActivitySettingsBinding.inflate(layoutInflater)
setContentView(binding.root)
if (!NativeConfig.isPerGameConfigLoaded() && args.game != null) {
SettingsFile.loadCustomConfig(args.game!!)
}
settingsViewModel.game = args.game
val navHostFragment =
@ -126,7 +128,6 @@ class SettingsActivity : AppCompatActivity() {
override fun onStart() {
super.onStart()
// TODO: Load custom settings contextually
if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start()
}
@ -134,24 +135,35 @@ class SettingsActivity : AppCompatActivity() {
override fun onStop() {
super.onStop()
CoroutineScope(Dispatchers.IO).launch {
NativeConfig.saveSettings()
Log.info("[SettingsActivity] Settings activity stopping. Saving settings to INI...")
if (isFinishing) {
NativeLibrary.applySettings()
if (args.game == null) {
NativeConfig.saveGlobalConfig()
} else if (NativeConfig.isPerGameConfigLoaded()) {
NativeLibrary.logSettings()
NativeConfig.savePerGameConfig()
NativeConfig.unloadPerGameConfig()
}
}
}
override fun onDestroy() {
settingsViewModel.clear()
super.onDestroy()
}
fun onSettingsReset() {
// Delete settings file because the user may have changed values that do not exist in the UI
NativeConfig.unloadConfig()
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
if (!settingsFile.delete()) {
throw IOException("Failed to delete $settingsFile")
if (args.game == null) {
NativeConfig.unloadGlobalConfig()
val settingsFile = SettingsFile.getSettingsFile(SettingsFile.FILE_NAME_CONFIG)
if (!settingsFile.delete()) {
throw IOException("Failed to delete $settingsFile")
}
NativeConfig.initializeGlobalConfig()
} else {
NativeConfig.unloadPerGameConfig()
val settingsFile = SettingsFile.getCustomSettingsFile(args.game!!)
if (!settingsFile.delete()) {
throw IOException("Failed to delete $settingsFile")
}
}
NativeConfig.initializeConfig()
Toast.makeText(
applicationContext,

View File

@ -102,8 +102,9 @@ class SettingsAdapter(
return currentList[position].type
}
fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
item.checked = checked
fun onBooleanClick(item: SwitchSetting, checked: Boolean, position: Int) {
item.setChecked(checked)
notifyItemChanged(position)
settingsViewModel.setShouldReloadSettingsList(true)
}
@ -126,7 +127,7 @@ class SettingsAdapter(
}
fun onDateTimeClick(item: DateTimeSetting, position: Int) {
val storedTime = item.value * 1000
val storedTime = item.getValue() * 1000
// Helper to extract hour and minute from epoch time
val calendar: Calendar = Calendar.getInstance()
@ -159,9 +160,9 @@ class SettingsAdapter(
var epochTime: Long = datePicker.selection!! / 1000
epochTime += timePicker.hour.toLong() * 60 * 60
epochTime += timePicker.minute.toLong() * 60
if (item.value != epochTime) {
if (item.getValue() != epochTime) {
notifyItemChanged(position)
item.value = epochTime
item.setValue(epochTime)
}
}
datePicker.show(
@ -195,6 +196,12 @@ class SettingsAdapter(
return true
}
fun onClearClick(item: SettingsItem, position: Int) {
item.setting.global = true
notifyItemChanged(position)
settingsViewModel.setShouldReloadSettingsList(true)
}
private class DiffCallback : DiffUtil.ItemCallback<SettingsItem>() {
override fun areItemsTheSame(oldItem: SettingsItem, newItem: SettingsItem): Boolean {
return oldItem.setting.key == newItem.setting.key

View File

@ -66,7 +66,13 @@ class SettingsFragment : Fragment() {
args.menuTag
)
binding.toolbarSettingsLayout.title = getString(args.menuTag.titleId)
binding.toolbarSettingsLayout.title = if (args.menuTag == Settings.MenuTag.SECTION_ROOT &&
args.game != null
) {
args.game!!.title
} else {
getString(args.menuTag.titleId)
}
binding.listSettings.apply {
adapter = settingsAdapter
layoutManager = LinearLayoutManager(requireContext())

View File

@ -3,10 +3,9 @@
package org.yuzu.yuzu_emu.features.settings.ui
import android.content.SharedPreferences
import android.os.Build
import android.widget.Toast
import androidx.preference.PreferenceManager
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.AbstractBooleanSetting
@ -28,15 +27,27 @@ class SettingsFragmentPresenter(
) {
private var settingsList = ArrayList<SettingsItem>()
private val preferences: SharedPreferences
get() = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
// Extension for populating settings list based on paired settings
// Extension for altering settings list based on each setting's properties
fun ArrayList<SettingsItem>.add(key: String) {
val item = SettingsItem.settingsItems[key]!!
if (settingsViewModel.game != null && !item.setting.isSwitchable) {
return
}
if (!NativeConfig.isPerGameConfigLoaded() && !NativeLibrary.isRunning()) {
item.setting.global = true
}
val pairedSettingKey = item.setting.pairedSettingKey
if (pairedSettingKey.isNotEmpty()) {
val pairedSettingValue = NativeConfig.getBoolean(pairedSettingKey, false)
val pairedSettingValue = NativeConfig.getBoolean(
pairedSettingKey,
if (NativeLibrary.isRunning() && !NativeConfig.isPerGameConfigLoaded()) {
!NativeConfig.usingGlobal(pairedSettingKey)
} else {
NativeConfig.usingGlobal(pairedSettingKey)
}
)
if (!pairedSettingValue) return
}
add(item)
@ -133,6 +144,7 @@ class SettingsFragmentPresenter(
add(IntSetting.RENDERER_VSYNC.key)
add(IntSetting.RENDERER_SCALING_FILTER.key)
add(IntSetting.RENDERER_ANTI_ALIASING.key)
add(IntSetting.MAX_ANISOTROPY.key)
add(IntSetting.RENDERER_SCREEN_LAYOUT.key)
add(IntSetting.RENDERER_ASPECT_RATIO.key)
add(BooleanSetting.PICTURE_IN_PICTURE.key)
@ -153,25 +165,19 @@ class SettingsFragmentPresenter(
private fun addThemeSettings(sl: ArrayList<SettingsItem>) {
sl.apply {
val theme: AbstractIntSetting = object : AbstractIntSetting {
override val int: Int
get() = preferences.getInt(Settings.PREF_THEME, 0)
override fun getInt(needsGlobal: Boolean): Int = IntSetting.THEME.getInt()
override fun setInt(value: Int) {
preferences.edit()
.putInt(Settings.PREF_THEME, value)
.apply()
IntSetting.THEME.setInt(value)
settingsViewModel.setShouldRecreate(true)
}
override val key: String = Settings.PREF_THEME
override val category = Settings.Category.UiGeneral
override val isRuntimeModifiable: Boolean = false
override val defaultValue: Int = 0
override fun reset() {
preferences.edit()
.putInt(Settings.PREF_THEME, defaultValue)
.apply()
}
override val key: String = IntSetting.THEME.key
override val isRuntimeModifiable: Boolean = IntSetting.THEME.isRuntimeModifiable
override fun getValueAsString(needsGlobal: Boolean): String =
IntSetting.THEME.getValueAsString()
override val defaultValue: Int = IntSetting.THEME.defaultValue
override fun reset() = IntSetting.THEME.setInt(defaultValue)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -197,24 +203,22 @@ class SettingsFragmentPresenter(
}
val themeMode: AbstractIntSetting = object : AbstractIntSetting {
override val int: Int
get() = preferences.getInt(Settings.PREF_THEME_MODE, -1)
override fun getInt(needsGlobal: Boolean): Int = IntSetting.THEME_MODE.getInt()
override fun setInt(value: Int) {
preferences.edit()
.putInt(Settings.PREF_THEME_MODE, value)
.apply()
IntSetting.THEME_MODE.setInt(value)
settingsViewModel.setShouldRecreate(true)
}
override val key: String = Settings.PREF_THEME_MODE
override val category = Settings.Category.UiGeneral
override val isRuntimeModifiable: Boolean = false
override val defaultValue: Int = -1
override val key: String = IntSetting.THEME_MODE.key
override val isRuntimeModifiable: Boolean =
IntSetting.THEME_MODE.isRuntimeModifiable
override fun getValueAsString(needsGlobal: Boolean): String =
IntSetting.THEME_MODE.getValueAsString()
override val defaultValue: Int = IntSetting.THEME_MODE.defaultValue
override fun reset() {
preferences.edit()
.putInt(Settings.PREF_BLACK_BACKGROUNDS, defaultValue)
.apply()
IntSetting.THEME_MODE.setInt(defaultValue)
settingsViewModel.setShouldRecreate(true)
}
}
@ -230,24 +234,25 @@ class SettingsFragmentPresenter(
)
val blackBackgrounds: AbstractBooleanSetting = object : AbstractBooleanSetting {
override val boolean: Boolean
get() = preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false)
override fun getBoolean(needsGlobal: Boolean): Boolean =
BooleanSetting.BLACK_BACKGROUNDS.getBoolean()
override fun setBoolean(value: Boolean) {
preferences.edit()
.putBoolean(Settings.PREF_BLACK_BACKGROUNDS, value)
.apply()
BooleanSetting.BLACK_BACKGROUNDS.setBoolean(value)
settingsViewModel.setShouldRecreate(true)
}
override val key: String = Settings.PREF_BLACK_BACKGROUNDS
override val category = Settings.Category.UiGeneral
override val isRuntimeModifiable: Boolean = false
override val defaultValue: Boolean = false
override val key: String = BooleanSetting.BLACK_BACKGROUNDS.key
override val isRuntimeModifiable: Boolean =
BooleanSetting.BLACK_BACKGROUNDS.isRuntimeModifiable
override fun getValueAsString(needsGlobal: Boolean): String =
BooleanSetting.BLACK_BACKGROUNDS.getValueAsString()
override val defaultValue: Boolean = BooleanSetting.BLACK_BACKGROUNDS.defaultValue
override fun reset() {
preferences.edit()
.putBoolean(Settings.PREF_BLACK_BACKGROUNDS, defaultValue)
.apply()
BooleanSetting.BLACK_BACKGROUNDS
.setBoolean(BooleanSetting.BLACK_BACKGROUNDS.defaultValue)
settingsViewModel.setShouldRecreate(true)
}
}

View File

@ -13,6 +13,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.features.settings.model.view.DateTimeSetting
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.NativeConfig
class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
@ -29,12 +30,23 @@ class DateTimeViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
}
binding.textSettingValue.visibility = View.VISIBLE
val epochTime = setting.value
val epochTime = setting.getValue()
val instant = Instant.ofEpochMilli(epochTime * 1000)
val zonedTime = ZonedDateTime.ofInstant(instant, ZoneId.of("UTC"))
val dateFormatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)
binding.textSettingValue.text = dateFormatter.format(zonedTime)
binding.buttonClear.visibility = if (setting.setting.global ||
!NativeConfig.isPerGameConfigLoaded()
) {
View.GONE
} else {
View.VISIBLE
}
binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition)
}
setStyle(setting.isEditable, binding)
}

View File

@ -38,6 +38,7 @@ class RunnableViewHolder(val binding: ListItemSettingBinding, adapter: SettingsA
binding.textSettingDescription.visibility = View.GONE
}
binding.textSettingValue.visibility = View.GONE
binding.buttonClear.visibility = View.GONE
setStyle(setting.isEditable, binding)
}

View File

@ -41,6 +41,7 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
binding.textSettingName.alpha = opacity
binding.textSettingDescription.alpha = opacity
binding.textSettingValue.alpha = opacity
binding.buttonClear.isEnabled = isEditable
}
fun setStyle(isEditable: Boolean, binding: ListItemSettingSwitchBinding) {
@ -48,5 +49,6 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
val opacity = if (isEditable) 1.0f else 0.5f
binding.textSettingName.alpha = opacity
binding.textSettingDescription.alpha = opacity
binding.buttonClear.isEnabled = isEditable
}
}

View File

@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.model.view.SingleChoiceSetting
import org.yuzu.yuzu_emu.features.settings.model.view.StringSingleChoiceSetting
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.NativeConfig
class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
@ -29,20 +30,31 @@ class SingleChoiceViewHolder(val binding: ListItemSettingBinding, adapter: Setti
val resMgr = binding.textSettingValue.context.resources
val values = resMgr.getIntArray(item.valuesId)
for (i in values.indices) {
if (values[i] == item.selectedValue) {
if (values[i] == item.getSelectedValue()) {
binding.textSettingValue.text = resMgr.getStringArray(item.choicesId)[i]
break
}
}
} else if (item is StringSingleChoiceSetting) {
for (i in item.values.indices) {
if (item.values[i] == item.selectedValue) {
if (item.values[i] == item.getSelectedValue()) {
binding.textSettingValue.text = item.choices[i]
break
}
}
}
binding.buttonClear.visibility = if (setting.setting.global ||
!NativeConfig.isPerGameConfigLoaded()
) {
View.GONE
} else {
View.VISIBLE
}
binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition)
}
setStyle(setting.isEditable, binding)
}

View File

@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingBinding
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.model.view.SliderSetting
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.NativeConfig
class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
@ -26,10 +27,21 @@ class SliderViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAda
binding.textSettingValue.visibility = View.VISIBLE
binding.textSettingValue.text = String.format(
binding.textSettingValue.context.getString(R.string.value_with_units),
setting.selectedValue,
setting.getSelectedValue(),
setting.units
)
binding.buttonClear.visibility = if (setting.setting.global ||
!NativeConfig.isPerGameConfigLoaded()
) {
View.GONE
} else {
View.VISIBLE
}
binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition)
}
setStyle(setting.isEditable, binding)
}

View File

@ -37,6 +37,7 @@ class SubmenuViewHolder(val binding: ListItemSettingBinding, adapter: SettingsAd
binding.textSettingDescription.visibility = View.GONE
}
binding.textSettingValue.visibility = View.GONE
binding.buttonClear.visibility = View.GONE
}
override fun onClick(clicked: View) {

View File

@ -9,6 +9,7 @@ import org.yuzu.yuzu_emu.databinding.ListItemSettingSwitchBinding
import org.yuzu.yuzu_emu.features.settings.model.view.SettingsItem
import org.yuzu.yuzu_emu.features.settings.model.view.SwitchSetting
import org.yuzu.yuzu_emu.features.settings.ui.SettingsAdapter
import org.yuzu.yuzu_emu.utils.NativeConfig
class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter: SettingsAdapter) :
SettingViewHolder(binding.root, adapter) {
@ -27,9 +28,20 @@ class SwitchSettingViewHolder(val binding: ListItemSettingSwitchBinding, adapter
}
binding.switchWidget.setOnCheckedChangeListener(null)
binding.switchWidget.isChecked = setting.checked
binding.switchWidget.isChecked = setting.getIsChecked(setting.needsRuntimeGlobal)
binding.switchWidget.setOnCheckedChangeListener { _: CompoundButton, _: Boolean ->
adapter.onBooleanClick(item, binding.switchWidget.isChecked)
adapter.onBooleanClick(item, binding.switchWidget.isChecked, bindingAdapterPosition)
}
binding.buttonClear.visibility = if (setting.setting.global ||
!NativeConfig.isPerGameConfigLoaded()
) {
View.GONE
} else {
View.VISIBLE
}
binding.buttonClear.setOnClickListener {
adapter.onClearClick(setting, bindingAdapterPosition)
}
setStyle(setting.isEditable, binding)

View File

@ -3,15 +3,27 @@
package org.yuzu.yuzu_emu.features.settings.utils
import android.net.Uri
import org.yuzu.yuzu_emu.model.Game
import java.io.*
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.NativeConfig
/**
* Contains static methods for interacting with .ini files in which settings are stored.
*/
object SettingsFile {
const val FILE_NAME_CONFIG = "config"
const val FILE_NAME_CONFIG = "config.ini"
fun getSettingsFile(fileName: String): File =
File(DirectoryInitialization.userDirectory + "/config/" + fileName + ".ini")
File(DirectoryInitialization.userDirectory + "/config/" + fileName)
fun getCustomSettingsFile(game: Game): File =
File(DirectoryInitialization.userDirectory + "/config/custom/" + game.settingsName + ".ini")
fun loadCustomConfig(game: Game) {
val fileName = FileUtil.getFilename(Uri.parse(game.path))
NativeConfig.initializePerGameConfig(game.programId, fileName)
}
}

View File

@ -14,8 +14,10 @@ import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogAddFolderBinding
import org.yuzu.yuzu_emu.model.GameDir
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
class AddGameFolderDialogFragment : DialogFragment() {
private val homeViewModel: HomeViewModel by activityViewModels()
private val gamesViewModel: GamesViewModel by activityViewModels()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -30,6 +32,7 @@ class AddGameFolderDialogFragment : DialogFragment() {
.setTitle(R.string.add_game_folder)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
val newGameDir = GameDir(folderUriString!!, binding.deepScanSwitch.isChecked)
homeViewModel.setGamesDirSelected(true)
gamesViewModel.addFolder(newGameDir)
}
.setNegativeButton(android.R.string.cancel, null)

View File

@ -0,0 +1,214 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import androidx.documentfile.provider.DocumentFile
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.recyclerview.widget.LinearLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.AddonAdapter
import org.yuzu.yuzu_emu.databinding.FragmentAddonsBinding
import org.yuzu.yuzu_emu.model.AddonViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.utils.AddonUtil
import org.yuzu.yuzu_emu.utils.FileUtil.copyFilesTo
import java.io.File
class AddonsFragment : Fragment() {
private var _binding: FragmentAddonsBinding? = null
private val binding get() = _binding!!
private val homeViewModel: HomeViewModel by activityViewModels()
private val addonViewModel: AddonViewModel by activityViewModels()
private val args by navArgs<AddonsFragmentArgs>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
addonViewModel.onOpenAddons(args.game)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentAddonsBinding.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 = false)
homeViewModel.setStatusBarShadeVisibility(false)
binding.toolbarAddons.setNavigationOnClickListener {
binding.root.findNavController().popBackStack()
}
binding.toolbarAddons.title = getString(R.string.addons_game, args.game.title)
binding.listAddons.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = AddonAdapter()
}
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
addonViewModel.addonList.collect {
(binding.listAddons.adapter as AddonAdapter).submitList(it)
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
addonViewModel.showModInstallPicker.collect {
if (it) {
installAddon.launch(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).data)
addonViewModel.showModInstallPicker(false)
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
addonViewModel.showModNoticeDialog.collect {
if (it) {
MessageDialogFragment.newInstance(
requireActivity(),
titleId = R.string.addon_notice,
descriptionId = R.string.addon_notice_description,
positiveAction = { addonViewModel.showModInstallPicker(true) }
).show(parentFragmentManager, MessageDialogFragment.TAG)
addonViewModel.showModNoticeDialog(false)
}
}
}
}
}
binding.buttonInstall.setOnClickListener {
ContentTypeSelectionDialogFragment().show(
parentFragmentManager,
ContentTypeSelectionDialogFragment.TAG
)
}
setInsets()
}
override fun onResume() {
super.onResume()
addonViewModel.refreshAddons()
}
override fun onDestroy() {
super.onDestroy()
addonViewModel.onCloseAddons()
}
val installAddon =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result == null) {
return@registerForActivityResult
}
val externalAddonDirectory = DocumentFile.fromTreeUri(requireContext(), result)
if (externalAddonDirectory == null) {
MessageDialogFragment.newInstance(
requireActivity(),
titleId = R.string.invalid_directory,
descriptionId = R.string.invalid_directory_description
).show(parentFragmentManager, MessageDialogFragment.TAG)
return@registerForActivityResult
}
val isValid = externalAddonDirectory.listFiles()
.any { AddonUtil.validAddonDirectories.contains(it.name) }
val errorMessage = MessageDialogFragment.newInstance(
requireActivity(),
titleId = R.string.invalid_directory,
descriptionId = R.string.invalid_directory_description
)
if (isValid) {
IndeterminateProgressDialogFragment.newInstance(
requireActivity(),
R.string.installing_game_content,
false
) {
val parentDirectoryName = externalAddonDirectory.name
val internalAddonDirectory =
File(args.game.addonDir + parentDirectoryName)
try {
externalAddonDirectory.copyFilesTo(internalAddonDirectory)
} catch (_: Exception) {
return@newInstance errorMessage
}
addonViewModel.refreshAddons()
return@newInstance getString(R.string.addon_installed_successfully)
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
} else {
errorMessage.show(parentFragmentManager, MessageDialogFragment.TAG)
}
}
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 mlpToolbar = binding.toolbarAddons.layoutParams as ViewGroup.MarginLayoutParams
mlpToolbar.leftMargin = leftInsets
mlpToolbar.rightMargin = rightInsets
binding.toolbarAddons.layoutParams = mlpToolbar
val mlpAddonsList = binding.listAddons.layoutParams as ViewGroup.MarginLayoutParams
mlpAddonsList.leftMargin = leftInsets
mlpAddonsList.rightMargin = rightInsets
binding.listAddons.layoutParams = mlpAddonsList
binding.listAddons.updatePadding(
bottom = barInsets.bottom +
resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
)
val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
val mlpFab =
binding.buttonInstall.layoutParams as ViewGroup.MarginLayoutParams
mlpFab.leftMargin = leftInsets + fabSpacing
mlpFab.rightMargin = rightInsets + fabSpacing
mlpFab.bottomMargin = barInsets.bottom + fabSpacing
binding.buttonInstall.layoutParams = mlpFab
windowInsets
}
}

View File

@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import androidx.preference.PreferenceManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.model.AddonViewModel
import org.yuzu.yuzu_emu.ui.main.MainActivity
class ContentTypeSelectionDialogFragment : DialogFragment() {
private val addonViewModel: AddonViewModel by activityViewModels()
private val preferences get() =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
private var selectedItem = 0
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val launchOptions =
arrayOf(getString(R.string.updates_and_dlc), getString(R.string.mods_and_cheats))
if (savedInstanceState != null) {
selectedItem = savedInstanceState.getInt(SELECTED_ITEM)
}
val mainActivity = requireActivity() as MainActivity
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.select_content_type)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
when (selectedItem) {
0 -> mainActivity.installGameUpdate.launch(arrayOf("*/*"))
else -> {
if (!preferences.getBoolean(MOD_NOTICE_SEEN, false)) {
preferences.edit().putBoolean(MOD_NOTICE_SEEN, true).apply()
addonViewModel.showModNoticeDialog(true)
return@setPositiveButton
}
addonViewModel.showModInstallPicker(true)
}
}
}
.setSingleChoiceItems(launchOptions, 0) { _: DialogInterface, i: Int ->
selectedItem = i
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(SELECTED_ITEM, selectedItem)
}
companion object {
const val TAG = "ContentTypeSelectionDialogFragment"
private const val SELECTED_ITEM = "SelectedItem"
private const val MOD_NOTICE_SEEN = "ModNoticeSeen"
}
}

View File

@ -15,6 +15,7 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
import kotlinx.coroutines.flow.collectLatest
@ -36,6 +37,8 @@ class DriverManagerFragment : Fragment() {
private val homeViewModel: HomeViewModel by activityViewModels()
private val driverViewModel: DriverViewModel by activityViewModels()
private val args by navArgs<DriverManagerFragmentArgs>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
@ -57,7 +60,9 @@ class DriverManagerFragment : Fragment() {
homeViewModel.setNavigationVisibility(visible = false, animated = true)
homeViewModel.setStatusBarShadeVisibility(visible = false)
if (!driverViewModel.isInteractionAllowed) {
driverViewModel.onOpenDriverManager(args.game)
if (!driverViewModel.isInteractionAllowed.value) {
DriversLoadingDialogFragment().show(
childFragmentManager,
DriversLoadingDialogFragment.TAG
@ -102,10 +107,9 @@ class DriverManagerFragment : Fragment() {
setInsets()
}
// Start installing requested driver
override fun onStop() {
super.onStop()
driverViewModel.onCloseDriverManager()
override fun onDestroy() {
super.onDestroy()
driverViewModel.onCloseDriverManager(args.game)
}
private fun setInsets() =

View File

@ -47,25 +47,9 @@ class DriversLoadingDialogFragment : DialogFragment() {
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
driverViewModel.areDriversLoading.collect { checkForDismiss() }
driverViewModel.isInteractionAllowed.collect { if (it) dismiss() }
}
}
launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
driverViewModel.isDriverReady.collect { checkForDismiss() }
}
}
launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
driverViewModel.isDeletingDrivers.collect { checkForDismiss() }
}
}
}
}
private fun checkForDismiss() {
if (driverViewModel.isInteractionAllowed) {
dismiss()
}
}

View File

@ -7,7 +7,6 @@ import android.annotation.SuppressLint
import android.app.AlertDialog
import android.content.Context
import android.content.DialogInterface
import android.content.SharedPreferences
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.net.Uri
@ -33,7 +32,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.findNavController
import androidx.navigation.fragment.navArgs
import androidx.preference.PreferenceManager
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
@ -46,21 +44,22 @@ import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.activities.EmulationActivity
import org.yuzu.yuzu_emu.databinding.DialogOverlayAdjustBinding
import org.yuzu.yuzu_emu.databinding.FragmentEmulationBinding
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.model.EmulationViewModel
import org.yuzu.yuzu_emu.overlay.InputOverlay
import org.yuzu.yuzu_emu.overlay.model.OverlayControl
import org.yuzu.yuzu_emu.overlay.model.OverlayLayout
import org.yuzu.yuzu_emu.utils.*
import java.lang.NullPointerException
class EmulationFragment : Fragment(), SurfaceHolder.Callback {
private lateinit var preferences: SharedPreferences
private lateinit var emulationState: EmulationState
private var emulationActivity: EmulationActivity? = null
private var perfStatsUpdater: (() -> Unit)? = null
@ -127,9 +126,19 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
return
}
// Always load custom settings when launching a game from an intent
if (args.custom || intentGame != null) {
SettingsFile.loadCustomConfig(game)
NativeConfig.unloadPerGameConfig()
} else {
NativeConfig.reloadGlobalConfig()
}
// Install the selected driver asynchronously as the game starts
driverViewModel.onLaunchGame()
// So this fragment doesn't restart on configuration changes; i.e. rotation.
retainInstance = true
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
emulationState = EmulationState(game.path)
}
@ -217,6 +226,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
true
}
R.id.menu_settings_per_game -> {
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
args.game,
Settings.MenuTag.SECTION_ROOT
)
binding.root.findNavController().navigate(action)
true
}
R.id.menu_overlay_controls -> {
showOverlayOptions()
true
@ -332,15 +350,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
driverViewModel.isDriverReady.collect {
if (it && !emulationState.isRunning) {
if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start()
}
updateScreenLayout()
emulationState.run(emulationActivity!!.isActivityRecreated)
driverViewModel.isInteractionAllowed.collect {
if (it) {
onEmulationStart()
}
}
}
@ -348,6 +360,18 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
}
private fun onEmulationStart() {
if (!NativeLibrary.isRunning() && !NativeLibrary.isPaused()) {
if (!DirectoryInitialization.areDirectoriesReady) {
DirectoryInitialization.start()
}
updateScreenLayout()
emulationState.run(emulationActivity!!.isActivityRecreated)
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (_binding == null) {
@ -355,24 +379,25 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
updateScreenLayout()
val showInputOverlay = BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean()
if (emulationActivity?.isInPictureInPictureMode == true) {
if (binding.drawerLayout.isOpen) {
binding.drawerLayout.close()
}
if (EmulationMenuSettings.showOverlay) {
if (showInputOverlay) {
binding.surfaceInputOverlay.visibility = View.INVISIBLE
}
} else {
if (EmulationMenuSettings.showOverlay && emulationViewModel.emulationStarted.value) {
if (showInputOverlay && emulationViewModel.emulationStarted.value) {
binding.surfaceInputOverlay.visibility = View.VISIBLE
} else {
binding.surfaceInputOverlay.visibility = View.INVISIBLE
}
if (!isInFoldableLayout) {
if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
binding.surfaceInputOverlay.layout = InputOverlay.PORTRAIT
binding.surfaceInputOverlay.layout = OverlayLayout.Portrait
} else {
binding.surfaceInputOverlay.layout = InputOverlay.LANDSCAPE
binding.surfaceInputOverlay.layout = OverlayLayout.Landscape
}
}
}
@ -396,17 +421,15 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
private fun resetInputOverlay() {
preferences.edit()
.remove(Settings.PREF_CONTROL_SCALE)
.remove(Settings.PREF_CONTROL_OPACITY)
.apply()
IntSetting.OVERLAY_SCALE.reset()
IntSetting.OVERLAY_OPACITY.reset()
binding.surfaceInputOverlay.post {
binding.surfaceInputOverlay.resetLayoutVisibilityAndPlacement()
}
}
private fun updateShowFpsOverlay() {
if (EmulationMenuSettings.showFps) {
if (BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean()) {
val SYSTEM_FPS = 0
val FPS = 1
val FRAMETIME = 2
@ -435,7 +458,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@SuppressLint("SourceLockedOrientationActivity")
private fun updateOrientation() {
emulationActivity?.let {
it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.int) {
it.requestedOrientation = when (IntSetting.RENDERER_SCREEN_LAYOUT.getInt()) {
Settings.LayoutOption_MobileLandscape ->
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
Settings.LayoutOption_MobilePortrait ->
@ -469,7 +492,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
binding.inGameMenu.layoutParams.height = it.bounds.bottom
isInFoldableLayout = true
binding.surfaceInputOverlay.layout = InputOverlay.FOLDABLE
binding.surfaceInputOverlay.layout = OverlayLayout.Foldable
}
}
it.isSeparating
@ -508,18 +531,22 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
popup.menuInflater.inflate(R.menu.menu_overlay_options, popup.menu)
popup.menu.apply {
findItem(R.id.menu_toggle_fps).isChecked = EmulationMenuSettings.showFps
findItem(R.id.menu_rel_stick_center).isChecked = EmulationMenuSettings.joystickRelCenter
findItem(R.id.menu_dpad_slide).isChecked = EmulationMenuSettings.dpadSlide
findItem(R.id.menu_show_overlay).isChecked = EmulationMenuSettings.showOverlay
findItem(R.id.menu_haptics).isChecked = EmulationMenuSettings.hapticFeedback
findItem(R.id.menu_toggle_fps).isChecked =
BooleanSetting.SHOW_PERFORMANCE_OVERLAY.getBoolean()
findItem(R.id.menu_rel_stick_center).isChecked =
BooleanSetting.JOYSTICK_REL_CENTER.getBoolean()
findItem(R.id.menu_dpad_slide).isChecked = BooleanSetting.DPAD_SLIDE.getBoolean()
findItem(R.id.menu_show_overlay).isChecked =
BooleanSetting.SHOW_INPUT_OVERLAY.getBoolean()
findItem(R.id.menu_haptics).isChecked = BooleanSetting.HAPTIC_FEEDBACK.getBoolean()
findItem(R.id.menu_touchscreen).isChecked = BooleanSetting.TOUCHSCREEN.getBoolean()
}
popup.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_toggle_fps -> {
it.isChecked = !it.isChecked
EmulationMenuSettings.showFps = it.isChecked
BooleanSetting.SHOW_PERFORMANCE_OVERLAY.setBoolean(it.isChecked)
updateShowFpsOverlay()
true
}
@ -537,11 +564,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
R.id.menu_toggle_controls -> {
val preferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
val optionsArray = BooleanArray(Settings.overlayPreferences.size)
Settings.overlayPreferences.forEachIndexed { i, _ ->
optionsArray[i] = preferences.getBoolean("buttonToggle$i", i < 15)
val overlayControlData = NativeConfig.getOverlayControlData()
val optionsArray = BooleanArray(overlayControlData.size)
overlayControlData.forEachIndexed { i, _ ->
optionsArray[i] = overlayControlData.firstOrNull { data ->
OverlayControl.entries[i].id == data.id
}?.enabled == true
}
val dialog = MaterialAlertDialogBuilder(requireContext())
@ -550,11 +578,13 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
R.array.gamepadButtons,
optionsArray
) { _, indexSelected, isChecked ->
preferences.edit()
.putBoolean("buttonToggle$indexSelected", isChecked)
.apply()
overlayControlData.firstOrNull { data ->
OverlayControl.entries[indexSelected].id == data.id
}?.enabled = isChecked
}
.setPositiveButton(android.R.string.ok) { _, _ ->
NativeConfig.setOverlayControlData(overlayControlData)
NativeConfig.saveGlobalConfig()
binding.surfaceInputOverlay.refreshControls()
}
.setNegativeButton(android.R.string.cancel, null)
@ -565,12 +595,10 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
.setOnClickListener {
val isChecked = !optionsArray[0]
Settings.overlayPreferences.forEachIndexed { i, _ ->
overlayControlData.forEachIndexed { i, _ ->
optionsArray[i] = isChecked
dialog.listView.setItemChecked(i, isChecked)
preferences.edit()
.putBoolean("buttonToggle$i", isChecked)
.apply()
overlayControlData[i].enabled = isChecked
}
}
true
@ -578,26 +606,32 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
R.id.menu_show_overlay -> {
it.isChecked = !it.isChecked
EmulationMenuSettings.showOverlay = it.isChecked
BooleanSetting.SHOW_INPUT_OVERLAY.setBoolean(it.isChecked)
binding.surfaceInputOverlay.refreshControls()
true
}
R.id.menu_rel_stick_center -> {
it.isChecked = !it.isChecked
EmulationMenuSettings.joystickRelCenter = it.isChecked
BooleanSetting.JOYSTICK_REL_CENTER.setBoolean(it.isChecked)
true
}
R.id.menu_dpad_slide -> {
it.isChecked = !it.isChecked
EmulationMenuSettings.dpadSlide = it.isChecked
BooleanSetting.DPAD_SLIDE.setBoolean(it.isChecked)
true
}
R.id.menu_haptics -> {
it.isChecked = !it.isChecked
EmulationMenuSettings.hapticFeedback = it.isChecked
BooleanSetting.HAPTIC_FEEDBACK.setBoolean(it.isChecked)
true
}
R.id.menu_touchscreen -> {
it.isChecked = !it.isChecked
BooleanSetting.TOUCHSCREEN.setBoolean(it.isChecked)
true
}
@ -617,7 +651,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
@SuppressLint("SourceLockedOrientationActivity")
private fun startConfiguringControls() {
// Lock the current orientation to prevent editing inconsistencies
if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) {
emulationActivity?.let {
it.requestedOrientation =
if (resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT) {
@ -635,11 +669,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
binding.doneControlConfig.visibility = View.GONE
binding.surfaceInputOverlay.setIsInEditMode(false)
// Unlock the orientation if it was locked for editing
if (IntSetting.RENDERER_SCREEN_LAYOUT.int == Settings.LayoutOption_Unspecified) {
if (IntSetting.RENDERER_SCREEN_LAYOUT.getInt() == Settings.LayoutOption_Unspecified) {
emulationActivity?.let {
it.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}
NativeConfig.saveGlobalConfig()
}
@SuppressLint("SetTextI18n")
@ -648,7 +683,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
adjustBinding.apply {
inputScaleSlider.apply {
valueTo = 150F
value = preferences.getInt(Settings.PREF_CONTROL_SCALE, 50).toFloat()
value = IntSetting.OVERLAY_SCALE.getInt().toFloat()
addOnChangeListener(
Slider.OnChangeListener { _, value, _ ->
inputScaleValue.text = "${value.toInt()}%"
@ -658,7 +693,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
inputOpacitySlider.apply {
valueTo = 100F
value = preferences.getInt(Settings.PREF_CONTROL_OPACITY, 100).toFloat()
value = IntSetting.OVERLAY_OPACITY.getInt().toFloat()
addOnChangeListener(
Slider.OnChangeListener { _, value, _ ->
inputOpacityValue.text = "${value.toInt()}%"
@ -682,16 +717,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback {
}
private fun setControlScale(scale: Int) {
preferences.edit()
.putInt(Settings.PREF_CONTROL_SCALE, scale)
.apply()
IntSetting.OVERLAY_SCALE.setInt(scale)
binding.surfaceInputOverlay.refreshControls()
}
private fun setControlOpacity(opacity: Int) {
preferences.edit()
.putInt(Settings.PREF_CONTROL_OPACITY, opacity)
.apply()
IntSetting.OVERLAY_OPACITY.setInt(opacity)
binding.surfaceInputOverlay.refreshControls()
}

View File

@ -13,6 +13,7 @@ import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.DialogFolderPropertiesBinding
import org.yuzu.yuzu_emu.model.GameDir
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.utils.NativeConfig
import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
class GameFolderPropertiesDialogFragment : DialogFragment() {
@ -49,6 +50,11 @@ class GameFolderPropertiesDialogFragment : DialogFragment() {
.show()
}
override fun onStop() {
super.onStop()
NativeConfig.saveGlobalConfig()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(DEEP_SCAN, deepScan)

View File

@ -0,0 +1,148 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
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.navigation.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.transition.MaterialSharedAxis
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.databinding.FragmentGameInfoBinding
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.utils.GameMetadata
class GameInfoFragment : Fragment() {
private var _binding: FragmentGameInfoBinding? = null
private val binding get() = _binding!!
private val homeViewModel: HomeViewModel by activityViewModels()
private val args by navArgs<GameInfoFragmentArgs>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
// Check for an up-to-date version string
args.game.version = GameMetadata.getVersion(args.game.path, true)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentGameInfoBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = false, animated = false)
homeViewModel.setStatusBarShadeVisibility(false)
binding.apply {
toolbarInfo.title = args.game.title
toolbarInfo.setNavigationOnClickListener {
view.findNavController().popBackStack()
}
val pathString = Uri.parse(args.game.path).path ?: ""
path.setHint(R.string.path)
pathField.setText(pathString)
pathField.setOnClickListener { copyToClipboard(getString(R.string.path), pathString) }
programId.setHint(R.string.program_id)
programIdField.setText(args.game.programIdHex)
programIdField.setOnClickListener {
copyToClipboard(getString(R.string.program_id), args.game.programIdHex)
}
if (args.game.developer.isNotEmpty()) {
developer.setHint(R.string.developer)
developerField.setText(args.game.developer)
developerField.setOnClickListener {
copyToClipboard(getString(R.string.developer), args.game.developer)
}
} else {
developer.visibility = View.GONE
}
version.setHint(R.string.version)
versionField.setText(args.game.version)
versionField.setOnClickListener {
copyToClipboard(getString(R.string.version), args.game.version)
}
buttonCopy.setOnClickListener {
val details = """
${args.game.title}
${getString(R.string.path)} - $pathString
${getString(R.string.program_id)} - ${args.game.programIdHex}
${getString(R.string.developer)} - ${args.game.developer}
${getString(R.string.version)} - ${args.game.version}
""".trimIndent()
copyToClipboard(args.game.title, details)
}
}
setInsets()
}
private fun copyToClipboard(label: String, body: String) {
val clipBoard =
requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(label, body)
clipBoard.setPrimaryClip(clip)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
Toast.makeText(
requireContext(),
R.string.copied_to_clipboard,
Toast.LENGTH_SHORT
).show()
}
}
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 mlpToolbar = binding.toolbarInfo.layoutParams as ViewGroup.MarginLayoutParams
mlpToolbar.leftMargin = leftInsets
mlpToolbar.rightMargin = rightInsets
binding.toolbarInfo.layoutParams = mlpToolbar
val mlpScrollAbout = binding.scrollInfo.layoutParams as ViewGroup.MarginLayoutParams
mlpScrollAbout.leftMargin = leftInsets
mlpScrollAbout.rightMargin = rightInsets
binding.scrollInfo.layoutParams = mlpScrollAbout
binding.contentInfo.updatePadding(bottom = barInsets.bottom)
windowInsets
}
}

View File

@ -0,0 +1,456 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.annotation.SuppressLint
import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
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.navigation.fragment.navArgs
import androidx.recyclerview.widget.GridLayoutManager
import com.google.android.material.transition.MaterialSharedAxis
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.adapters.GamePropertiesAdapter
import org.yuzu.yuzu_emu.databinding.FragmentGamePropertiesBinding
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.model.DriverViewModel
import org.yuzu.yuzu_emu.model.GameProperty
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.InstallableProperty
import org.yuzu.yuzu_emu.model.SubmenuProperty
import org.yuzu.yuzu_emu.model.TaskState
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.GameIconUtils
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.MemoryUtil
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.File
class GamePropertiesFragment : Fragment() {
private var _binding: FragmentGamePropertiesBinding? = null
private val binding get() = _binding!!
private val homeViewModel: HomeViewModel by activityViewModels()
private val gamesViewModel: GamesViewModel by activityViewModels()
private val driverViewModel: DriverViewModel by activityViewModels()
private val args by navArgs<GamePropertiesFragmentArgs>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialSharedAxis(MaterialSharedAxis.Y, true)
returnTransition = MaterialSharedAxis(MaterialSharedAxis.Y, false)
reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, false)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentGamePropertiesBinding.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)
homeViewModel.setNavigationVisibility(visible = false, animated = true)
homeViewModel.setStatusBarShadeVisibility(true)
binding.buttonBack.setOnClickListener {
view.findNavController().popBackStack()
}
GameIconUtils.loadGameIcon(args.game, binding.imageGameScreen)
binding.title.text = args.game.title
binding.title.postDelayed(
{
binding.title.ellipsize = TextUtils.TruncateAt.MARQUEE
binding.title.isSelected = true
},
3000
)
binding.buttonStart.setOnClickListener {
LaunchGameDialogFragment.newInstance(args.game)
.show(childFragmentManager, LaunchGameDialogFragment.TAG)
}
reloadList()
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
homeViewModel.openImportSaves.collect {
if (it) {
importSaves.launch(arrayOf("application/zip"))
homeViewModel.setOpenImportSaves(false)
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
homeViewModel.reloadPropertiesList.collect {
if (it) {
reloadList()
homeViewModel.reloadPropertiesList(false)
}
}
}
}
}
setInsets()
}
override fun onDestroy() {
super.onDestroy()
gamesViewModel.reloadGames(true)
}
private fun reloadList() {
_binding ?: return
driverViewModel.updateDriverNameForGame(args.game)
val properties = mutableListOf<GameProperty>().apply {
add(
SubmenuProperty(
R.string.info,
R.string.info_description,
R.drawable.ic_info_outline
) {
val action = GamePropertiesFragmentDirections
.actionPerGamePropertiesFragmentToGameInfoFragment(args.game)
binding.root.findNavController().navigate(action)
}
)
add(
SubmenuProperty(
R.string.preferences_settings,
R.string.per_game_settings_description,
R.drawable.ic_settings
) {
val action = HomeNavigationDirections.actionGlobalSettingsActivity(
args.game,
Settings.MenuTag.SECTION_ROOT
)
binding.root.findNavController().navigate(action)
}
)
if (GpuDriverHelper.supportsCustomDriverLoading()) {
add(
SubmenuProperty(
R.string.gpu_driver_manager,
R.string.install_gpu_driver_description,
R.drawable.ic_build,
detailsFlow = driverViewModel.selectedDriverTitle
) {
val action = GamePropertiesFragmentDirections
.actionPerGamePropertiesFragmentToDriverManagerFragment(args.game)
binding.root.findNavController().navigate(action)
}
)
}
if (!args.game.isHomebrew) {
add(
SubmenuProperty(
R.string.add_ons,
R.string.add_ons_description,
R.drawable.ic_edit
) {
val action = GamePropertiesFragmentDirections
.actionPerGamePropertiesFragmentToAddonsFragment(args.game)
binding.root.findNavController().navigate(action)
}
)
add(
InstallableProperty(
R.string.save_data,
R.string.save_data_description,
R.drawable.ic_save,
{
MessageDialogFragment.newInstance(
requireActivity(),
titleId = R.string.import_save_warning,
descriptionId = R.string.import_save_warning_description,
positiveAction = { homeViewModel.setOpenImportSaves(true) }
).show(parentFragmentManager, MessageDialogFragment.TAG)
},
if (File(args.game.saveDir).exists()) {
{ exportSaves.launch(args.game.saveZipName) }
} else {
null
}
)
)
val saveDirFile = File(args.game.saveDir)
if (saveDirFile.exists()) {
add(
SubmenuProperty(
R.string.delete_save_data,
R.string.delete_save_data_description,
R.drawable.ic_delete,
action = {
MessageDialogFragment.newInstance(
requireActivity(),
titleId = R.string.delete_save_data,
descriptionId = R.string.delete_save_data_warning_description,
positiveAction = {
File(args.game.saveDir).deleteRecursively()
Toast.makeText(
YuzuApplication.appContext,
R.string.save_data_deleted_successfully,
Toast.LENGTH_SHORT
).show()
homeViewModel.reloadPropertiesList(true)
}
).show(parentFragmentManager, MessageDialogFragment.TAG)
}
)
)
}
val shaderCacheDir = File(
DirectoryInitialization.userDirectory +
"/shader/" + args.game.settingsName.lowercase()
)
if (shaderCacheDir.exists()) {
add(
SubmenuProperty(
R.string.clear_shader_cache,
R.string.clear_shader_cache_description,
R.drawable.ic_delete,
{
if (shaderCacheDir.exists()) {
val bytes = shaderCacheDir.walkTopDown().filter { it.isFile }
.map { it.length() }.sum()
MemoryUtil.bytesToSizeUnit(bytes.toFloat())
} else {
MemoryUtil.bytesToSizeUnit(0f)
}
}
) {
MessageDialogFragment.newInstance(
requireActivity(),
titleId = R.string.clear_shader_cache,
descriptionId = R.string.clear_shader_cache_warning_description,
positiveAction = {
shaderCacheDir.deleteRecursively()
Toast.makeText(
YuzuApplication.appContext,
R.string.cleared_shaders_successfully,
Toast.LENGTH_SHORT
).show()
homeViewModel.reloadPropertiesList(true)
}
).show(parentFragmentManager, MessageDialogFragment.TAG)
}
)
}
}
}
binding.listProperties.apply {
layoutManager =
GridLayoutManager(requireContext(), resources.getInteger(R.integer.grid_columns))
adapter = GamePropertiesAdapter(viewLifecycleOwner, properties)
}
}
override fun onResume() {
super.onResume()
driverViewModel.updateDriverNameForGame(args.game)
}
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 smallLayout = resources.getBoolean(R.bool.small_layout)
if (smallLayout) {
val mlpListAll =
binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
mlpListAll.leftMargin = leftInsets
mlpListAll.rightMargin = rightInsets
binding.listAll.layoutParams = mlpListAll
} else {
if (ViewCompat.getLayoutDirection(binding.root) ==
ViewCompat.LAYOUT_DIRECTION_LTR
) {
val mlpListAll =
binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
mlpListAll.rightMargin = rightInsets
binding.listAll.layoutParams = mlpListAll
val mlpIconLayout =
binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams
mlpIconLayout.topMargin = barInsets.top
mlpIconLayout.leftMargin = leftInsets
binding.iconLayout!!.layoutParams = mlpIconLayout
} else {
val mlpListAll =
binding.listAll.layoutParams as ViewGroup.MarginLayoutParams
mlpListAll.leftMargin = leftInsets
binding.listAll.layoutParams = mlpListAll
val mlpIconLayout =
binding.iconLayout!!.layoutParams as ViewGroup.MarginLayoutParams
mlpIconLayout.topMargin = barInsets.top
mlpIconLayout.rightMargin = rightInsets
binding.iconLayout!!.layoutParams = mlpIconLayout
}
}
val fabSpacing = resources.getDimensionPixelSize(R.dimen.spacing_fab)
val mlpFab =
binding.buttonStart.layoutParams as ViewGroup.MarginLayoutParams
mlpFab.leftMargin = leftInsets + fabSpacing
mlpFab.rightMargin = rightInsets + fabSpacing
mlpFab.bottomMargin = barInsets.bottom + fabSpacing
binding.buttonStart.layoutParams = mlpFab
binding.layoutAll.updatePadding(
top = barInsets.top,
bottom = barInsets.bottom +
resources.getDimensionPixelSize(R.dimen.spacing_bottom_list_fab)
)
windowInsets
}
private val importSaves =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result == null) {
return@registerForActivityResult
}
val inputZip = requireContext().contentResolver.openInputStream(result)
val savesFolder = File(args.game.saveDir)
val cacheSaveDir = File("${requireContext().cacheDir.path}/saves/")
cacheSaveDir.mkdir()
if (inputZip == null) {
Toast.makeText(
YuzuApplication.appContext,
getString(R.string.fatal_error),
Toast.LENGTH_LONG
).show()
return@registerForActivityResult
}
IndeterminateProgressDialogFragment.newInstance(
requireActivity(),
R.string.save_files_importing,
false
) {
try {
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
val files = cacheSaveDir.listFiles()
var savesFolderFile: File? = null
if (files != null) {
val savesFolderName = args.game.programIdHex
for (file in files) {
if (file.isDirectory && file.name == savesFolderName) {
savesFolderFile = file
break
}
}
}
if (savesFolderFile != null) {
savesFolder.deleteRecursively()
savesFolder.mkdir()
savesFolderFile.copyRecursively(savesFolder)
savesFolderFile.deleteRecursively()
}
withContext(Dispatchers.Main) {
if (savesFolderFile == null) {
MessageDialogFragment.newInstance(
requireActivity(),
titleId = R.string.save_file_invalid_zip_structure,
descriptionId = R.string.save_file_invalid_zip_structure_description
).show(parentFragmentManager, MessageDialogFragment.TAG)
return@withContext
}
Toast.makeText(
YuzuApplication.appContext,
getString(R.string.save_file_imported_success),
Toast.LENGTH_LONG
).show()
homeViewModel.reloadPropertiesList(true)
}
cacheSaveDir.deleteRecursively()
} catch (e: Exception) {
Toast.makeText(
YuzuApplication.appContext,
getString(R.string.fatal_error),
Toast.LENGTH_LONG
).show()
}
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
}
/**
* Exports the save file located in the given folder path by creating a zip file and opening a
* file picker to save.
*/
private val exportSaves = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/zip")
) { result ->
if (result == null) {
return@registerForActivityResult
}
IndeterminateProgressDialogFragment.newInstance(
requireActivity(),
R.string.save_files_exporting,
false
) {
val saveLocation = args.game.saveDir
val zipResult = FileUtil.zipFromInternalStorage(
File(saveLocation),
saveLocation.replaceAfterLast("/", ""),
BufferedOutputStream(requireContext().contentResolver.openOutputStream(result))
)
return@newInstance when (zipResult) {
TaskState.Completed -> getString(R.string.export_success)
TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
}
}.show(parentFragmentManager, IndeterminateProgressDialogFragment.TAG)
}
}

View File

@ -68,6 +68,9 @@ class HomeSettingsFragment : Fragment() {
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = true, animated = true)
homeViewModel.setStatusBarShadeVisibility(visible = true)
mainActivity = requireActivity() as MainActivity
val optionsList: MutableList<HomeSetting> = mutableListOf<HomeSetting>().apply {
@ -91,13 +94,14 @@ class HomeSettingsFragment : Fragment() {
R.string.install_gpu_driver_description,
R.drawable.ic_build,
{
binding.root.findNavController()
.navigate(R.id.action_homeSettingsFragment_to_driverManagerFragment)
val action = HomeSettingsFragmentDirections
.actionHomeSettingsFragmentToDriverManagerFragment(null)
binding.root.findNavController().navigate(action)
},
{ GpuDriverHelper.supportsCustomDriverLoading() },
R.string.custom_driver_not_supported,
R.string.custom_driver_not_supported_description,
driverViewModel.selectedDriverMetadata
driverViewModel.selectedDriverTitle
)
)
add(
@ -212,8 +216,11 @@ class HomeSettingsFragment : Fragment() {
override fun onStart() {
super.onStart()
exitTransition = null
homeViewModel.setNavigationVisibility(visible = true, animated = true)
homeViewModel.setStatusBarShadeVisibility(visible = true)
}
override fun onResume() {
super.onResume()
driverViewModel.updateDriverNameForGame(null)
}
override fun onDestroyView() {

View File

@ -122,7 +122,7 @@ class IndeterminateProgressDialogFragment : DialogFragment() {
activity: FragmentActivity,
titleId: Int,
cancellable: Boolean = false,
task: () -> Any
task: suspend () -> Any
): IndeterminateProgressDialogFragment {
val dialog = IndeterminateProgressDialogFragment()
val args = Bundle()

View File

@ -21,8 +21,6 @@ import org.yuzu.yuzu_emu.databinding.FragmentInstallablesBinding
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.Installable
import org.yuzu.yuzu_emu.ui.main.MainActivity
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
class InstallableFragment : Fragment() {
private var _binding: FragmentInstallablesBinding? = null
@ -75,28 +73,6 @@ class InstallableFragment : Fragment() {
R.string.install_firmware_description,
install = { mainActivity.getFirmware.launch(arrayOf("application/zip")) }
),
if (mainActivity.savesFolderRoot != "") {
Installable(
R.string.manage_save_data,
R.string.import_export_saves_description,
install = { mainActivity.importSaves.launch(arrayOf("application/zip")) },
export = {
mainActivity.exportSaves.launch(
"yuzu saves - ${
LocalDateTime.now().format(
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
)
}.zip"
)
}
)
} else {
Installable(
R.string.manage_save_data,
R.string.import_export_saves_description,
install = { mainActivity.importSaves.launch(arrayOf("application/zip")) }
)
},
Installable(
R.string.install_prod_keys,
R.string.install_prod_keys_description,

View File

@ -0,0 +1,61 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.fragments
import android.app.Dialog
import android.content.DialogInterface
import android.os.Bundle
import androidx.fragment.app.DialogFragment
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.model.Game
import org.yuzu.yuzu_emu.utils.SerializableHelper.parcelable
class LaunchGameDialogFragment : DialogFragment() {
private var selectedItem = 1
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val game = requireArguments().parcelable<Game>(GAME)
val launchOptions = arrayOf(getString(R.string.global), getString(R.string.custom))
if (savedInstanceState != null) {
selectedItem = savedInstanceState.getInt(SELECTED_ITEM)
}
return MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.launch_options)
.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
val action = HomeNavigationDirections
.actionGlobalEmulationActivity(game, selectedItem != 0)
requireParentFragment().findNavController().navigate(action)
}
.setSingleChoiceItems(launchOptions, 1) { _: DialogInterface, i: Int ->
selectedItem = i
}
.setNegativeButton(android.R.string.cancel, null)
.show()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt(SELECTED_ITEM, selectedItem)
}
companion object {
const val TAG = "LaunchGameDialogFragment"
const val GAME = "Game"
const val SELECTED_ITEM = "SelectedItem"
fun newInstance(game: Game): LaunchGameDialogFragment {
val args = Bundle()
args.putParcelable(GAME, game)
val fragment = LaunchGameDialogFragment()
fragment.arguments = args
return fragment
}
}
}

View File

@ -27,30 +27,31 @@ class MessageDialogFragment : DialogFragment() {
val descriptionString = requireArguments().getString(DESCRIPTION_STRING)!!
val helpLinkId = requireArguments().getInt(HELP_LINK)
val dialog = MaterialAlertDialogBuilder(requireContext())
.setPositiveButton(R.string.close, null)
val builder = MaterialAlertDialogBuilder(requireContext())
if (titleId != 0) dialog.setTitle(titleId)
if (titleString.isNotEmpty()) dialog.setTitle(titleString)
if (messageDialogViewModel.positiveAction == null) {
builder.setPositiveButton(R.string.close, null)
} else {
builder.setPositiveButton(android.R.string.ok) { _: DialogInterface, _: Int ->
messageDialogViewModel.positiveAction?.invoke()
}.setNegativeButton(android.R.string.cancel, null)
}
if (titleId != 0) builder.setTitle(titleId)
if (titleString.isNotEmpty()) builder.setTitle(titleString)
if (descriptionId != 0) {
dialog.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY))
builder.setMessage(Html.fromHtml(getString(descriptionId), Html.FROM_HTML_MODE_LEGACY))
}
if (descriptionString.isNotEmpty()) dialog.setMessage(descriptionString)
if (descriptionString.isNotEmpty()) builder.setMessage(descriptionString)
if (helpLinkId != 0) {
dialog.setNeutralButton(R.string.learn_more) { _, _ ->
builder.setNeutralButton(R.string.learn_more) { _, _ ->
openLink(getString(helpLinkId))
}
}
return dialog.show()
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
messageDialogViewModel.dismissAction.invoke()
messageDialogViewModel.clear()
return builder.show()
}
private fun openLink(link: String) {
@ -74,7 +75,7 @@ class MessageDialogFragment : DialogFragment() {
descriptionId: Int = 0,
descriptionString: String = "",
helpLinkId: Int = 0,
dismissAction: () -> Unit = {}
positiveAction: (() -> Unit)? = null
): MessageDialogFragment {
val dialog = MessageDialogFragment()
val bundle = Bundle()
@ -85,8 +86,10 @@ class MessageDialogFragment : DialogFragment() {
putString(DESCRIPTION_STRING, descriptionString)
putInt(HELP_LINK, helpLinkId)
}
ViewModelProvider(activity)[MessageDialogViewModel::class.java].dismissAction =
dismissAction
ViewModelProvider(activity)[MessageDialogViewModel::class.java].apply {
clear()
this.positiveAction = positiveAction
}
dialog.arguments = bundle
return dialog
}

View File

@ -24,6 +24,7 @@ import androidx.lifecycle.repeatOnLifecycle
import androidx.preference.PreferenceManager
import info.debatty.java.stringsimilarity.Jaccard
import info.debatty.java.stringsimilarity.JaroWinkler
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import java.util.Locale
import org.yuzu.yuzu_emu.R
@ -60,7 +61,9 @@ class SearchFragment : Fragment() {
// This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
homeViewModel.setNavigationVisibility(visible = true, animated = false)
super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = true, animated = true)
homeViewModel.setStatusBarShadeVisibility(true)
preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
if (savedInstanceState != null) {
@ -99,7 +102,7 @@ class SearchFragment : Fragment() {
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
gamesViewModel.games.collect { filterAndSearch() }
gamesViewModel.games.collectLatest { filterAndSearch() }
}
}
launch {

View File

@ -70,7 +70,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
sliderBinding = DialogSliderBinding.inflate(layoutInflater)
val item = settingsViewModel.clickedItem as SliderSetting
settingsViewModel.setSliderTextValue(item.selectedValue.toFloat(), item.units)
settingsViewModel.setSliderTextValue(item.getSelectedValue().toFloat(), item.units)
sliderBinding.slider.apply {
valueFrom = item.min.toFloat()
valueTo = item.max.toFloat()
@ -136,18 +136,18 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
is SingleChoiceSetting -> {
val scSetting = settingsViewModel.clickedItem as SingleChoiceSetting
val value = getValueForSingleChoiceSelection(scSetting, which)
scSetting.selectedValue = value
scSetting.setSelectedValue(value)
}
is StringSingleChoiceSetting -> {
val scSetting = settingsViewModel.clickedItem as StringSingleChoiceSetting
val value = scSetting.getValueAt(which)
scSetting.selectedValue = value
scSetting.setSelectedValue(value)
}
is SliderSetting -> {
val sliderSetting = settingsViewModel.clickedItem as SliderSetting
sliderSetting.selectedValue = settingsViewModel.sliderProgress.value
sliderSetting.setSelectedValue(settingsViewModel.sliderProgress.value)
}
}
closeDialog()
@ -171,7 +171,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
}
private fun getSelectionForSingleChoiceValue(item: SingleChoiceSetting): Int {
val value = item.selectedValue
val value = item.getSelectedValue()
val valuesId = item.valuesId
if (valuesId > 0) {
val valuesArray = requireContext().resources.getIntArray(valuesId)
@ -211,7 +211,7 @@ class SettingsDialogFragment : DialogFragment(), DialogInterface.OnClickListener
throw IllegalArgumentException("[SettingsDialogFragment] Incompatible type!")
SettingsItem.TYPE_SLIDER -> settingsViewModel.setSliderProgress(
(clickedItem as SliderSetting).selectedValue.toFloat()
(clickedItem as SliderSetting).getSelectedValue().toFloat()
)
}
settingsViewModel.clickedItem = clickedItem

View File

@ -4,6 +4,7 @@
package org.yuzu.yuzu_emu.fragments
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Build
import android.os.Bundle
@ -75,6 +76,8 @@ class SetupFragment : Fragment() {
return binding.root
}
// This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
mainActivity = requireActivity() as MainActivity
@ -206,12 +209,24 @@ class SetupFragment : Fragment() {
)
}
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
homeViewModel.shouldPageForward.collect {
if (it) {
pageForward()
homeViewModel.setShouldPageForward(false)
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
homeViewModel.shouldPageForward.collect {
if (it) {
pageForward()
homeViewModel.setShouldPageForward(false)
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
homeViewModel.gamesDirSelected.collect {
if (it) {
gamesDirCallback.onStepCompleted()
homeViewModel.setGamesDirSelected(false)
}
}
}
}
@ -289,6 +304,11 @@ class SetupFragment : Fragment() {
setInsets()
}
override fun onStop() {
super.onStop()
NativeConfig.saveGlobalConfig()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
if (_binding != null) {
@ -339,7 +359,6 @@ class SetupFragment : Fragment() {
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
if (result != null) {
mainActivity.processGamesDir(result)
gamesDirCallback.onStepCompleted()
}
}

View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.model
data class Addon(
var enabled: Boolean,
val title: String,
val version: String
)

View File

@ -0,0 +1,83 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.model
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.utils.NativeConfig
import java.util.concurrent.atomic.AtomicBoolean
class AddonViewModel : ViewModel() {
private val _addonList = MutableStateFlow(mutableListOf<Addon>())
val addonList get() = _addonList.asStateFlow()
private val _showModInstallPicker = MutableStateFlow(false)
val showModInstallPicker get() = _showModInstallPicker.asStateFlow()
private val _showModNoticeDialog = MutableStateFlow(false)
val showModNoticeDialog get() = _showModNoticeDialog.asStateFlow()
var game: Game? = null
private val isRefreshing = AtomicBoolean(false)
fun onOpenAddons(game: Game) {
this.game = game
refreshAddons()
}
fun refreshAddons() {
if (isRefreshing.get() || game == null) {
return
}
isRefreshing.set(true)
viewModelScope.launch {
withContext(Dispatchers.IO) {
val addonList = mutableListOf<Addon>()
val disabledAddons = NativeConfig.getDisabledAddons(game!!.programId)
NativeLibrary.getAddonsForFile(game!!.path, game!!.programId)?.forEach {
val name = it.first.replace("[D] ", "")
addonList.add(Addon(!disabledAddons.contains(name), name, it.second))
}
addonList.sortBy { it.title }
_addonList.value = addonList
isRefreshing.set(false)
}
}
}
fun onCloseAddons() {
if (_addonList.value.isEmpty()) {
return
}
NativeConfig.setDisabledAddons(
game!!.programId,
_addonList.value.mapNotNull {
if (it.enabled) {
null
} else {
it.title
}
}.toTypedArray()
)
NativeConfig.saveGlobalConfig()
_addonList.value.clear()
game = null
}
fun showModInstallPicker(install: Boolean) {
_showModInstallPicker.value = install
}
fun showModNoticeDialog(show: Boolean) {
_showModNoticeDialog.value = show
}
}

View File

@ -7,81 +7,83 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import org.yuzu.yuzu_emu.features.settings.utils.SettingsFile
import org.yuzu.yuzu_emu.utils.FileUtil
import org.yuzu.yuzu_emu.utils.GpuDriverHelper
import org.yuzu.yuzu_emu.utils.GpuDriverMetadata
import org.yuzu.yuzu_emu.utils.NativeConfig
import java.io.BufferedOutputStream
import java.io.File
class DriverViewModel : ViewModel() {
private val _areDriversLoading = MutableStateFlow(false)
val areDriversLoading: StateFlow<Boolean> get() = _areDriversLoading
private val _isDriverReady = MutableStateFlow(true)
val isDriverReady: StateFlow<Boolean> get() = _isDriverReady
private val _isDeletingDrivers = MutableStateFlow(false)
val isDeletingDrivers: StateFlow<Boolean> get() = _isDeletingDrivers
private val _driverList = MutableStateFlow(mutableListOf<Pair<String, GpuDriverMetadata>>())
val isInteractionAllowed: StateFlow<Boolean> =
combine(
_areDriversLoading,
_isDriverReady,
_isDeletingDrivers
) { loading, ready, deleting ->
!loading && ready && !deleting
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), initialValue = false)
private val _driverList = MutableStateFlow(GpuDriverHelper.getDrivers())
val driverList: StateFlow<MutableList<Pair<String, GpuDriverMetadata>>> get() = _driverList
var previouslySelectedDriver = 0
var selectedDriver = -1
private val _selectedDriverMetadata =
MutableStateFlow(
GpuDriverHelper.customDriverData.name
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
)
val selectedDriverMetadata: StateFlow<String> get() = _selectedDriverMetadata
// Used for showing which driver is currently installed within the driver manager card
private val _selectedDriverTitle = MutableStateFlow("")
val selectedDriverTitle: StateFlow<String> get() = _selectedDriverTitle
private val _newDriverInstalled = MutableStateFlow(false)
val newDriverInstalled: StateFlow<Boolean> get() = _newDriverInstalled
val driversToDelete = mutableListOf<String>()
val isInteractionAllowed
get() = !areDriversLoading.value && isDriverReady.value && !isDeletingDrivers.value
init {
_areDriversLoading.value = true
viewModelScope.launch {
withContext(Dispatchers.IO) {
val drivers = GpuDriverHelper.getDrivers()
val currentDriverMetadata = GpuDriverHelper.customDriverData
for (i in drivers.indices) {
if (drivers[i].second == currentDriverMetadata) {
setSelectedDriverIndex(i)
break
}
}
val currentDriverMetadata = GpuDriverHelper.installedCustomDriverData
findSelectedDriver(currentDriverMetadata)
// If a user had installed a driver before the manager was implemented, this zips
// the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can
// be indexed and exported as expected.
if (selectedDriver == -1) {
val driverToSave =
File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip")
driverToSave.createNewFile()
FileUtil.zipFromInternalStorage(
File(GpuDriverHelper.driverInstallationPath!!),
GpuDriverHelper.driverInstallationPath!!,
BufferedOutputStream(driverToSave.outputStream())
)
drivers.add(Pair(driverToSave.path, currentDriverMetadata))
setSelectedDriverIndex(drivers.size - 1)
}
_driverList.value = drivers
_areDriversLoading.value = false
}
// If a user had installed a driver before the manager was implemented, this zips
// the installed driver to UserData/gpu_drivers/CustomDriver.zip so that it can
// be indexed and exported as expected.
if (selectedDriver == -1) {
val driverToSave =
File(GpuDriverHelper.driverStoragePath, "CustomDriver.zip")
driverToSave.createNewFile()
FileUtil.zipFromInternalStorage(
File(GpuDriverHelper.driverInstallationPath!!),
GpuDriverHelper.driverInstallationPath!!,
BufferedOutputStream(driverToSave.outputStream())
)
_driverList.value.add(Pair(driverToSave.path, currentDriverMetadata))
setSelectedDriverIndex(_driverList.value.size - 1)
}
// If a user had installed a driver before the config was reworked to be multiplatform,
// we have save the path of the previously selected driver to the new setting.
if (StringSetting.DRIVER_PATH.getString(true).isEmpty() && selectedDriver > 0 &&
StringSetting.DRIVER_PATH.global
) {
StringSetting.DRIVER_PATH.setString(_driverList.value[selectedDriver].first)
NativeConfig.saveGlobalConfig()
} else {
findSelectedDriver(GpuDriverHelper.customDriverSettingData)
}
updateDriverNameForGame(null)
}
fun setSelectedDriverIndex(value: Int) {
@ -98,9 +100,9 @@ class DriverViewModel : ViewModel() {
fun addDriver(driverData: Pair<String, GpuDriverMetadata>) {
val driverIndex = _driverList.value.indexOfFirst { it == driverData }
if (driverIndex == -1) {
setSelectedDriverIndex(_driverList.value.size)
_driverList.value.add(driverData)
_selectedDriverMetadata.value = driverData.second.name
setSelectedDriverIndex(_driverList.value.size - 1)
_selectedDriverTitle.value = driverData.second.name
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
} else {
setSelectedDriverIndex(driverIndex)
@ -111,8 +113,31 @@ class DriverViewModel : ViewModel() {
_driverList.value.remove(driverData)
}
fun onCloseDriverManager() {
fun onOpenDriverManager(game: Game?) {
if (game != null) {
SettingsFile.loadCustomConfig(game)
}
val driverPath = StringSetting.DRIVER_PATH.getString()
if (driverPath.isEmpty()) {
setSelectedDriverIndex(0)
} else {
findSelectedDriver(GpuDriverHelper.getMetadataFromZip(File(driverPath)))
}
}
fun onCloseDriverManager(game: Game?) {
_isDeletingDrivers.value = true
StringSetting.DRIVER_PATH.setString(driverList.value[selectedDriver].first)
updateDriverNameForGame(game)
if (game == null) {
NativeConfig.saveGlobalConfig()
} else {
NativeConfig.savePerGameConfig()
NativeConfig.unloadPerGameConfig()
NativeConfig.reloadGlobalConfig()
}
viewModelScope.launch {
withContext(Dispatchers.IO) {
driversToDelete.forEach {
@ -125,23 +150,29 @@ class DriverViewModel : ViewModel() {
_isDeletingDrivers.value = false
}
}
}
if (GpuDriverHelper.customDriverData == driverList.value[selectedDriver].second) {
// It is the Emulation Fragment's responsibility to load per-game settings so that this function
// knows what driver to load.
fun onLaunchGame() {
_isDriverReady.value = false
val selectedDriverFile = File(StringSetting.DRIVER_PATH.getString())
val selectedDriverMetadata = GpuDriverHelper.customDriverSettingData
if (GpuDriverHelper.installedCustomDriverData == selectedDriverMetadata) {
return
}
_isDriverReady.value = false
viewModelScope.launch {
withContext(Dispatchers.IO) {
if (selectedDriver == 0) {
if (selectedDriverMetadata.name == null) {
GpuDriverHelper.installDefaultDriver()
setDriverReady()
return@withContext
}
val driverToInstall = File(driverList.value[selectedDriver].first)
if (driverToInstall.exists()) {
GpuDriverHelper.installCustomDriver(driverToInstall)
if (selectedDriverFile.exists()) {
GpuDriverHelper.installCustomDriver(selectedDriverFile)
} else {
GpuDriverHelper.installDefaultDriver()
}
@ -150,9 +181,43 @@ class DriverViewModel : ViewModel() {
}
}
private fun findSelectedDriver(currentDriverMetadata: GpuDriverMetadata) {
if (driverList.value.size == 1) {
setSelectedDriverIndex(0)
return
}
driverList.value.forEachIndexed { i: Int, driver: Pair<String, GpuDriverMetadata> ->
if (driver.second == currentDriverMetadata) {
setSelectedDriverIndex(i)
return
}
}
}
fun updateDriverNameForGame(game: Game?) {
if (!GpuDriverHelper.supportsCustomDriverLoading()) {
return
}
if (game == null || NativeConfig.isPerGameConfigLoaded()) {
updateName()
} else {
SettingsFile.loadCustomConfig(game)
updateName()
NativeConfig.unloadPerGameConfig()
NativeConfig.reloadGlobalConfig()
}
}
private fun updateName() {
_selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
}
private fun setDriverReady() {
_isDriverReady.value = true
_selectedDriverMetadata.value = GpuDriverHelper.customDriverData.name
_selectedDriverTitle.value = GpuDriverHelper.customDriverSettingData.name
?: YuzuApplication.appContext.getString(R.string.system_gpu_driver)
}
}

View File

@ -3,10 +3,18 @@
package org.yuzu.yuzu_emu.model
import android.net.Uri
import android.os.Parcelable
import java.util.HashSet
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.utils.DirectoryInitialization
import org.yuzu.yuzu_emu.utils.FileUtil
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
@Parcelize
@Serializable
@ -15,12 +23,44 @@ class Game(
val path: String,
val programId: String = "",
val developer: String = "",
val version: String = "",
var version: String = "",
val isHomebrew: Boolean = false
) : Parcelable {
val keyAddedToLibraryTime get() = "${path}_AddedToLibraryTime"
val keyLastPlayedTime get() = "${path}_LastPlayed"
val settingsName: String
get() {
val programIdLong = programId.toLong()
return if (programIdLong == 0L) {
FileUtil.getFilename(Uri.parse(path))
} else {
"0" + programIdLong.toString(16).uppercase()
}
}
val programIdHex: String
get() {
val programIdLong = programId.toLong()
return if (programIdLong == 0L) {
"0"
} else {
"0" + programIdLong.toString(16).uppercase()
}
}
val saveZipName: String
get() = "$title ${YuzuApplication.appContext.getString(R.string.save_data).lowercase()} - ${
LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
}.zip"
val saveDir: String
get() = DirectoryInitialization.userDirectory + "/nand" +
NativeLibrary.getSavePath(programId)
val addonDir: String
get() = DirectoryInitialization.userDirectory + "/load/" + programIdHex + "/"
override fun equals(other: Any?): Boolean {
if (other !is Game) {
return false
@ -34,6 +74,7 @@ class Game(
result = 31 * result + path.hashCode()
result = 31 * result + programId.hashCode()
result = 31 * result + developer.hashCode()
result = 31 * result + version.hashCode()
result = 31 * result + isHomebrew.hashCode()
return result
}

View File

@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.model
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import kotlinx.coroutines.flow.StateFlow
interface GameProperty {
@get:StringRes
val titleId: Int
@get:StringRes
val descriptionId: Int
@get:DrawableRes
val iconId: Int
}
data class SubmenuProperty(
override val titleId: Int,
override val descriptionId: Int,
override val iconId: Int,
val details: (() -> String)? = null,
val detailsFlow: StateFlow<String>? = null,
val action: () -> Unit
) : GameProperty
data class InstallableProperty(
override val titleId: Int,
override val descriptionId: Int,
override val iconId: Int,
val install: (() -> Unit)? = null,
val export: (() -> Unit)? = null
) : GameProperty

View File

@ -20,8 +20,8 @@ import kotlinx.serialization.json.Json
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.utils.GameHelper
import org.yuzu.yuzu_emu.utils.GameMetadata
import org.yuzu.yuzu_emu.utils.NativeConfig
import java.util.concurrent.atomic.AtomicBoolean
class GamesViewModel : ViewModel() {
val games: StateFlow<List<Game>> get() = _games
@ -33,6 +33,8 @@ class GamesViewModel : ViewModel() {
val isReloading: StateFlow<Boolean> get() = _isReloading
private val _isReloading = MutableStateFlow(false)
private val reloading = AtomicBoolean(false)
val shouldSwapData: StateFlow<Boolean> get() = _shouldSwapData
private val _shouldSwapData = MutableStateFlow(false)
@ -49,38 +51,8 @@ class GamesViewModel : ViewModel() {
// Ensure keys are loaded so that ROM metadata can be decrypted.
NativeLibrary.reloadKeys()
// Retrieve list of cached games
val storedGames = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
.getStringSet(GameHelper.KEY_GAMES, emptySet())
viewModelScope.launch {
withContext(Dispatchers.IO) {
getGameDirs()
if (storedGames!!.isNotEmpty()) {
val deserializedGames = mutableSetOf<Game>()
storedGames.forEach {
val game: Game
try {
game = Json.decodeFromString(it)
} catch (e: Exception) {
// We don't care about any errors related to parsing the game cache
return@forEach
}
val gameExists =
DocumentFile.fromSingleUri(
YuzuApplication.appContext,
Uri.parse(game.path)
)?.exists()
if (gameExists == true) {
deserializedGames.add(game)
}
}
setGames(deserializedGames.toList())
}
reloadGames(false)
}
}
getGameDirs()
reloadGames(directoriesChanged = false, firstStartup = true)
}
fun setGames(games: List<Game>) {
@ -110,16 +82,46 @@ class GamesViewModel : ViewModel() {
_searchFocused.value = searchFocused
}
fun reloadGames(directoriesChanged: Boolean) {
if (isReloading.value) {
fun reloadGames(directoriesChanged: Boolean, firstStartup: Boolean = false) {
if (reloading.get()) {
return
}
reloading.set(true)
_isReloading.value = true
viewModelScope.launch {
withContext(Dispatchers.IO) {
GameMetadata.resetMetadata()
if (firstStartup) {
// Retrieve list of cached games
val storedGames =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
.getStringSet(GameHelper.KEY_GAMES, emptySet())
if (storedGames!!.isNotEmpty()) {
val deserializedGames = mutableSetOf<Game>()
storedGames.forEach {
val game: Game
try {
game = Json.decodeFromString(it)
} catch (e: Exception) {
// We don't care about any errors related to parsing the game cache
return@forEach
}
val gameExists =
DocumentFile.fromSingleUri(
YuzuApplication.appContext,
Uri.parse(game.path)
)?.exists()
if (gameExists == true) {
deserializedGames.add(game)
}
}
setGames(deserializedGames.toList())
}
}
setGames(GameHelper.getGames())
reloading.set(false)
_isReloading.value = false
if (directoriesChanged) {
@ -133,7 +135,7 @@ class GamesViewModel : ViewModel() {
viewModelScope.launch {
withContext(Dispatchers.IO) {
NativeConfig.addGameDir(gameDir)
getGameDirs()
getGameDirs(true)
}
}
@ -168,6 +170,7 @@ class GamesViewModel : ViewModel() {
fun onCloseGameFoldersFragment() =
viewModelScope.launch {
withContext(Dispatchers.IO) {
NativeConfig.saveGlobalConfig()
getGameDirs(true)
}
}

View File

@ -3,9 +3,11 @@
package org.yuzu.yuzu_emu.model
import android.net.Uri
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class HomeViewModel : ViewModel() {
val navigationVisible: StateFlow<Pair<Boolean, Boolean>> get() = _navigationVisible
@ -17,6 +19,18 @@ class HomeViewModel : ViewModel() {
val shouldPageForward: StateFlow<Boolean> get() = _shouldPageForward
private val _shouldPageForward = MutableStateFlow(false)
private val _gamesDirSelected = MutableStateFlow(false)
val gamesDirSelected get() = _gamesDirSelected.asStateFlow()
private val _openImportSaves = MutableStateFlow(false)
val openImportSaves get() = _openImportSaves.asStateFlow()
private val _contentToInstall = MutableStateFlow<List<Uri>?>(null)
val contentToInstall get() = _contentToInstall.asStateFlow()
private val _reloadPropertiesList = MutableStateFlow(false)
val reloadPropertiesList get() = _reloadPropertiesList.asStateFlow()
var navigatedToSetup = false
fun setNavigationVisibility(visible: Boolean, animated: Boolean) {
@ -36,4 +50,20 @@ class HomeViewModel : ViewModel() {
fun setShouldPageForward(pageForward: Boolean) {
_shouldPageForward.value = pageForward
}
fun setGamesDirSelected(selected: Boolean) {
_gamesDirSelected.value = selected
}
fun setOpenImportSaves(import: Boolean) {
_openImportSaves.value = import
}
fun setContentToInstall(documents: List<Uri>?) {
_contentToInstall.value = documents
}
fun reloadPropertiesList(reload: Boolean) {
_reloadPropertiesList.value = reload
}
}

View File

@ -6,9 +6,9 @@ package org.yuzu.yuzu_emu.model
import androidx.lifecycle.ViewModel
class MessageDialogViewModel : ViewModel() {
var dismissAction: () -> Unit = {}
var positiveAction: (() -> Unit)? = null
fun clear() {
dismissAction = {}
positiveAction = null
}
}

View File

@ -68,8 +68,4 @@ class SettingsViewModel : ViewModel() {
fun setAdapterItemChanged(value: Int) {
_adapterItemChanged.value = value
}
fun clear() {
game = null
}
}

View File

@ -23,7 +23,7 @@ class TaskViewModel : ViewModel() {
val cancelled: StateFlow<Boolean> get() = _cancelled
private val _cancelled = MutableStateFlow(false)
lateinit var task: () -> Any
lateinit var task: suspend () -> Any
fun clear() {
_result.value = Any()

View File

@ -10,6 +10,7 @@ import android.graphics.Rect
import android.graphics.drawable.BitmapDrawable
import android.view.MotionEvent
import org.yuzu.yuzu_emu.NativeLibrary.ButtonState
import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
/**
* Custom [BitmapDrawable] that is capable
@ -25,7 +26,7 @@ class InputOverlayDrawableButton(
defaultStateBitmap: Bitmap,
pressedStateBitmap: Bitmap,
val buttonId: Int,
val prefId: String
val overlayControlData: OverlayControlData
) {
// The ID value what motion event is tracking
var trackId: Int

View File

@ -14,7 +14,7 @@ import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.utils.EmulationMenuSettings
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
/**
* Custom [BitmapDrawable] that is capable
@ -125,7 +125,7 @@ class InputOverlayDrawableJoystick(
pressedState = true
outerBitmap.alpha = 0
boundsBoxBitmap.alpha = opacity
if (EmulationMenuSettings.joystickRelCenter) {
if (BooleanSetting.JOYSTICK_REL_CENTER.getBoolean()) {
virtBounds.offset(
xPosition - virtBounds.centerX(),
yPosition - virtBounds.centerY()

View File

@ -0,0 +1,188 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.overlay.model
import androidx.annotation.IntegerRes
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
enum class OverlayControl(
val id: String,
val defaultVisibility: Boolean,
@IntegerRes val defaultLandscapePositionResources: Pair<Int, Int>,
@IntegerRes val defaultPortraitPositionResources: Pair<Int, Int>,
@IntegerRes val defaultFoldablePositionResources: Pair<Int, Int>
) {
BUTTON_A(
"button_a",
true,
Pair(R.integer.BUTTON_A_X, R.integer.BUTTON_A_Y),
Pair(R.integer.BUTTON_A_X_PORTRAIT, R.integer.BUTTON_A_Y_PORTRAIT),
Pair(R.integer.BUTTON_A_X_FOLDABLE, R.integer.BUTTON_A_Y_FOLDABLE)
),
BUTTON_B(
"button_b",
true,
Pair(R.integer.BUTTON_B_X, R.integer.BUTTON_B_Y),
Pair(R.integer.BUTTON_B_X_PORTRAIT, R.integer.BUTTON_B_Y_PORTRAIT),
Pair(R.integer.BUTTON_B_X_FOLDABLE, R.integer.BUTTON_B_Y_FOLDABLE)
),
BUTTON_X(
"button_x",
true,
Pair(R.integer.BUTTON_X_X, R.integer.BUTTON_X_Y),
Pair(R.integer.BUTTON_X_X_PORTRAIT, R.integer.BUTTON_X_Y_PORTRAIT),
Pair(R.integer.BUTTON_X_X_FOLDABLE, R.integer.BUTTON_X_Y_FOLDABLE)
),
BUTTON_Y(
"button_y",
true,
Pair(R.integer.BUTTON_Y_X, R.integer.BUTTON_Y_Y),
Pair(R.integer.BUTTON_Y_X_PORTRAIT, R.integer.BUTTON_Y_Y_PORTRAIT),
Pair(R.integer.BUTTON_Y_X_FOLDABLE, R.integer.BUTTON_Y_Y_FOLDABLE)
),
BUTTON_PLUS(
"button_plus",
true,
Pair(R.integer.BUTTON_PLUS_X, R.integer.BUTTON_PLUS_Y),
Pair(R.integer.BUTTON_PLUS_X_PORTRAIT, R.integer.BUTTON_PLUS_Y_PORTRAIT),
Pair(R.integer.BUTTON_PLUS_X_FOLDABLE, R.integer.BUTTON_PLUS_Y_FOLDABLE)
),
BUTTON_MINUS(
"button_minus",
true,
Pair(R.integer.BUTTON_MINUS_X, R.integer.BUTTON_MINUS_Y),
Pair(R.integer.BUTTON_MINUS_X_PORTRAIT, R.integer.BUTTON_MINUS_Y_PORTRAIT),
Pair(R.integer.BUTTON_MINUS_X_FOLDABLE, R.integer.BUTTON_MINUS_Y_FOLDABLE)
),
BUTTON_HOME(
"button_home",
false,
Pair(R.integer.BUTTON_HOME_X, R.integer.BUTTON_HOME_Y),
Pair(R.integer.BUTTON_HOME_X_PORTRAIT, R.integer.BUTTON_HOME_Y_PORTRAIT),
Pair(R.integer.BUTTON_HOME_X_FOLDABLE, R.integer.BUTTON_HOME_Y_FOLDABLE)
),
BUTTON_CAPTURE(
"button_capture",
false,
Pair(R.integer.BUTTON_CAPTURE_X, R.integer.BUTTON_CAPTURE_Y),
Pair(R.integer.BUTTON_CAPTURE_X_PORTRAIT, R.integer.BUTTON_CAPTURE_Y_PORTRAIT),
Pair(R.integer.BUTTON_CAPTURE_X_FOLDABLE, R.integer.BUTTON_CAPTURE_Y_FOLDABLE)
),
BUTTON_L(
"button_l",
true,
Pair(R.integer.BUTTON_L_X, R.integer.BUTTON_L_Y),
Pair(R.integer.BUTTON_L_X_PORTRAIT, R.integer.BUTTON_L_Y_PORTRAIT),
Pair(R.integer.BUTTON_L_X_FOLDABLE, R.integer.BUTTON_L_Y_FOLDABLE)
),
BUTTON_R(
"button_r",
true,
Pair(R.integer.BUTTON_R_X, R.integer.BUTTON_R_Y),
Pair(R.integer.BUTTON_R_X_PORTRAIT, R.integer.BUTTON_R_Y_PORTRAIT),
Pair(R.integer.BUTTON_R_X_FOLDABLE, R.integer.BUTTON_R_Y_FOLDABLE)
),
BUTTON_ZL(
"button_zl",
true,
Pair(R.integer.BUTTON_ZL_X, R.integer.BUTTON_ZL_Y),
Pair(R.integer.BUTTON_ZL_X_PORTRAIT, R.integer.BUTTON_ZL_Y_PORTRAIT),
Pair(R.integer.BUTTON_ZL_X_FOLDABLE, R.integer.BUTTON_ZL_Y_FOLDABLE)
),
BUTTON_ZR(
"button_zr",
true,
Pair(R.integer.BUTTON_ZR_X, R.integer.BUTTON_ZR_Y),
Pair(R.integer.BUTTON_ZR_X_PORTRAIT, R.integer.BUTTON_ZR_Y_PORTRAIT),
Pair(R.integer.BUTTON_ZR_X_FOLDABLE, R.integer.BUTTON_ZR_Y_FOLDABLE)
),
BUTTON_STICK_L(
"button_stick_l",
true,
Pair(R.integer.BUTTON_STICK_L_X, R.integer.BUTTON_STICK_L_Y),
Pair(R.integer.BUTTON_STICK_L_X_PORTRAIT, R.integer.BUTTON_STICK_L_Y_PORTRAIT),
Pair(R.integer.BUTTON_STICK_L_X_FOLDABLE, R.integer.BUTTON_STICK_L_Y_FOLDABLE)
),
BUTTON_STICK_R(
"button_stick_r",
true,
Pair(R.integer.BUTTON_STICK_R_X, R.integer.BUTTON_STICK_R_Y),
Pair(R.integer.BUTTON_STICK_R_X_PORTRAIT, R.integer.BUTTON_STICK_R_Y_PORTRAIT),
Pair(R.integer.BUTTON_STICK_R_X_FOLDABLE, R.integer.BUTTON_STICK_R_Y_FOLDABLE)
),
STICK_L(
"stick_l",
true,
Pair(R.integer.STICK_L_X, R.integer.STICK_L_Y),
Pair(R.integer.STICK_L_X_PORTRAIT, R.integer.STICK_L_Y_PORTRAIT),
Pair(R.integer.STICK_L_X_FOLDABLE, R.integer.STICK_L_Y_FOLDABLE)
),
STICK_R(
"stick_r",
true,
Pair(R.integer.STICK_R_X, R.integer.STICK_R_Y),
Pair(R.integer.STICK_R_X_PORTRAIT, R.integer.STICK_R_Y_PORTRAIT),
Pair(R.integer.STICK_R_X_FOLDABLE, R.integer.STICK_R_Y_FOLDABLE)
),
COMBINED_DPAD(
"combined_dpad",
true,
Pair(R.integer.COMBINED_DPAD_X, R.integer.COMBINED_DPAD_Y),
Pair(R.integer.COMBINED_DPAD_X_PORTRAIT, R.integer.COMBINED_DPAD_Y_PORTRAIT),
Pair(R.integer.COMBINED_DPAD_X_FOLDABLE, R.integer.COMBINED_DPAD_Y_FOLDABLE)
);
fun getDefaultPositionForLayout(layout: OverlayLayout): Pair<Double, Double> {
val rawResourcePair: Pair<Int, Int>
YuzuApplication.appContext.resources.apply {
rawResourcePair = when (layout) {
OverlayLayout.Landscape -> {
Pair(
getInteger(this@OverlayControl.defaultLandscapePositionResources.first),
getInteger(this@OverlayControl.defaultLandscapePositionResources.second)
)
}
OverlayLayout.Portrait -> {
Pair(
getInteger(this@OverlayControl.defaultPortraitPositionResources.first),
getInteger(this@OverlayControl.defaultPortraitPositionResources.second)
)
}
OverlayLayout.Foldable -> {
Pair(
getInteger(this@OverlayControl.defaultFoldablePositionResources.first),
getInteger(this@OverlayControl.defaultFoldablePositionResources.second)
)
}
}
}
return Pair(
rawResourcePair.first.toDouble() / 1000,
rawResourcePair.second.toDouble() / 1000
)
}
fun toOverlayControlData(): OverlayControlData =
OverlayControlData(
id,
defaultVisibility,
getDefaultPositionForLayout(OverlayLayout.Landscape),
getDefaultPositionForLayout(OverlayLayout.Portrait),
getDefaultPositionForLayout(OverlayLayout.Foldable)
)
companion object {
val map: HashMap<String, OverlayControl> by lazy {
val hashMap = hashMapOf<String, OverlayControl>()
entries.forEach { hashMap[it.id] = it }
hashMap
}
fun from(id: String): OverlayControl? = map[id]
}
}

View File

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.overlay.model
data class OverlayControlData(
val id: String,
var enabled: Boolean,
var landscapePosition: Pair<Double, Double>,
var portraitPosition: Pair<Double, Double>,
var foldablePosition: Pair<Double, Double>
) {
fun positionFromLayout(layout: OverlayLayout): Pair<Double, Double> =
when (layout) {
OverlayLayout.Landscape -> landscapePosition
OverlayLayout.Portrait -> portraitPosition
OverlayLayout.Foldable -> foldablePosition
}
}

View File

@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.overlay.model
import androidx.annotation.IntegerRes
data class OverlayControlDefault(
val buttonId: String,
@IntegerRes val landscapePositionResource: Pair<Int, Int>,
@IntegerRes val portraitPositionResource: Pair<Int, Int>,
@IntegerRes val foldablePositionResource: Pair<Int, Int>
)

View File

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.overlay.model
enum class OverlayLayout(val id: String) {
Landscape("Landscape"),
Portrait("Portrait"),
Foldable("Foldable")
}

View File

@ -19,7 +19,7 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.google.android.material.color.MaterialColors
import com.google.android.material.transition.MaterialFadeThrough
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.adapters.GameAdapter
@ -35,11 +35,6 @@ class GamesFragment : Fragment() {
private val gamesViewModel: GamesViewModel by activityViewModels()
private val homeViewModel: HomeViewModel by activityViewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enterTransition = MaterialFadeThrough()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -52,7 +47,9 @@ class GamesFragment : Fragment() {
// This is using the correct scope, lint is just acting up
@SuppressLint("UnsafeRepeatOnLifecycleDetector")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
homeViewModel.setNavigationVisibility(visible = true, animated = false)
super.onViewCreated(view, savedInstanceState)
homeViewModel.setNavigationVisibility(visible = true, animated = true)
homeViewModel.setStatusBarShadeVisibility(true)
binding.gridGames.apply {
layoutManager = AutofitGridLayoutManager(
@ -94,18 +91,20 @@ class GamesFragment : Fragment() {
viewLifecycleOwner.lifecycleScope.apply {
launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
gamesViewModel.isReloading.collect { binding.swipeRefresh.isRefreshing = it }
gamesViewModel.isReloading.collect {
binding.swipeRefresh.isRefreshing = it
if (gamesViewModel.games.value.isEmpty() && !it) {
binding.noticeText.visibility = View.VISIBLE
} else {
binding.noticeText.visibility = View.INVISIBLE
}
}
}
}
launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
gamesViewModel.games.collect {
gamesViewModel.games.collectLatest {
(binding.gridGames.adapter as GameAdapter).submitList(it)
if (it.isEmpty()) {
binding.noticeText.visibility = View.VISIBLE
} else {
binding.noticeText.visibility = View.GONE
}
}
}
}

View File

@ -28,12 +28,9 @@ import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
import com.google.android.material.color.MaterialColors
import com.google.android.material.navigation.NavigationBarView
import kotlinx.coroutines.CoroutineScope
import java.io.File
import java.io.FilenameFilter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.HomeNavigationDirections
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R
@ -43,7 +40,7 @@ import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.fragments.AddGameFolderDialogFragment
import org.yuzu.yuzu_emu.fragments.IndeterminateProgressDialogFragment
import org.yuzu.yuzu_emu.fragments.MessageDialogFragment
import org.yuzu.yuzu_emu.getPublicFilesDir
import org.yuzu.yuzu_emu.model.AddonViewModel
import org.yuzu.yuzu_emu.model.GamesViewModel
import org.yuzu.yuzu_emu.model.HomeViewModel
import org.yuzu.yuzu_emu.model.TaskState
@ -60,15 +57,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
private val homeViewModel: HomeViewModel by viewModels()
private val gamesViewModel: GamesViewModel by viewModels()
private val taskViewModel: TaskViewModel by viewModels()
private val addonViewModel: AddonViewModel by viewModels()
override var themeId: Int = 0
private val savesFolder
get() = "${getPublicFilesDir().canonicalPath}/nand/user/save/0000000000000000"
// Get first subfolder in saves folder (should be the user folder)
val savesFolderRoot get() = File(savesFolder).listFiles()?.firstOrNull()?.canonicalPath ?: ""
override fun onCreate(savedInstanceState: Bundle?) {
val splashScreen = installSplashScreen()
splashScreen.setKeepOnScreenCondition { !DirectoryInitialization.areDirectoriesReady }
@ -145,6 +137,16 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
homeViewModel.statusBarShadeVisible.collect { showStatusBarShade(it) }
}
}
launch {
repeatOnLifecycle(Lifecycle.State.CREATED) {
homeViewModel.contentToInstall.collect {
if (it != null) {
installContent(it)
homeViewModel.setContentToInstall(null)
}
}
}
}
}
// Dismiss previous notifications (should not happen unless a crash occurred)
@ -253,13 +255,6 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
super.onResume()
}
override fun onStop() {
super.onStop()
CoroutineScope(Dispatchers.IO).launch {
NativeConfig.saveSettings()
}
}
override fun onDestroy() {
EmulationActivity.stopForegroundService(this)
super.onDestroy()
@ -468,110 +463,150 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
val installGameUpdate = registerForActivityResult(
ActivityResultContracts.OpenMultipleDocuments()
) { documents: List<Uri> ->
if (documents.isNotEmpty()) {
IndeterminateProgressDialogFragment.newInstance(
this@MainActivity,
R.string.installing_game_content
) {
var installSuccess = 0
var installOverwrite = 0
var errorBaseGame = 0
var errorExtension = 0
var errorOther = 0
documents.forEach {
when (
NativeLibrary.installFileToNand(
it.toString(),
FileUtil.getExtension(it)
)
) {
NativeLibrary.InstallFileToNandResult.Success -> {
installSuccess += 1
}
NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> {
installOverwrite += 1
}
NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> {
errorBaseGame += 1
}
NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> {
errorExtension += 1
}
else -> {
errorOther += 1
}
}
}
val separator = System.getProperty("line.separator") ?: "\n"
val installResult = StringBuilder()
if (installSuccess > 0) {
installResult.append(
getString(
R.string.install_game_content_success_install,
installSuccess
)
)
installResult.append(separator)
}
if (installOverwrite > 0) {
installResult.append(
getString(
R.string.install_game_content_success_overwrite,
installOverwrite
)
)
installResult.append(separator)
}
val errorTotal: Int = errorBaseGame + errorExtension + errorOther
if (errorTotal > 0) {
installResult.append(separator)
installResult.append(
getString(
R.string.install_game_content_failed_count,
errorTotal
)
)
installResult.append(separator)
if (errorBaseGame > 0) {
installResult.append(separator)
installResult.append(
getString(R.string.install_game_content_failure_base)
)
installResult.append(separator)
}
if (errorExtension > 0) {
installResult.append(separator)
installResult.append(
getString(R.string.install_game_content_failure_file_extension)
)
installResult.append(separator)
}
if (errorOther > 0) {
installResult.append(
getString(R.string.install_game_content_failure_description)
)
installResult.append(separator)
}
return@newInstance MessageDialogFragment.newInstance(
this,
titleId = R.string.install_game_content_failure,
descriptionString = installResult.toString().trim(),
helpLinkId = R.string.install_game_content_help_link
)
} else {
return@newInstance MessageDialogFragment.newInstance(
this,
titleId = R.string.install_game_content_success,
descriptionString = installResult.toString().trim()
)
}
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
if (documents.isEmpty()) {
return@registerForActivityResult
}
if (addonViewModel.game == null) {
installContent(documents)
return@registerForActivityResult
}
IndeterminateProgressDialogFragment.newInstance(
this@MainActivity,
R.string.verifying_content,
false
) {
var updatesMatchProgram = true
for (document in documents) {
val valid = NativeLibrary.doesUpdateMatchProgram(
addonViewModel.game!!.programId,
document.toString()
)
if (!valid) {
updatesMatchProgram = false
break
}
}
if (updatesMatchProgram) {
homeViewModel.setContentToInstall(documents)
} else {
MessageDialogFragment.newInstance(
this@MainActivity,
titleId = R.string.content_install_notice,
descriptionId = R.string.content_install_notice_description,
positiveAction = { homeViewModel.setContentToInstall(documents) }
)
}
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
}
private fun installContent(documents: List<Uri>) {
IndeterminateProgressDialogFragment.newInstance(
this@MainActivity,
R.string.installing_game_content
) {
var installSuccess = 0
var installOverwrite = 0
var errorBaseGame = 0
var errorExtension = 0
var errorOther = 0
documents.forEach {
when (
NativeLibrary.installFileToNand(
it.toString(),
FileUtil.getExtension(it)
)
) {
NativeLibrary.InstallFileToNandResult.Success -> {
installSuccess += 1
}
NativeLibrary.InstallFileToNandResult.SuccessFileOverwritten -> {
installOverwrite += 1
}
NativeLibrary.InstallFileToNandResult.ErrorBaseGame -> {
errorBaseGame += 1
}
NativeLibrary.InstallFileToNandResult.ErrorFilenameExtension -> {
errorExtension += 1
}
else -> {
errorOther += 1
}
}
}
addonViewModel.refreshAddons()
val separator = System.getProperty("line.separator") ?: "\n"
val installResult = StringBuilder()
if (installSuccess > 0) {
installResult.append(
getString(
R.string.install_game_content_success_install,
installSuccess
)
)
installResult.append(separator)
}
if (installOverwrite > 0) {
installResult.append(
getString(
R.string.install_game_content_success_overwrite,
installOverwrite
)
)
installResult.append(separator)
}
val errorTotal: Int = errorBaseGame + errorExtension + errorOther
if (errorTotal > 0) {
installResult.append(separator)
installResult.append(
getString(
R.string.install_game_content_failed_count,
errorTotal
)
)
installResult.append(separator)
if (errorBaseGame > 0) {
installResult.append(separator)
installResult.append(
getString(R.string.install_game_content_failure_base)
)
installResult.append(separator)
}
if (errorExtension > 0) {
installResult.append(separator)
installResult.append(
getString(R.string.install_game_content_failure_file_extension)
)
installResult.append(separator)
}
if (errorOther > 0) {
installResult.append(
getString(R.string.install_game_content_failure_description)
)
installResult.append(separator)
}
return@newInstance MessageDialogFragment.newInstance(
this,
titleId = R.string.install_game_content_failure,
descriptionString = installResult.toString().trim(),
helpLinkId = R.string.install_game_content_help_link
)
} else {
return@newInstance MessageDialogFragment.newInstance(
this,
titleId = R.string.install_game_content_success,
descriptionString = installResult.toString().trim()
)
}
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
}
val exportUserData = registerForActivityResult(
@ -632,7 +667,7 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
}
// Clear existing user data
NativeConfig.unloadConfig()
NativeConfig.unloadGlobalConfig()
File(DirectoryInitialization.userDirectory!!).deleteRecursively()
// Copy archive to internal storage
@ -651,108 +686,10 @@ class MainActivity : AppCompatActivity(), ThemeProvider {
// Reinitialize relevant data
NativeLibrary.initializeSystem(true)
NativeConfig.initializeConfig()
NativeConfig.initializeGlobalConfig()
gamesViewModel.reloadGames(false)
return@newInstance getString(R.string.user_data_import_success)
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
}
/**
* Exports the save file located in the given folder path by creating a zip file and sharing it via intent.
*/
val exportSaves = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/zip")
) { result ->
if (result == null) {
return@registerForActivityResult
}
IndeterminateProgressDialogFragment.newInstance(
this,
R.string.save_files_exporting,
false
) {
val zipResult = FileUtil.zipFromInternalStorage(
File(savesFolderRoot),
savesFolderRoot,
BufferedOutputStream(contentResolver.openOutputStream(result))
)
return@newInstance when (zipResult) {
TaskState.Completed -> getString(R.string.export_success)
TaskState.Cancelled, TaskState.Failed -> getString(R.string.export_failed)
}
}.show(supportFragmentManager, IndeterminateProgressDialogFragment.TAG)
}
private val startForResultExportSave =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { _ ->
File(getPublicFilesDir().canonicalPath, "temp").deleteRecursively()
}
val importSaves =
registerForActivityResult(ActivityResultContracts.OpenDocument()) { result ->
if (result == null) {
return@registerForActivityResult
}
NativeLibrary.initializeEmptyUserDirectory()
val inputZip = contentResolver.openInputStream(result)
// A zip needs to have at least one subfolder named after a TitleId in order to be considered valid.
var validZip = false
val savesFolder = File(savesFolderRoot)
val cacheSaveDir = File("${applicationContext.cacheDir.path}/saves/")
cacheSaveDir.mkdir()
if (inputZip == null) {
Toast.makeText(
applicationContext,
getString(R.string.fatal_error),
Toast.LENGTH_LONG
).show()
return@registerForActivityResult
}
val filterTitleId =
FilenameFilter { _, dirName -> dirName.matches(Regex("^0100[\\dA-Fa-f]{12}$")) }
try {
CoroutineScope(Dispatchers.IO).launch {
FileUtil.unzipToInternalStorage(BufferedInputStream(inputZip), cacheSaveDir)
cacheSaveDir.list(filterTitleId)?.forEach { savePath ->
File(savesFolder, savePath).deleteRecursively()
File(cacheSaveDir, savePath).copyRecursively(
File(savesFolder, savePath),
true
)
validZip = true
}
withContext(Dispatchers.Main) {
if (!validZip) {
MessageDialogFragment.newInstance(
this@MainActivity,
titleId = R.string.save_file_invalid_zip_structure,
descriptionId = R.string.save_file_invalid_zip_structure_description
).show(supportFragmentManager, MessageDialogFragment.TAG)
return@withContext
}
Toast.makeText(
applicationContext,
getString(R.string.save_file_imported_success),
Toast.LENGTH_LONG
).show()
}
cacheSaveDir.deleteRecursively()
}
} catch (e: Exception) {
Toast.makeText(
applicationContext,
getString(R.string.fatal_error),
Toast.LENGTH_LONG
).show()
}
}
}

View File

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.utils
object AddonUtil {
val validAddonDirectories = listOf("cheats", "exefs", "romfs")
}

View File

@ -3,9 +3,17 @@
package org.yuzu.yuzu_emu.utils
import androidx.preference.PreferenceManager
import java.io.IOException
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
import org.yuzu.yuzu_emu.overlay.model.OverlayControl
import org.yuzu.yuzu_emu.overlay.model.OverlayLayout
import org.yuzu.yuzu_emu.utils.PreferenceUtil.migratePreference
object DirectoryInitialization {
private var userPath: String? = null
@ -16,7 +24,8 @@ object DirectoryInitialization {
if (!areDirectoriesReady) {
initializeInternalStorage()
NativeLibrary.initializeSystem(false)
NativeConfig.initializeConfig()
NativeConfig.initializeGlobalConfig()
migrateSettings()
areDirectoriesReady = true
}
}
@ -35,4 +44,170 @@ object DirectoryInitialization {
e.printStackTrace()
}
}
private fun migrateSettings() {
val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
var saveConfig = false
val theme = preferences.migratePreference<Int>(Settings.PREF_THEME)
if (theme != null) {
IntSetting.THEME.setInt(theme)
saveConfig = true
}
val themeMode = preferences.migratePreference<Int>(Settings.PREF_THEME_MODE)
if (themeMode != null) {
IntSetting.THEME_MODE.setInt(themeMode)
saveConfig = true
}
val blackBackgrounds =
preferences.migratePreference<Boolean>(Settings.PREF_BLACK_BACKGROUNDS)
if (blackBackgrounds != null) {
BooleanSetting.BLACK_BACKGROUNDS.setBoolean(blackBackgrounds)
saveConfig = true
}
val joystickRelCenter =
preferences.migratePreference<Boolean>(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER)
if (joystickRelCenter != null) {
BooleanSetting.JOYSTICK_REL_CENTER.setBoolean(joystickRelCenter)
saveConfig = true
}
val dpadSlide =
preferences.migratePreference<Boolean>(Settings.PREF_MENU_SETTINGS_DPAD_SLIDE)
if (dpadSlide != null) {
BooleanSetting.DPAD_SLIDE.setBoolean(dpadSlide)
saveConfig = true
}
val hapticFeedback =
preferences.migratePreference<Boolean>(Settings.PREF_MENU_SETTINGS_HAPTICS)
if (hapticFeedback != null) {
BooleanSetting.HAPTIC_FEEDBACK.setBoolean(hapticFeedback)
saveConfig = true
}
val showPerformanceOverlay =
preferences.migratePreference<Boolean>(Settings.PREF_MENU_SETTINGS_SHOW_FPS)
if (showPerformanceOverlay != null) {
BooleanSetting.SHOW_PERFORMANCE_OVERLAY.setBoolean(showPerformanceOverlay)
saveConfig = true
}
val showInputOverlay =
preferences.migratePreference<Boolean>(Settings.PREF_MENU_SETTINGS_SHOW_OVERLAY)
if (showInputOverlay != null) {
BooleanSetting.SHOW_INPUT_OVERLAY.setBoolean(showInputOverlay)
saveConfig = true
}
val overlayOpacity = preferences.migratePreference<Int>(Settings.PREF_CONTROL_OPACITY)
if (overlayOpacity != null) {
IntSetting.OVERLAY_OPACITY.setInt(overlayOpacity)
saveConfig = true
}
val overlayScale = preferences.migratePreference<Int>(Settings.PREF_CONTROL_SCALE)
if (overlayScale != null) {
IntSetting.OVERLAY_SCALE.setInt(overlayScale)
saveConfig = true
}
var setOverlayData = false
val overlayControlData = NativeConfig.getOverlayControlData()
if (overlayControlData.isEmpty()) {
val overlayControlDataMap =
NativeConfig.getOverlayControlData().associateBy { it.id }.toMutableMap()
for (button in Settings.overlayPreferences) {
val buttonId = convertButtonId(button)
var buttonEnabled = preferences.migratePreference<Boolean>(button)
if (buttonEnabled == null) {
buttonEnabled = OverlayControl.map[buttonId]?.defaultVisibility == true
}
var landscapeXPosition = preferences.migratePreference<Float>(
"$button-X${Settings.PREF_LANDSCAPE_SUFFIX}"
)?.toDouble()
var landscapeYPosition = preferences.migratePreference<Float>(
"$button-Y${Settings.PREF_LANDSCAPE_SUFFIX}"
)?.toDouble()
if (landscapeXPosition == null || landscapeYPosition == null) {
val landscapePosition = OverlayControl.map[buttonId]
?.getDefaultPositionForLayout(OverlayLayout.Landscape) ?: Pair(0.0, 0.0)
landscapeXPosition = landscapePosition.first
landscapeYPosition = landscapePosition.second
}
var portraitXPosition = preferences.migratePreference<Float>(
"$button-X${Settings.PREF_PORTRAIT_SUFFIX}"
)?.toDouble()
var portraitYPosition = preferences.migratePreference<Float>(
"$button-Y${Settings.PREF_PORTRAIT_SUFFIX}"
)?.toDouble()
if (portraitXPosition == null || portraitYPosition == null) {
val portraitPosition = OverlayControl.map[buttonId]
?.getDefaultPositionForLayout(OverlayLayout.Portrait) ?: Pair(0.0, 0.0)
portraitXPosition = portraitPosition.first
portraitYPosition = portraitPosition.second
}
var foldableXPosition = preferences.migratePreference<Float>(
"$button-X${Settings.PREF_FOLDABLE_SUFFIX}"
)?.toDouble()
var foldableYPosition = preferences.migratePreference<Float>(
"$button-Y${Settings.PREF_FOLDABLE_SUFFIX}"
)?.toDouble()
if (foldableXPosition == null || foldableYPosition == null) {
val foldablePosition = OverlayControl.map[buttonId]
?.getDefaultPositionForLayout(OverlayLayout.Foldable) ?: Pair(0.0, 0.0)
foldableXPosition = foldablePosition.first
foldableYPosition = foldablePosition.second
}
val controlData = OverlayControlData(
buttonId,
buttonEnabled,
Pair(landscapeXPosition, landscapeYPosition),
Pair(portraitXPosition, portraitYPosition),
Pair(foldableXPosition, foldableYPosition)
)
overlayControlDataMap[buttonId] = controlData
setOverlayData = true
}
if (setOverlayData) {
NativeConfig.setOverlayControlData(
overlayControlDataMap.map { it.value }.toTypedArray()
)
saveConfig = true
}
}
if (saveConfig) {
NativeConfig.saveGlobalConfig()
}
}
private fun convertButtonId(buttonId: String): String =
when (buttonId) {
Settings.PREF_BUTTON_A -> OverlayControl.BUTTON_A.id
Settings.PREF_BUTTON_B -> OverlayControl.BUTTON_B.id
Settings.PREF_BUTTON_X -> OverlayControl.BUTTON_X.id
Settings.PREF_BUTTON_Y -> OverlayControl.BUTTON_Y.id
Settings.PREF_BUTTON_L -> OverlayControl.BUTTON_L.id
Settings.PREF_BUTTON_R -> OverlayControl.BUTTON_R.id
Settings.PREF_BUTTON_ZL -> OverlayControl.BUTTON_ZL.id
Settings.PREF_BUTTON_ZR -> OverlayControl.BUTTON_ZR.id
Settings.PREF_BUTTON_PLUS -> OverlayControl.BUTTON_PLUS.id
Settings.PREF_BUTTON_MINUS -> OverlayControl.BUTTON_MINUS.id
Settings.PREF_BUTTON_DPAD -> OverlayControl.COMBINED_DPAD.id
Settings.PREF_STICK_L -> OverlayControl.STICK_L.id
Settings.PREF_STICK_R -> OverlayControl.STICK_R.id
Settings.PREF_BUTTON_HOME -> OverlayControl.BUTTON_HOME.id
Settings.PREF_BUTTON_SCREENSHOT -> OverlayControl.BUTTON_CAPTURE.id
Settings.PREF_BUTTON_STICK_L -> OverlayControl.BUTTON_STICK_L.id
Settings.PREF_BUTTON_STICK_R -> OverlayControl.BUTTON_STICK_R.id
else -> ""
}
}

View File

@ -1,50 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.utils
import androidx.preference.PreferenceManager
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.Settings
object EmulationMenuSettings {
private val preferences =
PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
var joystickRelCenter: Boolean
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, true)
set(value) {
preferences.edit()
.putBoolean(Settings.PREF_MENU_SETTINGS_JOYSTICK_REL_CENTER, value)
.apply()
}
var dpadSlide: Boolean
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_DPAD_SLIDE, true)
set(value) {
preferences.edit()
.putBoolean(Settings.PREF_MENU_SETTINGS_DPAD_SLIDE, value)
.apply()
}
var hapticFeedback: Boolean
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_HAPTICS, false)
set(value) {
preferences.edit()
.putBoolean(Settings.PREF_MENU_SETTINGS_HAPTICS, value)
.apply()
}
var showFps: Boolean
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, false)
set(value) {
preferences.edit()
.putBoolean(Settings.PREF_MENU_SETTINGS_SHOW_FPS, value)
.apply()
}
var showOverlay: Boolean
get() = preferences.getBoolean(Settings.PREF_MENU_SETTINGS_SHOW_OVERLAY, true)
set(value) {
preferences.edit()
.putBoolean(Settings.PREF_MENU_SETTINGS_SHOW_OVERLAY, value)
.apply()
}
}

View File

@ -22,6 +22,7 @@ import java.io.BufferedOutputStream
import java.lang.NullPointerException
import java.nio.charset.StandardCharsets
import java.util.zip.ZipOutputStream
import kotlin.IllegalStateException
object FileUtil {
const val PATH_TREE = "tree"
@ -342,6 +343,37 @@ object FileUtil {
return TaskState.Completed
}
/**
* Helper function that copies the contents of a DocumentFile folder into a [File]
* @param file [File] representation of the folder to copy into
* @throws IllegalStateException Fails when trying to copy a folder into a file and vice versa
*/
fun DocumentFile.copyFilesTo(file: File) {
file.mkdirs()
if (!this.isDirectory || !file.isDirectory) {
throw IllegalStateException(
"[FileUtil] Tried to copy a folder into a file or vice versa"
)
}
this.listFiles().forEach {
val newFile = File(file, it.name!!)
if (it.isDirectory) {
newFile.mkdirs()
DocumentFile.fromTreeUri(YuzuApplication.appContext, it.uri)?.copyFilesTo(newFile)
} else {
val inputStream =
YuzuApplication.appContext.contentResolver.openInputStream(it.uri)
BufferedInputStream(inputStream).use { bos ->
if (!newFile.exists()) {
newFile.createNewFile()
}
newFile.outputStream().use { os -> bos.copyTo(os) }
}
}
}
}
fun isRootTreeUri(uri: Uri): Boolean {
val paths = uri.pathSegments
return paths.size == 2 && PATH_TREE == paths[0]

View File

@ -36,6 +36,12 @@ object GameHelper {
// Ensure keys are loaded so that ROM metadata can be decrypted.
NativeLibrary.reloadKeys()
// Reset metadata so we don't use stale information
GameMetadata.resetMetadata()
// Remove previous filesystem provider information so we can get up to date version info
NativeLibrary.clearFilesystemProvider()
val badDirs = mutableListOf<Int>()
gameDirs.forEachIndexed { index: Int, gameDir: GameDir ->
val gameDirUri = Uri.parse(gameDir.uriString)
@ -92,14 +98,24 @@ object GameHelper {
)
} else {
if (Game.extensions.contains(FileUtil.getExtension(it.uri))) {
games.add(getGame(it.uri, true))
val game = getGame(it.uri, true)
if (game != null) {
games.add(game)
}
}
}
}
}
fun getGame(uri: Uri, addedToLibrary: Boolean): Game {
fun getGame(uri: Uri, addedToLibrary: Boolean): Game? {
val filePath = uri.toString()
if (!GameMetadata.getIsValid(filePath)) {
return null
}
// Needed to update installed content information
NativeLibrary.addFileToFilesystemProvider(filePath)
var name = GameMetadata.getTitle(filePath)
// If the game's title field is empty, use the filename.
@ -118,7 +134,7 @@ object GameHelper {
filePath,
programId,
GameMetadata.getDeveloper(filePath),
GameMetadata.getVersion(filePath),
GameMetadata.getVersion(filePath, false),
GameMetadata.getIsHomebrew(filePath)
)

View File

@ -4,13 +4,15 @@
package org.yuzu.yuzu_emu.utils
object GameMetadata {
external fun getIsValid(path: String): Boolean
external fun getTitle(path: String): String
external fun getProgramId(path: String): String
external fun getDeveloper(path: String): String
external fun getVersion(path: String): String
external fun getVersion(path: String, reload: Boolean): String
external fun getIcon(path: String): ByteArray

View File

@ -10,6 +10,8 @@ import java.io.File
import java.io.IOException
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.StringSetting
import java.io.FileNotFoundException
import java.util.zip.ZipException
import java.util.zip.ZipFile
@ -44,7 +46,7 @@ object GpuDriverHelper {
NativeLibrary.initializeGpuDriver(
hookLibPath,
driverInstallationPath,
customDriverData.libraryName,
installedCustomDriverData.libraryName,
fileRedirectionPath
)
}
@ -190,6 +192,7 @@ object GpuDriverHelper {
}
}
} catch (_: ZipException) {
} catch (_: FileNotFoundException) {
}
return GpuDriverMetadata()
}
@ -197,9 +200,12 @@ object GpuDriverHelper {
external fun supportsCustomDriverLoading(): Boolean
// Parse the custom driver metadata to retrieve the name.
val customDriverData: GpuDriverMetadata
val installedCustomDriverData: GpuDriverMetadata
get() = GpuDriverMetadata(File(driverInstallationPath + META_JSON_FILENAME))
val customDriverSettingData: GpuDriverMetadata
get() = getMetadataFromZip(File(StringSetting.DRIVER_PATH.getString()))
fun initializeDirectories() {
// Ensure the file redirection directory exists.
val fileRedirectionDir = File(fileRedirectionPath!!)

View File

@ -27,13 +27,13 @@ object MemoryUtil {
const val Pb = Tb * 1024
const val Eb = Pb * 1024
private fun bytesToSizeUnit(size: Float, roundUp: Boolean = false): String =
fun bytesToSizeUnit(size: Float, roundUp: Boolean = false): String =
when {
size < Kb -> {
context.getString(
R.string.memory_formatted,
size.hundredths,
context.getString(R.string.memory_byte)
context.getString(R.string.memory_byte_shorthand)
)
}
size < Mb -> {

View File

@ -4,59 +4,117 @@
package org.yuzu.yuzu_emu.utils
import org.yuzu.yuzu_emu.model.GameDir
import org.yuzu.yuzu_emu.overlay.model.OverlayControlData
object NativeConfig {
/**
* Creates a Config object and opens the emulation config.
* Loads global config.
*/
@Synchronized
external fun initializeConfig()
external fun initializeGlobalConfig()
/**
* Destroys the stored config object. This automatically saves the existing config.
* Destroys the stored global config object. This does not save the existing config.
*/
@Synchronized
external fun unloadConfig()
external fun unloadGlobalConfig()
/**
* Reads values saved to the config file and saves them.
* Reads values in the global config file and saves them.
*/
@Synchronized
external fun reloadSettings()
external fun reloadGlobalConfig()
/**
* Saves settings values in memory to disk.
* Saves global settings values in memory to disk.
*/
@Synchronized
external fun saveSettings()
external fun saveGlobalConfig()
external fun getBoolean(key: String, getDefault: Boolean): Boolean
/**
* Creates per-game config for the specified parameters. Must be unloaded once per-game config
* is closed with [unloadPerGameConfig]. All switchable values that [NativeConfig] gets/sets
* will follow the per-game config until the global config is reloaded.
*
* @param programId String representation of the u64 programId
* @param fileName Filename of the game, including its extension
*/
@Synchronized
external fun initializePerGameConfig(programId: String, fileName: String)
@Synchronized
external fun isPerGameConfigLoaded(): Boolean
/**
* Saves per-game settings values in memory to disk.
*/
@Synchronized
external fun savePerGameConfig()
/**
* Destroys the stored per-game config object. This does not save the config.
*/
@Synchronized
external fun unloadPerGameConfig()
@Synchronized
external fun getBoolean(key: String, needsGlobal: Boolean): Boolean
@Synchronized
external fun setBoolean(key: String, value: Boolean)
external fun getByte(key: String, getDefault: Boolean): Byte
@Synchronized
external fun getByte(key: String, needsGlobal: Boolean): Byte
@Synchronized
external fun setByte(key: String, value: Byte)
external fun getShort(key: String, getDefault: Boolean): Short
@Synchronized
external fun getShort(key: String, needsGlobal: Boolean): Short
@Synchronized
external fun setShort(key: String, value: Short)
external fun getInt(key: String, getDefault: Boolean): Int
@Synchronized
external fun getInt(key: String, needsGlobal: Boolean): Int
@Synchronized
external fun setInt(key: String, value: Int)
external fun getFloat(key: String, getDefault: Boolean): Float
@Synchronized
external fun getFloat(key: String, needsGlobal: Boolean): Float
@Synchronized
external fun setFloat(key: String, value: Float)
external fun getLong(key: String, getDefault: Boolean): Long
@Synchronized
external fun getLong(key: String, needsGlobal: Boolean): Long
@Synchronized
external fun setLong(key: String, value: Long)
external fun getString(key: String, getDefault: Boolean): String
@Synchronized
external fun getString(key: String, needsGlobal: Boolean): String
@Synchronized
external fun setString(key: String, value: String)
external fun getIsRuntimeModifiable(key: String): Boolean
external fun getConfigHeader(category: Int): String
external fun getPairedSettingKey(key: String): String
external fun getIsSwitchable(key: String): Boolean
@Synchronized
external fun usingGlobal(key: String): Boolean
@Synchronized
external fun setGlobal(key: String, global: Boolean)
external fun getIsSaveable(key: String): Boolean
external fun getDefaultToString(key: String): String
/**
* Gets every [GameDir] in AndroidSettings::values.game_dirs
*/
@ -74,4 +132,40 @@ object NativeConfig {
*/
@Synchronized
external fun addGameDir(dir: GameDir)
/**
* Gets an array of the addons that are disabled for a given game
*
* @param programId String representation of a game's program ID
* @return An array of disabled addons
*/
@Synchronized
external fun getDisabledAddons(programId: String): Array<String>
/**
* Clears the disabled addons array corresponding to [programId] and replaces them
* with [disabledAddons]
*
* @param programId String representation of a game's program ID
* @param disabledAddons Replacement array of disabled addons
*/
@Synchronized
external fun setDisabledAddons(programId: String, disabledAddons: Array<String>)
/**
* Gets an array of [OverlayControlData] from settings
*
* @return An array of [OverlayControlData]
*/
@Synchronized
external fun getOverlayControlData(): Array<OverlayControlData>
/**
* Clears the AndroidSettings::values.overlay_control_data array and replaces its values
* with [overlayControlData]
*
* @param overlayControlData Replacement array of [OverlayControlData]
*/
@Synchronized
external fun setOverlayControlData(overlayControlData: Array<OverlayControlData>)
}

View File

@ -0,0 +1,37 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.utils
import android.content.SharedPreferences
object PreferenceUtil {
/**
* Retrieves a shared preference value and then deletes the value in storage.
* @param key Associated key for the value in this preferences instance
* @return Typed value associated with [key]. Null if no such key exists.
*/
inline fun <reified T> SharedPreferences.migratePreference(key: String): T? {
if (!this.contains(key)) {
return null
}
val value: Any = when (T::class) {
String::class -> this.getString(key, "")!!
Boolean::class -> this.getBoolean(key, false)
Int::class -> this.getInt(key, 0)
Float::class -> this.getFloat(key, 0f)
Long::class -> this.getLong(key, 0)
else -> throw IllegalStateException("Tried to migrate preference with invalid type!")
}
deletePreference(key)
return value as T
}
fun SharedPreferences.deletePreference(key: String) = this.edit().remove(key).apply()
}

View File

@ -5,38 +5,38 @@ package org.yuzu.yuzu_emu.utils
import android.content.res.Configuration
import android.graphics.Color
import android.os.Build
import androidx.annotation.ColorInt
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.preference.PreferenceManager
import kotlin.math.roundToInt
import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.YuzuApplication
import org.yuzu.yuzu_emu.features.settings.model.Settings
import org.yuzu.yuzu_emu.features.settings.model.BooleanSetting
import org.yuzu.yuzu_emu.features.settings.model.IntSetting
import org.yuzu.yuzu_emu.ui.main.ThemeProvider
object ThemeHelper {
const val SYSTEM_BAR_ALPHA = 0.9f
private const val DEFAULT = 0
private const val MATERIAL_YOU = 1
fun setTheme(activity: AppCompatActivity) {
val preferences = PreferenceManager.getDefaultSharedPreferences(YuzuApplication.appContext)
setThemeMode(activity)
when (preferences.getInt(Settings.PREF_THEME, 0)) {
DEFAULT -> activity.setTheme(R.style.Theme_Yuzu_Main)
MATERIAL_YOU -> activity.setTheme(R.style.Theme_Yuzu_Main_MaterialYou)
when (Theme.from(IntSetting.THEME.getInt())) {
Theme.Default -> activity.setTheme(R.style.Theme_Yuzu_Main)
Theme.MaterialYou -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
activity.setTheme(R.style.Theme_Yuzu_Main_MaterialYou)
} else {
activity.setTheme(R.style.Theme_Yuzu_Main)
}
}
}
// Using a specific night mode check because this could apply incorrectly when using the
// light app mode, dark system mode, and black backgrounds. Launching the settings activity
// will then show light mode colors/navigation bars but with black backgrounds.
if (preferences.getBoolean(Settings.PREF_BLACK_BACKGROUNDS, false) &&
isNightMode(activity)
) {
if (BooleanSetting.BLACK_BACKGROUNDS.getBoolean() && isNightMode(activity)) {
activity.setTheme(R.style.ThemeOverlay_Yuzu_Dark)
}
}
@ -60,8 +60,7 @@ object ThemeHelper {
}
fun setThemeMode(activity: AppCompatActivity) {
val themeMode = PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
.getInt(Settings.PREF_THEME_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
val themeMode = IntSetting.THEME_MODE.getInt()
activity.delegate.localNightMode = themeMode
val windowController = WindowCompat.getInsetsController(
activity.window,
@ -95,3 +94,12 @@ object ThemeHelper {
windowController.isAppearanceLightNavigationBars = false
}
}
enum class Theme(val int: Int) {
Default(0),
MaterialYou(1);
companion object {
fun from(int: Int): Theme = entries.firstOrNull { it.int == int } ?: Default
}
}

View File

@ -9,6 +9,7 @@
#include <jni.h>
#include "common/string_util.h"
#include "jni/id_cache.h"
std::string GetJString(JNIEnv* env, jstring jstr) {
if (!jstr) {
@ -33,3 +34,11 @@ jstring ToJString(JNIEnv* env, std::string_view str) {
jstring ToJString(JNIEnv* env, std::u16string_view str) {
return ToJString(env, Common::UTF16ToUTF8(str));
}
double GetJDouble(JNIEnv* env, jobject jdouble) {
return env->GetDoubleField(jdouble, IDCache::GetDoubleValueField());
}
jobject ToJDouble(JNIEnv* env, double value) {
return env->NewObject(IDCache::GetDoubleClass(), IDCache::GetDoubleConstructor(), value);
}

View File

@ -10,3 +10,6 @@
std::string GetJString(JNIEnv* env, jstring jstr);
jstring ToJString(JNIEnv* env, std::string_view str);
jstring ToJString(JNIEnv* env, std::u16string_view str);
double GetJDouble(JNIEnv* env, jobject jdouble);
jobject ToJDouble(JNIEnv* env, double value);

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