mirror of
https://github.com/libretro/parallel-gx1.git
synced 2026-11-07 04:26:01 -10:00
Merge tag '2609' into 2606a-update
Release for September 2026
This commit is contained in:
commit
eb97db0652
123 files changed
+26388
-22047
No files matched your search
@@ -33,7 +33,7 @@ endif()
|
||||
string(TIMESTAMP DOLPHIN_WC_BUILD_DATE "%Y-%m-%d" UTC)
|
||||
|
||||
# version number
|
||||
set(DOLPHIN_VERSION_MAJOR "2606")
|
||||
set(DOLPHIN_VERSION_MAJOR "2609")
|
||||
set(DOLPHIN_VERSION_MINOR "0")
|
||||
set(DOLPHIN_VERSION_PATCH ${DOLPHIN_WC_REVISION})
|
||||
|
||||
|
||||
+9
-2
@@ -61,7 +61,7 @@ set(AppleClang_min_version 14.0.3)
|
||||
set(MSVC_min_version 19.32)
|
||||
# Standard libraries
|
||||
set(libstdc++_min_version 12) # This should match GCC_min_version's major version.
|
||||
set(libc++_min_version 150000) # This should match Clang_min_version in the format "xxyyzz" instead of "xx.yy.zz"
|
||||
set(libc++_min_version 150000) # This should match Clang_min_version in the format "xxyyzz" instead of "xx.yy.zz"
|
||||
|
||||
dolphin_check_toolset_version("Xcode" XCODE_VERSION ${Xcode_min_version})
|
||||
dolphin_check_toolset_version("MSVC Toolset" MSVC_TOOLSET_VERSION ${MSVC_toolset_min_version})
|
||||
@@ -104,7 +104,7 @@ if(APPROVED_VENDORED_DEPENDENCIES)
|
||||
message(WARNING "APPROVED_VENDORED_DEPENDENCIES is deprecated. Please migrate to setting USE_SYSTEM_LIBS to ON and setting USE_SYSTEM_<dependency> to either AUTO or OFF to allow bundled libs.")
|
||||
endif()
|
||||
option(USE_UPNP "Enables UPnP port mapping support" ON)
|
||||
option(ENABLE_NOGUI "Enable NoGUI frontend" ON)
|
||||
option(ENABLE_NOGUI "Enable NoGUI frontend" OFF)
|
||||
option(ENABLE_QT "Enable Qt (Default)" ON)
|
||||
option(ENABLE_LTO "Enables Link Time Optimization" OFF)
|
||||
option(ENABLE_GENERIC "Enables generic build that should run on any little-endian host" OFF)
|
||||
@@ -242,6 +242,12 @@ else()
|
||||
" Enable generic build if you really want a JIT-less binary.")
|
||||
endif()
|
||||
|
||||
if(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64")
|
||||
set(DOLPHIN_HOST ARM64)
|
||||
elseif(CMAKE_HOST_SYSTEM_PROCESSOR MATCHES "x64|x86_64|amd64|AMD64")
|
||||
set(DOLPHIN_HOST x64)
|
||||
endif()
|
||||
|
||||
if(CMAKE_GENERATOR MATCHES "Ninja")
|
||||
check_and_add_flag(DIAGNOSTICS_COLOR -fdiagnostics-color)
|
||||
elseif(CMAKE_GENERATOR MATCHES "Visual Studio")
|
||||
@@ -280,6 +286,7 @@ if(MSVC)
|
||||
|
||||
# Additional warnings
|
||||
add_compile_options(
|
||||
/w15262 # Unannotated fallthrough between switch labels
|
||||
/w44263 # Non-virtual member function hides base class virtual function
|
||||
/w44265 # Class has virtual functions, but destructor is not virtual
|
||||
/w44946 # Reinterpret cast between related types
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"D43J01":{"loophack":"CAB9CED2D904F12CCB21F5B1DE9B5433620C3E13"},"DPOJ8P":{"Bypass Modem Detection":"90BA66E25640A538FEFA6693715718139079FA7B"},"G2BE5G":{"Disable interlaced rendering":"7FFF6BDD93713BEDFD23739C32B86153FA19AEA0"},"G2BP7D":{"Disable interlaced rendering":"56E85D7285F10348E1E5354E379918D07E79EDA9"},"G2VE08":{"16:9 Widescreen":"3ED033396382424533A263A39687A0961EC994A6"},"G2VP08":{"16:9 Widescreen":"EEBC3C5F04BB70AFCA1A4C3D638335CDC9142BA2"},"G3RP52":{"16:9 Widescreen":"7F6B7743F0DDD3704B15882330E490103AE24E36","60fps":"4C9A8572A35F1DB85BBB2F32ED34288A343F1FC0"},"G3YP52":{"16:9 Widescreen":"3D102AFEE2F4945894E74BB702ED3ED2294C6FEC","Force PAL60":"FD61581C593EA34DD358AE33819FAB310F2CE961"},"G4AEE9":{"16:9 Aspect Ratio Fix":"8C2564CF47BB6A9B57D712C90E17F459B0BB6CCD"},"G4ME69":{"16:9 Widescreen":"6FC6E5D91693FAABE25CC721F2121CB0B5CA9D12"},"G5SE7D":{"16:9 Widescreen":"16119398BFE241A84151C046E0BC5BCF89D22049","Remove Bloom":"A5F2FB2B3BFF61F34F8AB1DD5A912A0FD8DFA720"},"G5SP7D":{"16:9 Widescreen":"718AA8BD831961C33114852FDA75BB58F768F46A"},"G8ME01":{"16:9 Aspect Ratio Fix - Centered HUD":"8C2FC3FC6F7766AA8E87425BF3BDD2E46E73DE71","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"AE3DBE2604CA0660165CA870C4B80E2B523C9475","16:9 Aspect Ratio Fix - Normal HUD":"F6E8264CF14D0189219C1E2D20C4182F190F4B88","16:9 Aspect Ratio Fix - Stretched HUD":"8EA4C7B80ADE3E8FEC28EEFA3A046B6B04162E81","No Letterbox":"B1DE2A3526F2E4511F2B94A5171BF7B3C2C52B9B"},"G8MJ01":{"16:9 Aspect Ratio Fix - Centered HUD":"B3181CA1AAB11EAE9132184DA48D76E775AC0A24","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"904C0EEEF16D761F7BDF7099DEB384DD9AB26E00","16:9 Aspect Ratio Fix - Normal HUD":"631CE99BDE3F1475303C00C67E3C130D4EB2547B","16:9 Aspect Ratio Fix - Stretched HUD":"81F7AD591F12A01DE82850457ECF147998D1238F"},"G8MP01":{"16:9 Aspect Ratio Fix - Centered HUD":"75AF5D7FA49BE9C3C73EA1482B49CD9B36CE817E","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"C2E5003E74551DF693A7CB814850CA51D77549EF","16:9 Aspect Ratio Fix - Normal HUD":"FB26AA84B0CB787705E84E869B649307776A2839","16:9 Aspect Ratio Fix - Stretched HUD":"C5F8A81520193418C07C7BC4881F02BE9C4DC9BC","No Letterbox":"7828F5B0C8B470620755C16C60A5A4554F92640C"},"G9SE8P":{"16:9 Widescreen":"CD043229AC47AC93D537B6A0725A2D0BB6FEF4E8"},"G9SJ8P":{"16:9 Widescreen":"C55EF90B542AA3973352A232629A828BC22A8509"},"G9SP8P":{"16:9 Widescreen":"9521774ADEE769CFCCE86F8170FE626387E73019"},"GAFE01":{"16:9 Widescreen":"D24F6D9BE5D79D3C795C2844634A0214BB68B5DA","Make Game Save Copyable (donny2112)":"F9A812FF62A20D5440CAB79DB6AFAE068457A10E"},"GAHEGG":{"Limit internal frame rate (speed hack)":"2873E5178C8068AFA8AFE751F903A69CA9A41F16"},"GAL":{"Trophy Viewer":"DBDB50216B22CDFFF77BDD80261CD0FF75803D3B"},"GALE01r0":{"Normal C Stick Functionality in Singleplayer Modes":"9DC8D90D986FAC175F45006AFF20CF00EDFBE053","Proper 16:9 Widescreen Support":"18B4159E77013EDFE499A33281B771C442970E3B","Properly Display in 4:3":"DC2BCA7D99EBDE1DC7C68CA6AC7F543B93FD6A67"},"GALE01r1":{"Normal C Stick Functionality in Singleplayer Modes":"F6030959C1CAC92F37287D556F18B3C5298FAD5B"},"GALE01r2":{"Disable Rumble":"CFDEEA8AD2A99394BFFEA7211EF03394E7471FA9","Normal C Stick Functionality in Singleplayer Modes":"BCD918A72368B2A30E797C76994E7539A1C0B5D9","PAL Stock Icons":"6B92D95F0EEA165EA84FD753F74B0A9E2079C2D7","Proper 16:9 Widescreen Support":"CEDF18354CDCFC49398551C629E51B053331A55B","Widescreen Support v1.2 [Dan Salvato]":"D32ED4CD685F3FF649F7E749E8C182C657013918"},"GALP01":{"Normal C Stick Functionality in Singleplayer Modes":"4817F202678559291F9F76B46DC7561A45D00B77","Proper 16:9 Widescreen Support":"BFCBE098FE8679B9140A48EA1D8620D5A369697C","Properly Display in 4:3":"5BA2933B8365E21553AA273B91954A4C722B1035"},"GAZE69":{"60FPS":"BC281053C5266B21FDDC6C8996A6021DEA03D459"},"GBLE52":{"16:9 Widescreen":"C0F3AB3BBC9B21F4BF25F44E48CF1CD36D2B3F45"},"GBLP52":{"16:9 Widescreen":"797040CB097BFD369490A1BE29659929D76CE6F7"},"GBRJ18":{"16:9 Widescreen":"B3C867AB34D90E2E9C5B799D800F4C191DE279FA"},"GBSE8P":{"16:9 Widescreen":"4148D453F61A5B81E53669335BC667E651865C76"},"GBSP8P":{"16:9 Widescreen":"6CB029AE768BA5B9995D5F12C1EBB37B3D1FE242"},"GC6E01":{"16:9 Widescreen":"DE932CCAD1B5EB1FAA8D08733BD0D3940C78382A","60 FPS [Nerdzilla]":"85F98064F17AFE0FB02A99A182AF3FEB7644DCAA","Allow Memory Card saving with Savestates":"2F64F98686E62B60E466E931A9EBCD19A750FF4E"},"GC6J01":{"60 FPS [Nerdzilla]":"A6146772E37970EA5564F51A622A04792158B321","Allow Memory Card saving with Savestates":"D8F327304A88FBC717BB1F775494C5F864B9E8D2"},"GC6P01":{"16:9 Widescreen [Ralf]":"05C9016BF58A628A3FD7B16E1B9BB0769A14846F","60 FPS [Nerdzilla]":"1D3FB017C0D20754E48A8BA54EEE7DC4851BE84B","Allow Memory Card saving with Savestates":"EDEE0E28EEA1834868F2865336290FFBDFB9C6DA"},"GC8JA4":{"Fix C4 texture tiling (used for buttons and some character icons)":"843297019804192AF1FC660BC85F4B10891D0BA3"},"GCBE7D":{"16:9 Widescreen":"478DA1D596D8513C343850719155813CLine truncated
|
||||
{"D43J01":{"loophack":"CAB9CED2D904F12CCB21F5B1DE9B5433620C3E13"},"DPOJ8P":{"Bypass Modem Detection":"90BA66E25640A538FEFA6693715718139079FA7B"},"G2BE5G":{"Disable interlaced rendering":"7FFF6BDD93713BEDFD23739C32B86153FA19AEA0"},"G2BP7D":{"Disable interlaced rendering":"56E85D7285F10348E1E5354E379918D07E79EDA9"},"G2VE08":{"16:9 Widescreen":"3ED033396382424533A263A39687A0961EC994A6"},"G2VP08":{"16:9 Widescreen":"EEBC3C5F04BB70AFCA1A4C3D638335CDC9142BA2"},"G3RP52":{"16:9 Widescreen":"7F6B7743F0DDD3704B15882330E490103AE24E36","60fps":"4C9A8572A35F1DB85BBB2F32ED34288A343F1FC0"},"G3YP52":{"16:9 Widescreen":"3D102AFEE2F4945894E74BB702ED3ED2294C6FEC","Force PAL60":"FD61581C593EA34DD358AE33819FAB310F2CE961"},"G4AEE9":{"16:9 Aspect Ratio Fix":"8C2564CF47BB6A9B57D712C90E17F459B0BB6CCD"},"G4ME69":{"16:9 Widescreen":"6FC6E5D91693FAABE25CC721F2121CB0B5CA9D12"},"G5SE7D":{"16:9 Widescreen":"16119398BFE241A84151C046E0BC5BCF89D22049","Remove Bloom":"A5F2FB2B3BFF61F34F8AB1DD5A912A0FD8DFA720"},"G5SP7D":{"16:9 Widescreen":"718AA8BD831961C33114852FDA75BB58F768F46A"},"G8ME01":{"16:9 Aspect Ratio Fix - Centered HUD":"8C2FC3FC6F7766AA8E87425BF3BDD2E46E73DE71","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"AE3DBE2604CA0660165CA870C4B80E2B523C9475","16:9 Aspect Ratio Fix - Normal HUD":"F6E8264CF14D0189219C1E2D20C4182F190F4B88","16:9 Aspect Ratio Fix - Stretched HUD":"8EA4C7B80ADE3E8FEC28EEFA3A046B6B04162E81","No Letterbox":"B1DE2A3526F2E4511F2B94A5171BF7B3C2C52B9B"},"G8MJ01":{"16:9 Aspect Ratio Fix - Centered HUD":"B3181CA1AAB11EAE9132184DA48D76E775AC0A24","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"904C0EEEF16D761F7BDF7099DEB384DD9AB26E00","16:9 Aspect Ratio Fix - Normal HUD":"631CE99BDE3F1475303C00C67E3C130D4EB2547B","16:9 Aspect Ratio Fix - Stretched HUD":"81F7AD591F12A01DE82850457ECF147998D1238F"},"G8MP01":{"16:9 Aspect Ratio Fix - Centered HUD":"75AF5D7FA49BE9C3C73EA1482B49CD9B36CE817E","16:9 Aspect Ratio Fix - Centered HUD with letterboxing":"C2E5003E74551DF693A7CB814850CA51D77549EF","16:9 Aspect Ratio Fix - Normal HUD":"FB26AA84B0CB787705E84E869B649307776A2839","16:9 Aspect Ratio Fix - Stretched HUD":"C5F8A81520193418C07C7BC4881F02BE9C4DC9BC","No Letterbox":"7828F5B0C8B470620755C16C60A5A4554F92640C"},"G9SE8P":{"16:9 Widescreen":"CD043229AC47AC93D537B6A0725A2D0BB6FEF4E8"},"G9SJ8P":{"16:9 Widescreen":"C55EF90B542AA3973352A232629A828BC22A8509"},"G9SP8P":{"16:9 Widescreen":"9521774ADEE769CFCCE86F8170FE626387E73019"},"GAFE01":{"16:9 Widescreen":"D24F6D9BE5D79D3C795C2844634A0214BB68B5DA","Make Game Save Copyable (donny2112)":"F9A812FF62A20D5440CAB79DB6AFAE068457A10E"},"GAHEGG":{"Limit internal frame rate (speed hack)":"2873E5178C8068AFA8AFE751F903A69CA9A41F16"},"GAL":{"Trophy Viewer":"DBDB50216B22CDFFF77BDD80261CD0FF75803D3B"},"GALE01r0":{"Normal C Stick Functionality in Singleplayer Modes":"9DC8D90D986FAC175F45006AFF20CF00EDFBE053","Proper 16:9 Widescreen Support":"18B4159E77013EDFE499A33281B771C442970E3B","Properly Display in 4:3":"DC2BCA7D99EBDE1DC7C68CA6AC7F543B93FD6A67"},"GALE01r1":{"Normal C Stick Functionality in Singleplayer Modes":"F6030959C1CAC92F37287D556F18B3C5298FAD5B"},"GALE01r2":{"Disable Rumble":"CFDEEA8AD2A99394BFFEA7211EF03394E7471FA9","Normal C Stick Functionality in Singleplayer Modes":"BCD918A72368B2A30E797C76994E7539A1C0B5D9","PAL Stock Icons":"6B92D95F0EEA165EA84FD753F74B0A9E2079C2D7","Proper 16:9 Widescreen Support":"CEDF18354CDCFC49398551C629E51B053331A55B","Widescreen Support v1.2 [Dan Salvato]":"D32ED4CD685F3FF649F7E749E8C182C657013918"},"GALP01":{"Normal C Stick Functionality in Singleplayer Modes":"4817F202678559291F9F76B46DC7561A45D00B77","Proper 16:9 Widescreen Support":"BFCBE098FE8679B9140A48EA1D8620D5A369697C","Properly Display in 4:3":"5BA2933B8365E21553AA273B91954A4C722B1035"},"GAZE69":{"60FPS":"BC281053C5266B21FDDC6C8996A6021DEA03D459"},"GBLE52":{"16:9 Widescreen":"C0F3AB3BBC9B21F4BF25F44E48CF1CD36D2B3F45"},"GBLP52":{"16:9 Widescreen":"797040CB097BFD369490A1BE29659929D76CE6F7"},"GBRJ18":{"16:9 Widescreen":"B3C867AB34D90E2E9C5B799D800F4C191DE279FA"},"GBSE8P":{"16:9 Widescreen":"4148D453F61A5B81E53669335BC667E651865C76"},"GBSP8P":{"16:9 Widescreen":"6CB029AE768BA5B9995D5F12C1EBB37B3D1FE242"},"GC6E01":{"16:9 Widescreen":"DE932CCAD1B5EB1FAA8D08733BD0D3940C78382A","60 FPS [Nerdzilla]":"85F98064F17AFE0FB02A99A182AF3FEB7644DCAA","Allow Memory Card saving with Savestates":"2F64F98686E62B60E466E931A9EBCD19A750FF4E"},"GC6J01":{"60 FPS [Nerdzilla]":"A6146772E37970EA5564F51A622A04792158B321","Allow Memory Card saving with Savestates":"D8F327304A88FBC717BB1F775494C5F864B9E8D2"},"GC6P01":{"16:9 Widescreen [Ralf]":"05C9016BF58A628A3FD7B16E1B9BB0769A14846F","60 FPS [Nerdzilla]":"1D3FB017C0D20754E48A8BA54EEE7DC4851BE84B","Allow Memory Card saving with Savestates":"EDEE0E28EEA1834868F2865336290FFBDFB9C6DA"},"GC8JA4":{"Fix C4 texture tiling (used for buttons and some character icons)":"843297019804192AF1FC660BC85F4B10891D0BA3"},"GCBE7D":{"16:9 Widescreen":"478DA1D596D8513C343850719155813CLine truncated
|
||||
@@ -10,5 +10,9 @@
|
||||
# Add action replay cheats here.
|
||||
|
||||
[Video_Hacks]
|
||||
# Required for Smeargle Paint
|
||||
EFBToTextureEnable = False
|
||||
|
||||
[Video_Settings]
|
||||
# Avoid skipping frames in the anime (most noticeable in part 3)
|
||||
SafeTextureCacheColorSamples = 2048
|
||||
@@ -9,5 +9,15 @@ $viWidth 704 Aspect Ratio Fix
|
||||
0004968F 000000C0
|
||||
000496A3 000000E0
|
||||
|
||||
[Gecko]
|
||||
$Increase Player LoD Range [retroben]
|
||||
48000000 80343558
|
||||
DE000000 80008180
|
||||
120000A8 00004330
|
||||
E0000000 80008000
|
||||
|
||||
[AR_RetroAchievements_Verified]
|
||||
$viWidth 704 Aspect Ratio Fix
|
||||
|
||||
[Gecko_RetroAchievements_Verified]
|
||||
$Increase Player LoD Range
|
||||
@@ -5,6 +5,43 @@
|
||||
|
||||
[ActionReplay]
|
||||
# Add action replay cheats here.
|
||||
$Border Fix
|
||||
0446E504 44200000
|
||||
04335720 44200000
|
||||
0446E500 43DC0000
|
||||
04335724 43DC0000
|
||||
$16:9
|
||||
0446CE94 3F9C71C7
|
||||
0446D668 40AAAAAB
|
||||
0446D724 3F9C71C7
|
||||
0446D870 40AAAAAB
|
||||
0446DF70 3F9C71C7
|
||||
0446DF74 40D55555
|
||||
0446F358 3F800000
|
||||
0419AA0C ECA62824
|
||||
0419AA2C 48074AA5
|
||||
0419AA48 48074A89
|
||||
041B7788 ECA62824
|
||||
041B77A8 48057D29
|
||||
041B77C4 48057D0D
|
||||
041BA070 ECA62824
|
||||
041F343C 4801C095
|
||||
041F5548 48019F89
|
||||
0420F4CC 480000F4
|
||||
0420F4D0 9421FFE0
|
||||
0420F4D4 7C0802A6
|
||||
0420F4D8 90010024
|
||||
0420F4DC 4808E999
|
||||
0420F4E0 C002D7B8
|
||||
0420F4E4 EC210024
|
||||
0420F4E8 80010024
|
||||
0420F4EC 38210020
|
||||
0420F4F0 7C0803A6
|
||||
04271490 EC631824
|
||||
04271494 EC631024
|
||||
04271498 C042D7C0
|
||||
0427149C D85F0004
|
||||
0427150C C122C958
|
||||
$Inf Ammo/No Reloads
|
||||
04122D9C 60000000
|
||||
04122D70 60000000
|
||||
@@ -220,3 +257,7 @@ $13th Loaded Player Size Modifier (Twilight Spectre)
|
||||
$14th Loaded Player Size Modifier (Twilight Spectre)
|
||||
404013FC 00129F10
|
||||
424013FC 0000099C
|
||||
|
||||
[AR_RetroAchievements_Verified]
|
||||
$Border Fix
|
||||
$16:9
|
||||
@@ -0,0 +1,6 @@
|
||||
# R9IE01 - PIKMIN1 for Wii
|
||||
|
||||
[Gecko]
|
||||
$Disable Blur [Minty Meeo]
|
||||
0445ad34 00000000
|
||||
* Disables the "Motion Blur" effect that is permanently on during gameplay.
|
||||
@@ -0,0 +1,6 @@
|
||||
# R9IJ01 - PIKMIN1 for Wii
|
||||
|
||||
[Gecko]
|
||||
$Disable Blur [Minty Meeo]
|
||||
04459B34 00000000
|
||||
* Disables the "Motion Blur" effect that is permanently on during gameplay.
|
||||
@@ -0,0 +1,6 @@
|
||||
# R9IK01 - PIKMIN1 for Wii
|
||||
|
||||
[Gecko]
|
||||
$Disable Blur [Minty Meeo]
|
||||
04684354 00000000
|
||||
* Disables the "Motion Blur" effect that is permanently on during gameplay.
|
||||
@@ -0,0 +1,6 @@
|
||||
# R9IP01 - PIKMIN1 for Wii
|
||||
|
||||
[Gecko]
|
||||
$Disable Blur [Minty Meeo]
|
||||
04459D34 00000000
|
||||
* Disables the "Motion Blur" effect that is permanently on during gameplay.
|
||||
@@ -0,0 +1,6 @@
|
||||
# RDCE78, RDCP78 - Deadly Creatures
|
||||
|
||||
[Core]
|
||||
# Overclock to work around long black screen on boot.
|
||||
Overclock = 1.10
|
||||
OverclockEnable = True
|
||||
@@ -3,6 +3,11 @@
|
||||
# https://github.com/flathub/com.discordapp.Discord/wiki/Rich-Precense-(discord-rpc)#flatpak-applications
|
||||
for i in {0..9}; do
|
||||
test -S $XDG_RUNTIME_DIR/discord-ipc-$i || ln -sf {app/com.discordapp.Discord,$XDG_RUNTIME_DIR}/discord-ipc-$i;
|
||||
# Do the same for Discord Canary as a fallback
|
||||
# (will only stick if available during startup)
|
||||
test -S $XDG_RUNTIME_DIR/discord-ipc-$i || ln -sf {app/com.discordapp.DiscordCanary,$XDG_RUNTIME_DIR}/discord-ipc-$i;
|
||||
# Make any dangling links use the stable Discord though
|
||||
test -S $XDG_RUNTIME_DIR/discord-ipc-$i || ln -sf {app/com.discordapp.Discord,$XDG_RUNTIME_DIR}/discord-ipc-$i;
|
||||
done
|
||||
|
||||
dolphin-emu "$@"
|
||||
@@ -14,7 +14,9 @@ finish-args:
|
||||
# required for the emulated bluetooth adapter feature to work.
|
||||
- --allow=bluetooth
|
||||
- --filesystem=xdg-run/app/com.discordapp.Discord:create
|
||||
- --filesystem=xdg-run/discord-ipc-0:create
|
||||
# also consider Discord Canary for rich presence
|
||||
- --filesystem=xdg-run/app/com.discordapp.DiscordCanary:create
|
||||
- --filesystem=xdg-run/discord-ipc-0
|
||||
# required to disable the screensaver in various desktops
|
||||
- --talk-name=org.freedesktop.ScreenSaver
|
||||
- --talk-name=org.xfce.ScreenSaver
|
||||
|
||||
+883
-795
File diff suppressed because it is too large.
Load diff
+756
-672
File diff suppressed because it is too large.
Load diff
+752
-668
File diff suppressed because it is too large.
Load diff
+758
-674
File diff suppressed because it is too large.
Load diff
+770
-692
File diff suppressed because it is too large.
Load diff
+751
-667
File diff suppressed because it is too large.
Load diff
+763
-679
File diff suppressed because it is too large.
Load diff
+751
-667
File diff suppressed because it is too large.
Load diff
+804
-714
File diff suppressed because it is too large.
Load diff
+751
-667
File diff suppressed because it is too large.
Load diff
+795
-706
File diff suppressed because it is too large.
Load diff
+887
-717
File diff suppressed because it is too large.
Load diff
+751
-667
File diff suppressed because it is too large.
Load diff
+772
-694
File diff suppressed because it is too large.
Load diff
+795
-706
File diff suppressed because it is too large.
Load diff
+772
-699
File diff suppressed because it is too large.
Load diff
+772
-697
File diff suppressed because it is too large.
Load diff
+764
-680
File diff suppressed because it is too large.
Load diff
+767
-683
File diff suppressed because it is too large.
Load diff
+772
-693
File diff suppressed because it is too large.
Load diff
+759
-675
File diff suppressed because it is too large.
Load diff
+752
-668
File diff suppressed because it is too large.
Load diff
+790
-702
File diff suppressed because it is too large.
Load diff
+752
-668
File diff suppressed because it is too large.
Load diff
+772
-699
File diff suppressed because it is too large.
Load diff
+751
-667
File diff suppressed because it is too large.
Load diff
+951
-758
File diff suppressed because it is too large.
Load diff
+765
-685
File diff suppressed because it is too large.
Load diff
+933
-847
File diff suppressed because it is too large.
Load diff
+754
-670
File diff suppressed because it is too large.
Load diff
+18
-11
@@ -43,6 +43,7 @@ import org.dolphinemu.dolphinemu.features.infinitybase.ui.FigureSlotAdapter
|
||||
import org.dolphinemu.dolphinemu.features.input.model.ControllerInterface
|
||||
import org.dolphinemu.dolphinemu.features.input.model.DolphinSensorEventListener
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
|
||||
import org.dolphinemu.dolphinemu.utils.ContentHandler
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.Settings
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting
|
||||
@@ -104,17 +105,21 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
|
||||
}
|
||||
|
||||
val requestSkylanderFile = registerForActivityResult(
|
||||
ActivityResultContracts.GetContent()
|
||||
ActivityResultContracts.OpenDocument()
|
||||
) { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
val slot = SkylanderConfig.loadSkylander(
|
||||
skylanderSlots[skylanderSlot].portalSlot,
|
||||
uri.toString()
|
||||
)!!
|
||||
clearSkylander(skylanderSlot)
|
||||
skylanderSlots[skylanderSlot].portalSlot = slot.first!!
|
||||
skylanderSlots[skylanderSlot].label = slot.second!!
|
||||
skylandersBinding.figureManager.adapter!!.notifyItemChanged(skylanderSlot)
|
||||
)
|
||||
if (slot != null && slot.first != null && slot.second != null) {
|
||||
clearSkylander(skylanderSlot)
|
||||
skylanderSlots[skylanderSlot].portalSlot = slot.first!!
|
||||
skylanderSlots[skylanderSlot].label = slot.second!!
|
||||
skylandersBinding.figureManager.adapter?.notifyItemChanged(skylanderSlot)
|
||||
} else {
|
||||
Toast.makeText(this, R.string.skylander_load_failed, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
skylanderSlot = -1
|
||||
skylanderData = Skylander.BLANK_SKYLANDER
|
||||
}
|
||||
@@ -131,10 +136,12 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
|
||||
uri.toString(),
|
||||
skylanderSlots[skylanderSlot].portalSlot
|
||||
)
|
||||
clearSkylander(skylanderSlot)
|
||||
skylanderSlots[skylanderSlot].portalSlot = slot.first
|
||||
skylanderSlots[skylanderSlot].label = slot.second
|
||||
skylandersBinding.figureManager.adapter?.notifyItemChanged(skylanderSlot)
|
||||
if (slot.first != -1) {
|
||||
clearSkylander(skylanderSlot)
|
||||
skylanderSlots[skylanderSlot].portalSlot = slot.first
|
||||
skylanderSlots[skylanderSlot].label = slot.second
|
||||
skylandersBinding.figureManager.adapter?.notifyItemChanged(skylanderSlot)
|
||||
}
|
||||
skylanderSlot = -1
|
||||
skylanderData = Skylander.BLANK_SKYLANDER
|
||||
}
|
||||
@@ -142,7 +149,7 @@ class EmulationActivity : AppCompatActivity(), ThemeProvider {
|
||||
}
|
||||
|
||||
val requestInfinityFigureFile = registerForActivityResult(
|
||||
ActivityResultContracts.GetContent()
|
||||
ActivityResultContracts.OpenDocument()
|
||||
) { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
val label = InfinityConfig.loadFigure(infinityPosition, uri.toString())
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ class FigureSlotAdapter(
|
||||
|
||||
holder.binding.buttonLoadFigure.setOnClickListener {
|
||||
activity.setInfinityFigureData(0, "", figure.position, position)
|
||||
activity.requestInfinityFigureFile.launch("*/*")
|
||||
activity.requestInfinityFigureFile.launch(arrayOf("*/*"))
|
||||
}
|
||||
|
||||
val inflater = LayoutInflater.from(activity)
|
||||
|
||||
+9
-3
@@ -186,7 +186,7 @@ object ControllerInterface {
|
||||
private fun vibrate(vibrator: Vibrator) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
vibrator.vibrate(
|
||||
VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE),
|
||||
VibrationEffect.createOneShot(10000, VibrationEffect.DEFAULT_AMPLITUDE),
|
||||
VibrationAttributes.Builder().setUsage(VibrationAttributes.USAGE_MEDIA).build()
|
||||
)
|
||||
} else {
|
||||
@@ -194,16 +194,22 @@ object ControllerInterface {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrator.vibrate(
|
||||
VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE),
|
||||
VibrationEffect.createOneShot(10000, VibrationEffect.DEFAULT_AMPLITUDE),
|
||||
attributes
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
vibrator.vibrate(100, attributes)
|
||||
vibrator.vibrate(10000, attributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Keep
|
||||
@JvmStatic
|
||||
private fun cancelVibration(vibrator: Vibrator) {
|
||||
vibrator.cancel()
|
||||
}
|
||||
|
||||
private class InputDeviceListener : InputManager.InputDeviceListener {
|
||||
override external fun onInputDeviceAdded(deviceId: Int)
|
||||
|
||||
|
||||
+30
-1
@@ -23,6 +23,7 @@ import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.runningFold
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.ControllerMapping
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.NetplayMessage
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.Player
|
||||
@@ -87,6 +88,12 @@ class NetplaySession(
|
||||
)
|
||||
val players = _players.asSharedFlow().distinctUntilChanged()
|
||||
|
||||
private val _controllerMapping = MutableSharedFlow<ControllerMapping>(
|
||||
replay = 1,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
)
|
||||
val controllerMapping = _controllerMapping.asSharedFlow().distinctUntilChanged()
|
||||
|
||||
private val _chatMessages = MutableSharedFlow<String>(
|
||||
extraBufferCapacity = 32,
|
||||
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||
@@ -185,6 +192,11 @@ class NetplaySession(
|
||||
|
||||
fun reconnectTraversal() = nativeReconnectTraversal()
|
||||
|
||||
fun setControllerMapping(mapping: ControllerMapping) {
|
||||
nativeSetPadMapping(mapping.gamecubePorts.map { it?.pid ?: 0 }.toIntArray())
|
||||
nativeSetWiimoteMapping(mapping.wiiRemotes.map { it?.pid ?: 0 }.toIntArray())
|
||||
}
|
||||
|
||||
fun consumeBootSessionData(): Long {
|
||||
return bootSessionDataPointer.also {
|
||||
bootSessionDataPointer = 0
|
||||
@@ -278,6 +290,14 @@ class NetplaySession(
|
||||
|
||||
private external fun nativeReconnectTraversal()
|
||||
|
||||
private external fun nativeGetPadMapping(): IntArray
|
||||
|
||||
private external fun nativeGetWiimoteMapping(): IntArray
|
||||
|
||||
private external fun nativeSetPadMapping(mapping: IntArray)
|
||||
|
||||
private external fun nativeSetWiimoteMapping(mapping: IntArray)
|
||||
|
||||
// NetPlayUI callbacks
|
||||
|
||||
@Keep
|
||||
@@ -304,7 +324,16 @@ class NetplaySession(
|
||||
|
||||
@Keep
|
||||
fun onUpdate(players: Array<Player>) {
|
||||
_players.tryEmit(players.toList())
|
||||
val playersList = players.toList()
|
||||
_players.tryEmit(playersList)
|
||||
|
||||
fun findPlayer(playerId: Int) = playersList.find { it.pid == playerId }
|
||||
_controllerMapping.tryEmit(
|
||||
ControllerMapping(
|
||||
gamecubePorts = nativeGetPadMapping().map(::findPlayer).toList(),
|
||||
wiiRemotes = nativeGetWiimoteMapping().map(::findPlayer).toList(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@Keep
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.dolphinemu.dolphinemu.features.netplay.model
|
||||
|
||||
data class ControllerMapping(
|
||||
val gamecubePorts: List<Player?>,
|
||||
val wiiRemotes: List<Player?>,
|
||||
) {
|
||||
fun withGamecubePort(port: Int, player: Player?) =
|
||||
copy(gamecubePorts = gamecubePorts.replace(port, player))
|
||||
|
||||
fun withWiiRemote(port: Int, player: Player?) =
|
||||
copy(wiiRemotes = wiiRemotes.replace(port, player))
|
||||
|
||||
companion object {
|
||||
fun emptyControllerMapping(): ControllerMapping = ControllerMapping(
|
||||
gamecubePorts = emptyList(),
|
||||
wiiRemotes = emptyList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> List<T>.replace(index: Int, value: T) =
|
||||
toMutableList().also { it[index] = value }
|
||||
+42
-4
@@ -11,15 +11,17 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import org.dolphinemu.dolphinemu.features.netplay.NetplaySession
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.ControllerMapping.Companion.emptyControllerMapping
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.BooleanSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.IntSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.NativeConfig
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.StringSetting
|
||||
@@ -58,6 +60,9 @@ class NetplayViewModel(
|
||||
val players = netplaySession.players
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||
|
||||
val controllerMapping = netplaySession.controllerMapping
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyControllerMapping())
|
||||
|
||||
val messages = netplaySession.messages
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
|
||||
|
||||
@@ -104,18 +109,37 @@ class NetplayViewModel(
|
||||
private val _notAllPlayersHaveGame = Channel<Unit>(Channel.CONFLATED)
|
||||
val notAllPlayersHaveGame = _notAllPlayersHaveGame.receiveAsFlow()
|
||||
|
||||
private val _dualCoreWarning = Channel<Unit>(Channel.CONFLATED)
|
||||
val dualCoreWarning = _dualCoreWarning.receiveAsFlow()
|
||||
|
||||
fun startGame() {
|
||||
if (netplaySession.doAllPlayersHaveGame()) {
|
||||
netplaySession.startGame()
|
||||
} else {
|
||||
if (!netplaySession.doAllPlayersHaveGame()) {
|
||||
_notAllPlayersHaveGame.trySend(Unit)
|
||||
return
|
||||
}
|
||||
|
||||
if (BooleanSetting.MAIN_CPU_THREAD.boolean &&
|
||||
!BooleanSetting.NETPLAY_SKIP_DUAL_CORE_WARNING.boolean
|
||||
) {
|
||||
_dualCoreWarning.trySend(Unit)
|
||||
return
|
||||
}
|
||||
|
||||
confirmStartGame()
|
||||
}
|
||||
|
||||
fun confirmStartGame() {
|
||||
netplaySession.startGame()
|
||||
}
|
||||
|
||||
fun setDualCoreEnabled(enabled: Boolean) {
|
||||
BooleanSetting.MAIN_CPU_THREAD.setBoolean(NativeConfig.LAYER_BASE, enabled)
|
||||
}
|
||||
|
||||
fun skipDualCoreWarning() {
|
||||
BooleanSetting.NETPLAY_SKIP_DUAL_CORE_WARNING.setBoolean(NativeConfig.LAYER_BASE, true)
|
||||
}
|
||||
|
||||
fun sendMessage(message: String) {
|
||||
val trimmedMessage = message.trim()
|
||||
if (trimmedMessage.isEmpty()) {
|
||||
@@ -148,6 +172,18 @@ class NetplayViewModel(
|
||||
netplaySession.changeGame(gameFile)
|
||||
}
|
||||
|
||||
fun setGamecubePort(portNumber: Int, player: Player?) {
|
||||
netplaySession.setControllerMapping(
|
||||
controllerMapping.value.withGamecubePort(portNumber, player)
|
||||
)
|
||||
}
|
||||
|
||||
fun setWiiRemote(remoteNumber: Int, player: Player?) {
|
||||
netplaySession.setControllerMapping(
|
||||
controllerMapping.value.withWiiRemote(remoteNumber, player)
|
||||
)
|
||||
}
|
||||
|
||||
private fun getLocalIp(): JoinAddress {
|
||||
val localIp = networkHelper.getLocalIpString()
|
||||
?: return JoinAddress.Unknown { _joinAddresses.value += JoinInfoType.LOCAL to getLocalIp() }
|
||||
@@ -176,12 +212,14 @@ class NetplayViewModel(
|
||||
JoinInfoType.EXTERNAL to JoinAddress.Loading,
|
||||
)
|
||||
}
|
||||
|
||||
is TraversalState.Connected -> {
|
||||
_joinAddresses.value += mapOf(
|
||||
JoinInfoType.ROOM_ID to JoinAddress.Loaded(state.hostCode),
|
||||
JoinInfoType.EXTERNAL to JoinAddress.Loaded(state.externalAddress),
|
||||
)
|
||||
}
|
||||
|
||||
is TraversalState.Failure -> {
|
||||
_joinAddresses.value += mapOf(
|
||||
JoinInfoType.ROOM_ID to JoinAddress.Unknown(retry),
|
||||
|
||||
+6
@@ -58,6 +58,9 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
|
||||
onGameSelected = viewModel::changeGame,
|
||||
gameFiles = viewModel.gameFiles.collectAsState().value,
|
||||
notAllPlayersHaveGame = viewModel.notAllPlayersHaveGame,
|
||||
dualCoreWarning = viewModel.dualCoreWarning,
|
||||
onSetDualCoreEnabled = viewModel::setDualCoreEnabled,
|
||||
onSkipDualCoreWarning = viewModel::skipDualCoreWarning,
|
||||
onConfirmStartGame = viewModel::confirmStartGame,
|
||||
players = viewModel.players.collectAsState().value,
|
||||
hostInputAuthorityEnabled = viewModel.hostInputAuthority.collectAsState().value,
|
||||
@@ -70,6 +73,9 @@ class NetplayActivity : AppCompatActivity(), ThemeProvider {
|
||||
saveTransferProgress = viewModel.saveTransferProgress.collectAsState().value,
|
||||
gameDigestProgress = viewModel.gameDigestProgress.collectAsState().value,
|
||||
joinAddresses = viewModel.joinAddresses.collectAsState().value,
|
||||
controllerMapping = viewModel.controllerMapping.collectAsState().value,
|
||||
onGamecubePortChanged = viewModel::setGamecubePort,
|
||||
onWiiRemoteChanged = viewModel::setWiiRemote,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+368
-38
@@ -4,7 +4,9 @@ package org.dolphinemu.dolphinemu.features.netplay.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -19,6 +21,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
@@ -59,6 +62,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.SheetValue
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
@@ -72,11 +76,12 @@ import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onGloballyPositioned
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -86,8 +91,8 @@ import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.window.core.layout.WindowSizeClass
|
||||
import coil.compose.AsyncImage
|
||||
@@ -95,6 +100,7 @@ import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import org.dolphinemu.dolphinemu.R
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.ControllerMapping
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.GameDigestProgress
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.JoinAddress
|
||||
import org.dolphinemu.dolphinemu.features.netplay.model.JoinInfoType
|
||||
@@ -129,6 +135,9 @@ fun NetplayScreen(
|
||||
onGameSelected: (GameFile) -> Unit,
|
||||
gameFiles: List<GameFile>,
|
||||
notAllPlayersHaveGame: Flow<Unit>,
|
||||
dualCoreWarning: Flow<Unit>,
|
||||
onSetDualCoreEnabled: (Boolean) -> Unit,
|
||||
onSkipDualCoreWarning: () -> Unit,
|
||||
onConfirmStartGame: () -> Unit,
|
||||
hostInputAuthorityEnabled: Boolean,
|
||||
networkMode: NetworkMode,
|
||||
@@ -141,6 +150,9 @@ fun NetplayScreen(
|
||||
saveTransferProgress: SaveTransferProgress?,
|
||||
gameDigestProgress: GameDigestProgress?,
|
||||
joinAddresses: Map<JoinInfoType, JoinAddress>,
|
||||
controllerMapping: ControllerMapping,
|
||||
onGamecubePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
onWiiRemoteChanged: (port: Int, player: Player?) -> Unit,
|
||||
) {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
@@ -171,6 +183,7 @@ fun NetplayScreen(
|
||||
// State which must live above the landscape/portrait split.
|
||||
var showChat by rememberSaveable { mutableStateOf(false) }
|
||||
var showGamePicker by rememberSaveable { mutableStateOf(false) }
|
||||
var showControllerMapping by rememberSaveable { mutableStateOf(false) }
|
||||
var selectedJoinInfoType by rememberSaveable {
|
||||
mutableStateOf(joinAddresses.keys.firstOrNull() ?: JoinInfoType.EXTERNAL)
|
||||
}
|
||||
@@ -194,6 +207,11 @@ fun NetplayScreen(
|
||||
showGamePicker = showGamePicker,
|
||||
onShowGamePickerChanged = { showGamePicker = it },
|
||||
players = players,
|
||||
controllerMapping = controllerMapping,
|
||||
onGamecubePortChanged = onGamecubePortChanged,
|
||||
onWiimotePortChanged = onWiiRemoteChanged,
|
||||
showControllerMapping = showControllerMapping,
|
||||
onShowControllerMappingChanged = { showControllerMapping = it },
|
||||
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
||||
networkMode = networkMode,
|
||||
onNetworkModeChanged = onNetworkModeChanged,
|
||||
@@ -221,6 +239,11 @@ fun NetplayScreen(
|
||||
showGamePicker = showGamePicker,
|
||||
onShowGamePickerChanged = { showGamePicker = it },
|
||||
players = players,
|
||||
controllerMapping = controllerMapping,
|
||||
onGamecubePortChanged = onGamecubePortChanged,
|
||||
onWiimotePortChanged = onWiiRemoteChanged,
|
||||
showControllerMapping = showControllerMapping,
|
||||
onShowControllerMappingChanged = { showControllerMapping = it },
|
||||
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
||||
networkMode = networkMode,
|
||||
onNetworkModeChanged = onNetworkModeChanged,
|
||||
@@ -252,6 +275,11 @@ fun NetplayScreen(
|
||||
notAllPlayersHaveGame.collect { showNotAllPlayersHaveGame = true }
|
||||
}
|
||||
|
||||
var showDualCoreWarning by rememberSaveable { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
dualCoreWarning.collect { showDualCoreWarning = true }
|
||||
}
|
||||
|
||||
var dismissSaveTransferProgressDialog by rememberSaveable { mutableStateOf(false) }
|
||||
if (saveTransferProgress == null) {
|
||||
dismissSaveTransferProgressDialog = false
|
||||
@@ -323,6 +351,21 @@ fun NetplayScreen(
|
||||
onDismissRequest = { showNotAllPlayersHaveGame = false },
|
||||
)
|
||||
}
|
||||
|
||||
showDualCoreWarning -> {
|
||||
DualCoreWarningDialog(
|
||||
onSetDualCoreEnabled = onSetDualCoreEnabled,
|
||||
onStartGame = {
|
||||
showDualCoreWarning = false
|
||||
onConfirmStartGame()
|
||||
},
|
||||
onSkipWarning = {
|
||||
showDualCoreWarning = false
|
||||
onSkipDualCoreWarning()
|
||||
},
|
||||
onDismiss = { showDualCoreWarning = false },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -340,6 +383,11 @@ private fun PortraitContent(
|
||||
showGamePicker: Boolean,
|
||||
onShowGamePickerChanged: (Boolean) -> Unit,
|
||||
players: List<Player>,
|
||||
controllerMapping: ControllerMapping,
|
||||
onGamecubePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
onWiimotePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
showControllerMapping: Boolean,
|
||||
onShowControllerMappingChanged: (Boolean) -> Unit,
|
||||
hostInputAuthorityEnabled: Boolean,
|
||||
networkMode: NetworkMode,
|
||||
onNetworkModeChanged: (NetworkMode) -> Unit,
|
||||
@@ -379,6 +427,11 @@ private fun PortraitContent(
|
||||
showGamePicker = showGamePicker,
|
||||
onShowGamePickerChanged = onShowGamePickerChanged,
|
||||
players = players,
|
||||
controllerMapping = controllerMapping,
|
||||
onGamecubePortChanged = onGamecubePortChanged,
|
||||
onWiimotePortChanged = onWiimotePortChanged,
|
||||
showControllerMapping = showControllerMapping,
|
||||
onShowControllerMappingChanged = onShowControllerMappingChanged,
|
||||
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
||||
networkMode = networkMode,
|
||||
onNetworkModeChanged = onNetworkModeChanged,
|
||||
@@ -413,6 +466,11 @@ private fun LandscapeContent(
|
||||
showGamePicker: Boolean,
|
||||
onShowGamePickerChanged: (Boolean) -> Unit,
|
||||
players: List<Player>,
|
||||
controllerMapping: ControllerMapping,
|
||||
onGamecubePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
onWiimotePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
showControllerMapping: Boolean,
|
||||
onShowControllerMappingChanged: (Boolean) -> Unit,
|
||||
hostInputAuthorityEnabled: Boolean,
|
||||
networkMode: NetworkMode,
|
||||
onNetworkModeChanged: (NetworkMode) -> Unit,
|
||||
@@ -445,7 +503,6 @@ private fun LandscapeContent(
|
||||
.fillMaxHeight()
|
||||
)
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
@@ -464,6 +521,11 @@ private fun LandscapeContent(
|
||||
showGamePicker = showGamePicker,
|
||||
onShowGamePickerChanged = onShowGamePickerChanged,
|
||||
players = players,
|
||||
controllerMapping = controllerMapping,
|
||||
onGamecubePortChanged = onGamecubePortChanged,
|
||||
onWiimotePortChanged = onWiimotePortChanged,
|
||||
showControllerMapping = showControllerMapping,
|
||||
onShowControllerMappingChanged = onShowControllerMappingChanged,
|
||||
hostInputAuthorityEnabled = hostInputAuthorityEnabled,
|
||||
networkMode = networkMode,
|
||||
onNetworkModeChanged = onNetworkModeChanged,
|
||||
@@ -485,6 +547,7 @@ private fun LandscapeContent(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun PlayersAndSettings(
|
||||
game: String,
|
||||
@@ -493,6 +556,11 @@ private fun PlayersAndSettings(
|
||||
showGamePicker: Boolean,
|
||||
onShowGamePickerChanged: (Boolean) -> Unit,
|
||||
players: List<Player>,
|
||||
controllerMapping: ControllerMapping,
|
||||
onGamecubePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
onWiimotePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
showControllerMapping: Boolean,
|
||||
onShowControllerMappingChanged: (Boolean) -> Unit,
|
||||
hostInputAuthorityEnabled: Boolean,
|
||||
networkMode: NetworkMode,
|
||||
onNetworkModeChanged: (NetworkMode) -> Unit,
|
||||
@@ -530,25 +598,15 @@ private fun PlayersAndSettings(
|
||||
|
||||
MenuSpacer()
|
||||
|
||||
OutlinedBox(
|
||||
label = { Text(stringResource(R.string.netplay_players_label)) },
|
||||
) {
|
||||
PlayersTable(
|
||||
rows = buildList {
|
||||
add(
|
||||
listOf(
|
||||
stringResource(R.string.netplay_players_name),
|
||||
stringResource(R.string.netplay_players_ping),
|
||||
stringResource(R.string.netplay_players_mapping),
|
||||
)
|
||||
)
|
||||
addAll(players.map { listOf(it.name, it.ping.toString(), it.mapping) })
|
||||
repeat(4 - players.size) { add(listOf("", "", "")) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
Players(
|
||||
players = players,
|
||||
isHosting = isHosting,
|
||||
controllerMapping = controllerMapping,
|
||||
onGamecubePortChanged = onGamecubePortChanged,
|
||||
onWiimotePortChanged = onWiimotePortChanged,
|
||||
showControllerMapping = showControllerMapping,
|
||||
onShowControllerMappingChanged = onShowControllerMappingChanged,
|
||||
)
|
||||
|
||||
if (isHosting) {
|
||||
MenuSpacer()
|
||||
@@ -932,6 +990,64 @@ private fun AddressRow(
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
private fun Players(
|
||||
players: List<Player>,
|
||||
isHosting: Boolean,
|
||||
controllerMapping: ControllerMapping,
|
||||
onGamecubePortChanged: (Int, Player?) -> Unit,
|
||||
onWiimotePortChanged: (Int, Player?) -> Unit,
|
||||
showControllerMapping: Boolean,
|
||||
onShowControllerMappingChanged: (Boolean) -> Unit,
|
||||
) {
|
||||
val sheetState = rememberSheetState(
|
||||
skipPartiallyExpanded = true,
|
||||
initialValue = if (showControllerMapping) SheetValue.Expanded else SheetValue.Hidden,
|
||||
)
|
||||
|
||||
if (showControllerMapping) {
|
||||
ModalBottomSheet(
|
||||
onDismissRequest = { onShowControllerMappingChanged(false) },
|
||||
sheetState = sheetState,
|
||||
modifier = Modifier
|
||||
.statusBarsPadding()
|
||||
) {
|
||||
ControllerMapping(
|
||||
mapping = controllerMapping,
|
||||
players = players,
|
||||
onGamecubePortChanged = onGamecubePortChanged,
|
||||
onWiimotePortChanged = onWiimotePortChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedBox(
|
||||
label = { Text(stringResource(R.string.netplay_players_label)) },
|
||||
onClick = if (isHosting) {
|
||||
{ onShowControllerMappingChanged(true) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
) {
|
||||
PlayersTable(
|
||||
rows = buildList {
|
||||
add(
|
||||
listOf(
|
||||
stringResource(R.string.netplay_players_name),
|
||||
stringResource(R.string.netplay_players_ping),
|
||||
stringResource(R.string.netplay_players_mapping),
|
||||
)
|
||||
)
|
||||
addAll(players.map { listOf(it.name, it.ping.toString(), it.mapping) })
|
||||
repeat(4 - players.size) { add(listOf("", "", "")) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A table arranged into columns sized to wrap the largest item. Except the
|
||||
* first column which takes up the remaining space left by the other columns.
|
||||
@@ -944,7 +1060,6 @@ private fun PlayersTable(
|
||||
) {
|
||||
rows.zipWithNext { a, b -> if (a.size != b.size) throw IllegalArgumentException("Rows must all contain the same number of elements.") }
|
||||
val maxWidths = remember { List(rows.first().size) { mutableIntStateOf(0) } }
|
||||
val density = LocalDensity.current
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
@@ -956,23 +1071,21 @@ private fun PlayersTable(
|
||||
) {
|
||||
row.forEachIndexed { itemIndex, text ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
when {
|
||||
itemIndex == 0 -> Modifier.weight(1f)
|
||||
|
||||
maxWidths[itemIndex].intValue > 0 -> Modifier
|
||||
.width(with(density) { maxWidths[itemIndex].intValue.toDp() })
|
||||
|
||||
else -> Modifier
|
||||
modifier = if (itemIndex == 0) {
|
||||
Modifier.weight(1f)
|
||||
} else {
|
||||
val maxWidth = maxWidths[itemIndex]
|
||||
Modifier.layout { measurable, constraints ->
|
||||
val placeable =
|
||||
measurable.measure(constraints.copy(maxWidth = Constraints.Infinity))
|
||||
if (placeable.width > maxWidth.intValue) {
|
||||
maxWidth.intValue = placeable.width
|
||||
}
|
||||
)
|
||||
.onGloballyPositioned { coordinates ->
|
||||
val width = coordinates.size.width
|
||||
if (width > maxWidths[itemIndex].intValue) {
|
||||
maxWidths[itemIndex].intValue = width
|
||||
layout(maxWidth.intValue, placeable.height) {
|
||||
placeable.place(x = 0, y = 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
@@ -989,6 +1102,150 @@ private fun PlayersTable(
|
||||
}
|
||||
}
|
||||
|
||||
private val PORT_DROPDOWN_MIN_WIDTH = 140.dp
|
||||
private val PORT_DROPDOWN_SPACING = 10.dp
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun ControllerMapping(
|
||||
mapping: ControllerMapping,
|
||||
players: List<Player>,
|
||||
onGamecubePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
onWiimotePortChanged: (port: Int, player: Player?) -> Unit,
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = DolphinTheme.scaffoldPadding)
|
||||
.padding(bottom = 24.dp)
|
||||
.navigationBarsPadding()
|
||||
) {
|
||||
val columns =
|
||||
((maxWidth + PORT_DROPDOWN_SPACING) / (PORT_DROPDOWN_MIN_WIDTH + PORT_DROPDOWN_SPACING))
|
||||
.toInt()
|
||||
.coerceAtLeast(1)
|
||||
.let { if (it == 3) 2 else it } // Three per row would look strange so bump down to two.
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
) {
|
||||
PortsSection(
|
||||
titleId = R.string.netplay_controllers_gamecube,
|
||||
portLabelId = R.string.netplay_controllers_port,
|
||||
mapping = mapping.gamecubePorts,
|
||||
players = players,
|
||||
onPortChanged = onGamecubePortChanged,
|
||||
columns = columns,
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
PortsSection(
|
||||
titleId = R.string.netplay_controllers_wii_remotes,
|
||||
portLabelId = R.string.netplay_controllers_remote,
|
||||
mapping = mapping.wiiRemotes,
|
||||
players = players,
|
||||
onPortChanged = onWiimotePortChanged,
|
||||
columns = columns,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PortsSection(
|
||||
@StringRes titleId: Int,
|
||||
@StringRes portLabelId: Int,
|
||||
mapping: List<Player?>,
|
||||
players: List<Player>,
|
||||
onPortChanged: (port: Int, player: Player?) -> Unit,
|
||||
columns: Int,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(PORT_DROPDOWN_SPACING),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(titleId),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
|
||||
mapping.indices.chunked(columns).forEach { row ->
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(PORT_DROPDOWN_SPACING),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
row.forEach { port ->
|
||||
PortDropdown(
|
||||
label = stringResource(portLabelId, port + 1),
|
||||
selectedPlayer = mapping[port],
|
||||
players = players,
|
||||
onPlayerSelected = { onPortChanged(port, it) },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun PortDropdown(
|
||||
label: String,
|
||||
selectedPlayer: Player?,
|
||||
players: List<Player>,
|
||||
onPlayerSelected: (player: Player?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
fun Player.displayName() = "$name ($pid)"
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = expanded,
|
||||
onExpandedChange = { expanded = it },
|
||||
modifier = modifier,
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = selectedPlayer?.displayName()
|
||||
?: stringResource(R.string.netplay_controllers_none),
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
singleLine = true,
|
||||
label = { Text(label) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
|
||||
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
|
||||
modifier = Modifier
|
||||
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.netplay_controllers_none)) },
|
||||
onClick = {
|
||||
onPlayerSelected(null)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
players.forEach { player ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(player.displayName()) },
|
||||
onClick = {
|
||||
onPlayerSelected(player)
|
||||
expanded = false
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun NetworkModeDropdown(
|
||||
@@ -1273,6 +1530,73 @@ private fun GameDigestPlayerRow(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DualCoreWarningDialog(
|
||||
onSetDualCoreEnabled: (Boolean) -> Unit,
|
||||
onStartGame: () -> Unit,
|
||||
onSkipWarning: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
var dualCoreEnabled by rememberSaveable { mutableStateOf(true) }
|
||||
AlertDialog(
|
||||
title = { Text(stringResource(R.string.netplay_dual_core_warning_title)) },
|
||||
text = {
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.netplay_dual_core_warning_message),
|
||||
)
|
||||
val ripplePadding = 12.dp
|
||||
Spacer(Modifier.height(12.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.layout { measurable, constraints ->
|
||||
// Enlarge the ripple area beyond the dialog's padding
|
||||
val hPx = ripplePadding.roundToPx()
|
||||
val placeable = measurable.measure(
|
||||
constraints.copy(
|
||||
minWidth = constraints.maxWidth + hPx * 2,
|
||||
maxWidth = constraints.maxWidth + hPx * 2
|
||||
)
|
||||
)
|
||||
layout(constraints.maxWidth, placeable.height) {
|
||||
placeable.place(-hPx, 0)
|
||||
}
|
||||
}
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.clickable {
|
||||
dualCoreEnabled = !dualCoreEnabled
|
||||
onSetDualCoreEnabled(dualCoreEnabled)
|
||||
}
|
||||
.padding(ripplePadding),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.dual_core),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Switch(
|
||||
checked = dualCoreEnabled,
|
||||
onCheckedChange = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onStartGame) {
|
||||
Text(stringResource(R.string.netplay_start))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onSkipWarning) {
|
||||
Text(stringResource(R.string.netplay_dont_warn_again))
|
||||
}
|
||||
},
|
||||
onDismissRequest = onDismiss,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NetplayMessage.color(): Color {
|
||||
val isDark = isSystemInDarkTheme()
|
||||
@@ -1361,6 +1685,9 @@ private fun PreviewNetplayScreen() {
|
||||
onGameSelected = {},
|
||||
gameFiles = emptyList(),
|
||||
notAllPlayersHaveGame = emptyFlow(),
|
||||
dualCoreWarning = emptyFlow(),
|
||||
onSetDualCoreEnabled = {},
|
||||
onSkipDualCoreWarning = {},
|
||||
onConfirmStartGame = {},
|
||||
hostInputAuthorityEnabled = true,
|
||||
networkMode = NetworkMode.HOST_INPUT_AUTHORITY,
|
||||
@@ -1375,6 +1702,9 @@ private fun PreviewNetplayScreen() {
|
||||
JoinInfoType.EXTERNAL to JoinAddress.Loaded("203.0.113.1:2626"),
|
||||
JoinInfoType.LOCAL to JoinAddress.Loaded("192.168.1.5:2626"),
|
||||
),
|
||||
controllerMapping = ControllerMapping.emptyControllerMapping(),
|
||||
onGamecubePortChanged = { _, _ -> },
|
||||
onWiiRemoteChanged = { _, _ -> },
|
||||
// saveTransferProgress = SaveTransferProgress(
|
||||
// title = "Title",
|
||||
// totalSize = 1024L,
|
||||
|
||||
+2
-1
@@ -947,7 +947,8 @@ enum class BooleanSetting(
|
||||
"ProgressEnabled",
|
||||
false
|
||||
),
|
||||
NETPLAY_USE_UPNP(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "UseUPNP", false);
|
||||
NETPLAY_USE_UPNP(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "UseUPNP", false),
|
||||
NETPLAY_SKIP_DUAL_CORE_WARNING(Settings.FILE_DOLPHIN, Settings.SECTION_INI_NETPLAY, "SkipDualCoreWarning", false);
|
||||
|
||||
override val isOverridden: Boolean
|
||||
get() = NativeConfig.isOverridden(file, section, key)
|
||||
|
||||
+1
@@ -86,5 +86,6 @@ abstract class SettingsItem {
|
||||
const val TYPE_STRING = 12
|
||||
const val TYPE_HYPERLINK_HEADER = 13
|
||||
const val TYPE_DATETIME_CHOICE = 14
|
||||
const val TYPE_SEARCH_RESULT = 15
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.dolphinemu.dolphinemu.features.settings.model.view
|
||||
|
||||
import android.os.Bundle
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.AbstractSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.MenuTag
|
||||
|
||||
class SettingsSearchResult(
|
||||
name: CharSequence,
|
||||
description: CharSequence,
|
||||
val menuKey: MenuTag,
|
||||
val settingPosition: Int,
|
||||
val navigationExtras: Bundle?
|
||||
) : SettingsItem(name, description) {
|
||||
override val type: Int = TYPE_SEARCH_RESULT
|
||||
|
||||
override val setting: AbstractSetting? = null
|
||||
}
|
||||
+191
-34
@@ -7,14 +7,17 @@ import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.animation.PathInterpolator
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
@@ -39,6 +42,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
|
||||
private var dialog: AlertDialog? = null
|
||||
private var toolbarLayout: CollapsingToolbarLayout? = null
|
||||
private var binding: ActivitySettingsBinding? = null
|
||||
private lateinit var searchView: SearchView
|
||||
private var expandedToolbarHeight = 0
|
||||
private var toolbarStateGeneration = 0
|
||||
private var currentToolbarTitle: String? = null
|
||||
private var currentToolbarShowsHeadline = false
|
||||
private var currentToolbarShowsSearch = false
|
||||
private var currentToolbarShowsSearchMode = false
|
||||
override val settingsSearchQuery: String
|
||||
get() = presenter!!.settingsSearchQuery
|
||||
override val isSettingsSearchActive: Boolean
|
||||
get() = presenter!!.isSettingsSearchActive
|
||||
|
||||
override var themeId: Int = 0
|
||||
override var isMappingAllDevices = false
|
||||
@@ -76,8 +90,11 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
|
||||
presenter = SettingsActivityPresenter(this, settings)
|
||||
presenter!!.onCreate(savedInstanceState, menuTag, gameID, revision, isWii, this)
|
||||
toolbarLayout = binding!!.toolbarSettingsLayout
|
||||
expandedToolbarHeight = toolbarLayout!!.layoutParams.height
|
||||
setSupportActionBar(binding!!.toolbarSettings)
|
||||
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
|
||||
setUpSettingsSearch()
|
||||
setUpBackNavigation()
|
||||
|
||||
// TODO: Remove this when CollapsingToolbarLayouts are fixed by Google
|
||||
// https://github.com/material-components/material-components-android/issues/1310
|
||||
@@ -86,16 +103,78 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
|
||||
enableScrollTint(this, binding!!.toolbarSettings, binding!!.appbarSettings)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
val inflater = menuInflater
|
||||
inflater.inflate(R.menu.menu_settings, menu)
|
||||
return true
|
||||
private fun setUpSettingsSearch() {
|
||||
searchView = binding!!.settingsSearch
|
||||
searchView.setQuery(settingsSearchQuery, false)
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||
searchView.clearFocus()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
presenter!!.onSettingsSearchQueryChanged(newText.orEmpty())
|
||||
return true
|
||||
}
|
||||
})
|
||||
binding!!.settingsSearchPreview.setOnClickListener { enterSettingsSearch() }
|
||||
binding!!.settingsSearchToolbar.setNavigationOnClickListener { exitSettingsSearch() }
|
||||
}
|
||||
|
||||
private fun enterSettingsSearch() {
|
||||
if (!presenter!!.enterSettingsSearch()) {
|
||||
return
|
||||
}
|
||||
|
||||
refreshToolbarState()
|
||||
val focusDelay = if (areSystemAnimationsEnabled()) SEARCH_FOCUS_DELAY_MS else 0L
|
||||
searchView.postDelayed({
|
||||
if (!isSettingsSearchActive) {
|
||||
return@postDelayed
|
||||
}
|
||||
searchView.requestFocus()
|
||||
WindowCompat.getInsetsController(window, searchView).show(WindowInsetsCompat.Type.ime())
|
||||
}, focusDelay)
|
||||
}
|
||||
|
||||
private fun exitSettingsSearch() {
|
||||
if (!presenter!!.exitSettingsSearch()) {
|
||||
return
|
||||
}
|
||||
|
||||
searchView.setQuery("", false)
|
||||
searchView.clearFocus()
|
||||
WindowCompat.getInsetsController(window, searchView).hide(WindowInsetsCompat.Type.ime())
|
||||
refreshToolbarState()
|
||||
}
|
||||
|
||||
private fun refreshToolbarState() {
|
||||
val title = currentToolbarTitle ?: getString(R.string.settings)
|
||||
setToolbarState(
|
||||
title, currentToolbarShowsHeadline, currentToolbarShowsSearch
|
||||
)
|
||||
}
|
||||
|
||||
private fun setUpBackNavigation() {
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
if (supportFragmentManager.backStackEntryCount == 0 && isSettingsSearchActive) {
|
||||
exitSettingsSearch()
|
||||
return
|
||||
}
|
||||
|
||||
isEnabled = false
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
isEnabled = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
// Critical: If super method is not called, rotations will be busted.
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putBoolean(KEY_MAPPING_ALL_DEVICES, isMappingAllDevices)
|
||||
presenter!!.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
@@ -128,10 +207,17 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
|
||||
}
|
||||
|
||||
override fun showSettingsFragment(
|
||||
menuTag: MenuTag, extras: Bundle?, addToStack: Boolean, gameId: String
|
||||
) {
|
||||
replaceSettingsFragment(menuTag, extras, addToStack, gameId, false)
|
||||
}
|
||||
|
||||
private fun replaceSettingsFragment(
|
||||
menuTag: MenuTag,
|
||||
extras: Bundle?,
|
||||
addToStack: Boolean,
|
||||
gameId: String
|
||||
gameId: String,
|
||||
isSearchResult: Boolean
|
||||
) {
|
||||
if (!addToStack && fragment != null) return
|
||||
val transaction = supportFragmentManager.beginTransaction()
|
||||
@@ -140,15 +226,18 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
|
||||
transaction.setCustomAnimations(
|
||||
R.anim.anim_settings_fragment_in,
|
||||
R.anim.anim_settings_fragment_out,
|
||||
0,
|
||||
R.anim.anim_pop_settings_fragment_out
|
||||
if (isSearchResult) R.anim.anim_settings_search_pop_in else 0,
|
||||
if (isSearchResult) {
|
||||
R.anim.anim_settings_search_pop_out
|
||||
} else {
|
||||
R.anim.anim_pop_settings_fragment_out
|
||||
}
|
||||
)
|
||||
}
|
||||
transaction.addToBackStack(null)
|
||||
}
|
||||
transaction.replace(
|
||||
R.id.frame_content_settings,
|
||||
newInstance(menuTag, gameId, extras), FRAGMENT_TAG
|
||||
R.id.frame_content_settings, newInstance(menuTag, gameId, extras), FRAGMENT_TAG
|
||||
)
|
||||
transaction.commit()
|
||||
}
|
||||
@@ -157,16 +246,22 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
|
||||
fragment.show(supportFragmentManager, FRAGMENT_DIALOG_TAG)
|
||||
}
|
||||
|
||||
override fun showSearchResult(
|
||||
menuTag: MenuTag, settingPosition: Int, gameId: String, extras: Bundle?
|
||||
) {
|
||||
val navigationExtras = extras?.let(::Bundle) ?: Bundle()
|
||||
navigationExtras.putInt(
|
||||
SettingsFragment.ARGUMENT_SCROLL_TO_SETTING_POSITION, settingPosition
|
||||
)
|
||||
replaceSettingsFragment(menuTag, navigationExtras, true, gameId, true)
|
||||
}
|
||||
|
||||
private fun areSystemAnimationsEnabled(): Boolean {
|
||||
val duration = android.provider.Settings.Global.getFloat(
|
||||
contentResolver,
|
||||
android.provider.Settings.Global.ANIMATOR_DURATION_SCALE,
|
||||
1f
|
||||
contentResolver, android.provider.Settings.Global.ANIMATOR_DURATION_SCALE, 1f
|
||||
)
|
||||
val transition = android.provider.Settings.Global.getFloat(
|
||||
contentResolver,
|
||||
android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE,
|
||||
1f
|
||||
contentResolver, android.provider.Settings.Global.TRANSITION_ANIMATION_SCALE, 1f
|
||||
)
|
||||
return duration != 0f && transition != 0f
|
||||
}
|
||||
@@ -183,10 +278,8 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
|
||||
|
||||
override fun showLoading() {
|
||||
if (dialog == null) {
|
||||
dialog = MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.load_settings))
|
||||
.setView(R.layout.dialog_indeterminate_progress)
|
||||
.create()
|
||||
dialog = MaterialAlertDialogBuilder(this).setTitle(getString(R.string.load_settings))
|
||||
.setView(R.layout.dialog_indeterminate_progress).create()
|
||||
}
|
||||
dialog!!.show()
|
||||
}
|
||||
@@ -196,12 +289,10 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
|
||||
}
|
||||
|
||||
override fun showGameIniJunkDeletionQuestion() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.game_ini_junk_title))
|
||||
MaterialAlertDialogBuilder(this).setTitle(getString(R.string.game_ini_junk_title))
|
||||
.setMessage(getString(R.string.game_ini_junk_question))
|
||||
.setPositiveButton(R.string.yes) { _: DialogInterface?, _: Int -> presenter!!.clearGameSettings() }
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.show()
|
||||
.setNegativeButton(R.string.no, null).show()
|
||||
}
|
||||
|
||||
override fun onSettingsFileLoaded(settings: Settings) {
|
||||
@@ -229,13 +320,78 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
|
||||
return presenter!!.hasMenuTagActionForValue(menuTag, value)
|
||||
}
|
||||
|
||||
override fun getMenuTagActionExtras(menuTag: MenuTag, value: Int): Bundle? {
|
||||
return presenter!!.getMenuTagActionExtras(menuTag, value)
|
||||
}
|
||||
|
||||
override fun filterSettings(query: String) {
|
||||
fragment?.filterSettings(query)
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
onBackPressed()
|
||||
onBackPressedDispatcher.onBackPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun setToolbarTitle(title: String) {
|
||||
binding!!.toolbarSettingsLayout.title = title
|
||||
override fun setToolbarState(title: String, showHeadline: Boolean, showSearch: Boolean) {
|
||||
val appBar = binding!!.appbarSettings
|
||||
val generation = ++toolbarStateGeneration
|
||||
val showSearchMode = showSearch && isSettingsSearchActive
|
||||
val stateChanged =
|
||||
currentToolbarTitle != title || currentToolbarShowsHeadline != showHeadline || currentToolbarShowsSearch != showSearch || currentToolbarShowsSearchMode != showSearchMode
|
||||
appBar.animate().cancel()
|
||||
|
||||
if (!appBar.isLaidOut || !stateChanged) {
|
||||
applyToolbarState(title, showHeadline, showSearch)
|
||||
appBar.alpha = 1f
|
||||
return
|
||||
}
|
||||
|
||||
if (!showSearch) {
|
||||
searchView.clearFocus()
|
||||
}
|
||||
|
||||
appBar.animate().alpha(0f).setDuration(APP_BAR_FADE_OUT_DURATION_MS)
|
||||
.setInterpolator(APP_BAR_FADE_OUT_INTERPOLATOR).withEndAction {
|
||||
if (generation != toolbarStateGeneration) {
|
||||
return@withEndAction
|
||||
}
|
||||
|
||||
applyToolbarState(title, showHeadline, showSearch)
|
||||
appBar.post {
|
||||
if (generation != toolbarStateGeneration) {
|
||||
return@post
|
||||
}
|
||||
|
||||
appBar.animate().alpha(1f).setDuration(APP_BAR_FADE_IN_DURATION_MS)
|
||||
.setInterpolator(APP_BAR_FADE_IN_INTERPOLATOR).start()
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun applyToolbarState(title: String, showHeadline: Boolean, showSearch: Boolean) {
|
||||
val showSearchMode = showSearch && isSettingsSearchActive
|
||||
toolbarLayout!!.isTitleEnabled = showHeadline
|
||||
supportActionBar!!.title = title
|
||||
if (showHeadline) {
|
||||
toolbarLayout!!.title = title
|
||||
}
|
||||
toolbarLayout!!.layoutParams = toolbarLayout!!.layoutParams.apply {
|
||||
height = if (showHeadline) {
|
||||
expandedToolbarHeight
|
||||
} else {
|
||||
binding!!.toolbarSettings.layoutParams.height
|
||||
}
|
||||
}
|
||||
toolbarLayout!!.visibility = if (showSearchMode) View.GONE else View.VISIBLE
|
||||
binding!!.settingsSearchContainer.visibility =
|
||||
if (showSearch && !showSearchMode) View.VISIBLE else View.GONE
|
||||
binding!!.settingsSearchModeContainer.visibility =
|
||||
if (showSearchMode) View.VISIBLE else View.GONE
|
||||
currentToolbarTitle = title
|
||||
currentToolbarShowsHeadline = showHeadline
|
||||
currentToolbarShowsSearch = showSearch
|
||||
currentToolbarShowsSearchMode = showSearchMode
|
||||
}
|
||||
|
||||
override fun setOldControllerSettingsWarningVisibility(visible: Boolean): Int {
|
||||
@@ -274,14 +430,16 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
|
||||
private const val KEY_MAPPING_ALL_DEVICES = "all_devices"
|
||||
private const val FRAGMENT_TAG = "settings"
|
||||
private const val FRAGMENT_DIALOG_TAG = "settings_dialog"
|
||||
private const val APP_BAR_FADE_OUT_DURATION_MS = 90L
|
||||
private const val APP_BAR_FADE_IN_DURATION_MS = 180L
|
||||
private const val SEARCH_FOCUS_DELAY_MS =
|
||||
APP_BAR_FADE_OUT_DURATION_MS + APP_BAR_FADE_IN_DURATION_MS
|
||||
private val APP_BAR_FADE_OUT_INTERPOLATOR = PathInterpolator(0.4f, 0f, 1f, 1f)
|
||||
private val APP_BAR_FADE_IN_INTERPOLATOR = PathInterpolator(0f, 0f, 0.2f, 1f)
|
||||
|
||||
@JvmStatic
|
||||
fun launch(
|
||||
context: Context,
|
||||
menuTag: MenuTag?,
|
||||
gameId: String?,
|
||||
revision: Int,
|
||||
isWii: Boolean
|
||||
context: Context, menuTag: MenuTag?, gameId: String?, revision: Int, isWii: Boolean
|
||||
) {
|
||||
val settings = Intent(context, SettingsActivity::class.java)
|
||||
settings.putExtra(ARG_MENU_TAG, menuTag)
|
||||
@@ -296,8 +454,7 @@ class SettingsActivity : AppCompatActivity(), SettingsActivityView, ThemeProvide
|
||||
val settings = Intent(context, SettingsActivity::class.java)
|
||||
settings.putExtra(ARG_MENU_TAG, menuTag)
|
||||
settings.putExtra(
|
||||
ARG_IS_WII,
|
||||
!NativeLibrary.IsRunning() || NativeLibrary.IsEmulatingWii()
|
||||
ARG_IS_WII, !NativeLibrary.IsRunning() || NativeLibrary.IsEmulatingWii()
|
||||
)
|
||||
context.startActivity(settings)
|
||||
}
|
||||
|
||||
+79
-45
@@ -10,14 +10,17 @@ import org.dolphinemu.dolphinemu.utils.AfterDirectoryInitializationRunner
|
||||
import org.dolphinemu.dolphinemu.utils.Log
|
||||
|
||||
class SettingsActivityPresenter(
|
||||
private val activityView: SettingsActivityView,
|
||||
var settings: Settings?
|
||||
private val activityView: SettingsActivityView, var settings: Settings?
|
||||
) {
|
||||
private var menuTag: MenuTag? = null
|
||||
private var gameId: String? = null
|
||||
private var revision = 0
|
||||
private var isWii = false
|
||||
private lateinit var activity: AppCompatActivity
|
||||
var settingsSearchQuery = ""
|
||||
private set
|
||||
var isSettingsSearchActive = false
|
||||
private set
|
||||
|
||||
fun onCreate(
|
||||
savedInstanceState: Bundle?,
|
||||
@@ -32,6 +35,43 @@ class SettingsActivityPresenter(
|
||||
this.revision = revision
|
||||
this.isWii = isWii
|
||||
this.activity = activity
|
||||
if (savedInstanceState != null) {
|
||||
isSettingsSearchActive =
|
||||
savedInstanceState.getBoolean(KEY_SETTINGS_SEARCH_ACTIVE)
|
||||
settingsSearchQuery =
|
||||
savedInstanceState.getString(KEY_SETTINGS_SEARCH_QUERY).orEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(KEY_SETTINGS_SEARCH_ACTIVE, isSettingsSearchActive)
|
||||
outState.putString(KEY_SETTINGS_SEARCH_QUERY, settingsSearchQuery)
|
||||
}
|
||||
|
||||
fun onSettingsSearchQueryChanged(query: String) {
|
||||
settingsSearchQuery = query
|
||||
activityView.filterSettings(query)
|
||||
}
|
||||
|
||||
fun enterSettingsSearch(): Boolean {
|
||||
if (isSettingsSearchActive) {
|
||||
return false
|
||||
}
|
||||
|
||||
isSettingsSearchActive = true
|
||||
activityView.filterSettings(settingsSearchQuery)
|
||||
return true
|
||||
}
|
||||
|
||||
fun exitSettingsSearch(): Boolean {
|
||||
if (!isSettingsSearchActive) {
|
||||
return false
|
||||
}
|
||||
|
||||
isSettingsSearchActive = false
|
||||
settingsSearchQuery = ""
|
||||
activityView.filterSettings("")
|
||||
return true
|
||||
}
|
||||
|
||||
fun onDestroy() {
|
||||
@@ -85,55 +125,49 @@ class SettingsActivityPresenter(
|
||||
}
|
||||
|
||||
fun onMenuTagAction(menuTag: MenuTag, value: Int) {
|
||||
if (menuTag.isSerialPort1Menu) {
|
||||
// Not disabled or dummy
|
||||
if (value != 0 && value != 255) {
|
||||
val bundle = Bundle()
|
||||
bundle.putInt(SettingsFragmentPresenter.ARG_SERIALPORT1_TYPE, value)
|
||||
activityView.showSettingsFragment(menuTag, bundle, true, gameId!!)
|
||||
}
|
||||
}
|
||||
if (menuTag.isGCPadMenu) {
|
||||
// Not disabled
|
||||
if (value != 0)
|
||||
{
|
||||
val bundle = Bundle()
|
||||
bundle.putInt(SettingsFragmentPresenter.ARG_CONTROLLER_TYPE, value)
|
||||
activityView.showSettingsFragment(menuTag, bundle, true, gameId!!)
|
||||
}
|
||||
}
|
||||
if (menuTag.isWiimoteMenu) {
|
||||
// Emulated Wii Remote
|
||||
if (value == 1) {
|
||||
activityView.showSettingsFragment(menuTag, null, true, gameId!!)
|
||||
}
|
||||
}
|
||||
if (menuTag.isWiimoteExtensionMenu) {
|
||||
// Not disabled
|
||||
if (value != 0) {
|
||||
val bundle = Bundle()
|
||||
bundle.putInt(SettingsFragmentPresenter.ARG_CONTROLLER_TYPE, value)
|
||||
activityView.showSettingsFragment(menuTag, bundle, true, gameId!!)
|
||||
}
|
||||
}
|
||||
val action = getMenuTagAction(menuTag, value) ?: return
|
||||
activityView.showSettingsFragment(action.menuTag, action.extras, true, gameId!!)
|
||||
}
|
||||
|
||||
fun hasMenuTagActionForValue(menuTag: MenuTag, value: Int): Boolean {
|
||||
if (menuTag.isSerialPort1Menu) {
|
||||
return getMenuTagAction(menuTag, value) != null
|
||||
}
|
||||
|
||||
fun getMenuTagActionExtras(menuTag: MenuTag, value: Int): Bundle? {
|
||||
return getMenuTagAction(menuTag, value)?.extras
|
||||
}
|
||||
|
||||
private fun getMenuTagAction(menuTag: MenuTag, value: Int): MenuTagAction? {
|
||||
return when {
|
||||
// Not disabled or dummy
|
||||
return value != 0 && value != 255
|
||||
}
|
||||
if (menuTag.isGCPadMenu) {
|
||||
menuTag.isSerialPort1Menu && value != 0 && value != 255 -> MenuTagAction(
|
||||
menuTag, Bundle().apply {
|
||||
putInt(SettingsFragmentPresenter.ARG_SERIALPORT1_TYPE, value)
|
||||
})
|
||||
|
||||
// Not disabled
|
||||
return value != 0
|
||||
}
|
||||
if (menuTag.isWiimoteMenu) {
|
||||
menuTag.isGCPadMenu && value != 0 -> MenuTagAction(
|
||||
menuTag, Bundle().apply {
|
||||
putInt(SettingsFragmentPresenter.ARG_CONTROLLER_TYPE, value)
|
||||
})
|
||||
|
||||
// Emulated Wii Remote
|
||||
return value == 1
|
||||
}
|
||||
return if (menuTag.isWiimoteExtensionMenu) {
|
||||
menuTag.isWiimoteMenu && value == 1 -> MenuTagAction(menuTag, null)
|
||||
|
||||
// Not disabled
|
||||
value != 0
|
||||
} else false
|
||||
menuTag.isWiimoteExtensionMenu && value != 0 -> MenuTagAction(
|
||||
menuTag, Bundle().apply {
|
||||
putInt(SettingsFragmentPresenter.ARG_CONTROLLER_TYPE, value)
|
||||
})
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
private data class MenuTagAction(val menuTag: MenuTag, val extras: Bundle?)
|
||||
|
||||
companion object {
|
||||
private const val KEY_SETTINGS_SEARCH_ACTIVE = "settings_search_active"
|
||||
private const val KEY_SETTINGS_SEARCH_QUERY = "settings_search_query"
|
||||
}
|
||||
}
|
||||
+28
-6
@@ -10,6 +10,16 @@ import org.dolphinemu.dolphinemu.features.settings.model.Settings
|
||||
* Abstraction for the Activity that manages SettingsFragments.
|
||||
*/
|
||||
interface SettingsActivityView {
|
||||
/**
|
||||
* The query currently displayed in the settings search view.
|
||||
*/
|
||||
val settingsSearchQuery: String
|
||||
|
||||
/**
|
||||
* Whether the dedicated settings search screen is active.
|
||||
*/
|
||||
val isSettingsSearchActive: Boolean
|
||||
|
||||
/**
|
||||
* Show a new SettingsFragment.
|
||||
*
|
||||
@@ -17,12 +27,19 @@ interface SettingsActivityView {
|
||||
* @param addToStack Whether or not this fragment should replace a previous one.
|
||||
*/
|
||||
fun showSettingsFragment(
|
||||
menuTag: MenuTag,
|
||||
extras: Bundle?,
|
||||
addToStack: Boolean,
|
||||
gameId: String
|
||||
menuTag: MenuTag, extras: Bundle?, addToStack: Boolean, gameId: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Opens the settings screen containing a search result and scrolls to the result.
|
||||
*/
|
||||
fun showSearchResult(menuTag: MenuTag, settingPosition: Int, gameId: String, extras: Bundle?)
|
||||
|
||||
/**
|
||||
* Filters the root settings screen using the current search query.
|
||||
*/
|
||||
fun filterSettings(query: String)
|
||||
|
||||
/**
|
||||
* Shows a DialogFragment.
|
||||
*
|
||||
@@ -86,6 +103,11 @@ interface SettingsActivityView {
|
||||
*/
|
||||
fun hasMenuTagActionForValue(menuTag: MenuTag, value: Int): Boolean
|
||||
|
||||
/**
|
||||
* Returns the arguments used when opening a navigable setting's associated screen.
|
||||
*/
|
||||
fun getMenuTagActionExtras(menuTag: MenuTag, value: Int): Bundle?
|
||||
|
||||
/**
|
||||
* Show loading dialog while loading the settings
|
||||
*/
|
||||
@@ -102,9 +124,9 @@ interface SettingsActivityView {
|
||||
fun showGameIniJunkDeletionQuestion()
|
||||
|
||||
/**
|
||||
* Accesses the material toolbar layout and changes the title
|
||||
* Updates the settings app bar as a single state change.
|
||||
*/
|
||||
fun setToolbarTitle(title: String)
|
||||
fun setToolbarState(title: String, showHeadline: Boolean, showSearch: Boolean)
|
||||
/**
|
||||
* Returns whether the input mapping dialog should detect inputs from all devices,
|
||||
* not just the device configured for the controller.
|
||||
|
||||
+121
-111
@@ -27,14 +27,46 @@ import com.google.android.material.slider.Slider
|
||||
import com.google.android.material.timepicker.MaterialTimePicker
|
||||
import com.google.android.material.timepicker.TimeFormat
|
||||
import org.dolphinemu.dolphinemu.R
|
||||
import org.dolphinemu.dolphinemu.databinding.*
|
||||
import org.dolphinemu.dolphinemu.databinding.DialogAdvancedMappingBinding
|
||||
import org.dolphinemu.dolphinemu.databinding.DialogInputStringBinding
|
||||
import org.dolphinemu.dolphinemu.databinding.DialogSliderBinding
|
||||
import org.dolphinemu.dolphinemu.databinding.ListItemHeaderBinding
|
||||
import org.dolphinemu.dolphinemu.databinding.ListItemMappingBinding
|
||||
import org.dolphinemu.dolphinemu.databinding.ListItemSearchResultBinding
|
||||
import org.dolphinemu.dolphinemu.databinding.ListItemSettingBinding
|
||||
import org.dolphinemu.dolphinemu.databinding.ListItemSettingSwitchBinding
|
||||
import org.dolphinemu.dolphinemu.databinding.ListItemSubmenuBinding
|
||||
import org.dolphinemu.dolphinemu.features.input.model.view.InputMappingControlSetting
|
||||
import org.dolphinemu.dolphinemu.features.input.ui.AdvancedMappingDialog
|
||||
import org.dolphinemu.dolphinemu.features.input.ui.MotionAlertDialog
|
||||
import org.dolphinemu.dolphinemu.features.input.ui.viewholder.InputMappingControlSettingViewHolder
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.Settings
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.*
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.*
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.DateTimeChoiceSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.DirectoryPicker
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.FilePicker
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.FloatSliderSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.InputStringSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.IntSliderSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsSearchResult
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.SingleChoiceSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.SingleChoiceSettingDynamicDescriptions
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.SliderSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.StringSingleChoiceSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.SubmenuSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.SwitchSetting
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.DateTimeSettingViewHolder
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.FilePickerViewHolder
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.HeaderHyperLinkViewHolder
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.HeaderViewHolder
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.InputStringSettingViewHolder
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.RunRunnableViewHolder
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHolder
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingsSearchResultViewHolder
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SingleChoiceViewHolder
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SliderViewHolder
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SubmenuViewHolder
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SwitchSettingViewHolder
|
||||
import org.dolphinemu.dolphinemu.utils.DirectoryInitialization
|
||||
import org.dolphinemu.dolphinemu.utils.FileBrowserHelper
|
||||
import org.dolphinemu.dolphinemu.utils.Log
|
||||
@@ -42,14 +74,13 @@ import org.dolphinemu.dolphinemu.utils.PermissionsHandler
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.RandomAccessFile
|
||||
import java.util.*
|
||||
import java.util.Calendar
|
||||
import java.util.TimeZone
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class SettingsAdapter(
|
||||
private val fragmentView: SettingsFragmentView,
|
||||
private val context: Context
|
||||
) :
|
||||
RecyclerView.Adapter<SettingViewHolder>(), DialogInterface.OnClickListener,
|
||||
private val fragmentView: SettingsFragmentView, private val context: Context
|
||||
) : RecyclerView.Adapter<SettingViewHolder>(), DialogInterface.OnClickListener,
|
||||
Slider.OnChangeListener {
|
||||
private var settingsList: ArrayList<SettingsItem>? = null
|
||||
private var clickedItem: SettingsItem? = null
|
||||
@@ -68,55 +99,59 @@ class SettingsAdapter(
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return when (viewType) {
|
||||
SettingsItem.TYPE_HEADER -> HeaderViewHolder(
|
||||
ListItemHeaderBinding.inflate(inflater, parent, false),
|
||||
this
|
||||
ListItemHeaderBinding.inflate(inflater, parent, false), this
|
||||
)
|
||||
|
||||
SettingsItem.TYPE_SWITCH -> SwitchSettingViewHolder(
|
||||
ListItemSettingSwitchBinding.inflate(inflater, parent, false),
|
||||
this
|
||||
ListItemSettingSwitchBinding.inflate(inflater, parent, false), this
|
||||
)
|
||||
SettingsItem.TYPE_STRING_SINGLE_CHOICE,
|
||||
SettingsItem.TYPE_SINGLE_CHOICE_DYNAMIC_DESCRIPTIONS,
|
||||
SettingsItem.TYPE_SINGLE_CHOICE -> SingleChoiceViewHolder(
|
||||
ListItemSettingBinding.inflate(inflater, parent, false),
|
||||
this
|
||||
|
||||
SettingsItem.TYPE_STRING_SINGLE_CHOICE, SettingsItem.TYPE_SINGLE_CHOICE_DYNAMIC_DESCRIPTIONS, SettingsItem.TYPE_SINGLE_CHOICE -> SingleChoiceViewHolder(
|
||||
ListItemSettingBinding.inflate(inflater, parent, false), this
|
||||
)
|
||||
|
||||
SettingsItem.TYPE_SLIDER -> SliderViewHolder(
|
||||
ListItemSettingBinding.inflate(inflater, parent, false),
|
||||
this,
|
||||
context
|
||||
ListItemSettingBinding.inflate(inflater, parent, false), this, context
|
||||
)
|
||||
|
||||
SettingsItem.TYPE_SUBMENU -> SubmenuViewHolder(
|
||||
ListItemSubmenuBinding.inflate(inflater, parent, false),
|
||||
this
|
||||
ListItemSubmenuBinding.inflate(inflater, parent, false), this
|
||||
)
|
||||
|
||||
SettingsItem.TYPE_INPUT_MAPPING_CONTROL -> InputMappingControlSettingViewHolder(
|
||||
ListItemMappingBinding.inflate(inflater, parent, false),
|
||||
this
|
||||
ListItemMappingBinding.inflate(inflater, parent, false), this
|
||||
)
|
||||
SettingsItem.TYPE_FILE_PICKER,
|
||||
SettingsItem.TYPE_DIRECTORY_PICKER -> FilePickerViewHolder(
|
||||
ListItemSettingBinding.inflate(inflater, parent, false),
|
||||
this
|
||||
|
||||
SettingsItem.TYPE_FILE_PICKER, SettingsItem.TYPE_DIRECTORY_PICKER -> FilePickerViewHolder(
|
||||
ListItemSettingBinding.inflate(inflater, parent, false), this
|
||||
)
|
||||
|
||||
SettingsItem.TYPE_RUN_RUNNABLE -> RunRunnableViewHolder(
|
||||
ListItemSettingBinding.inflate(inflater, parent, false),
|
||||
this, context
|
||||
ListItemSettingBinding.inflate(inflater, parent, false), this, context
|
||||
)
|
||||
|
||||
SettingsItem.TYPE_STRING -> InputStringSettingViewHolder(
|
||||
ListItemSettingBinding.inflate(inflater, parent, false), this
|
||||
)
|
||||
|
||||
SettingsItem.TYPE_HYPERLINK_HEADER -> HeaderHyperLinkViewHolder(
|
||||
ListItemHeaderBinding.inflate(inflater, parent, false), this
|
||||
)
|
||||
|
||||
SettingsItem.TYPE_DATETIME_CHOICE -> DateTimeSettingViewHolder(
|
||||
ListItemSettingBinding.inflate(inflater, parent, false), this
|
||||
)
|
||||
|
||||
SettingsItem.TYPE_SEARCH_RESULT -> SettingsSearchResultViewHolder(
|
||||
ListItemSearchResultBinding.inflate(inflater, parent, false), this
|
||||
)
|
||||
|
||||
else -> throw IllegalArgumentException("Invalid view type: $viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: SettingViewHolder, position: Int) {
|
||||
holder.clearSearchResultHighlight()
|
||||
holder.bind(getItem(position))
|
||||
}
|
||||
|
||||
@@ -143,7 +178,7 @@ class SettingsAdapter(
|
||||
|
||||
fun clearSetting(item: SettingsItem) {
|
||||
item.clear(settings!!)
|
||||
fragmentView.onSettingChanged()
|
||||
fragmentView.onSettingChanged(item)
|
||||
}
|
||||
|
||||
fun notifyAllSettingsChanged() {
|
||||
@@ -153,7 +188,7 @@ class SettingsAdapter(
|
||||
|
||||
fun onBooleanClick(item: SwitchSetting, checked: Boolean) {
|
||||
item.setChecked(settings!!, checked)
|
||||
fragmentView.onSettingChanged()
|
||||
fragmentView.onSettingChanged(item)
|
||||
}
|
||||
|
||||
fun onInputStringClick(item: InputStringSetting, position: Int) {
|
||||
@@ -161,29 +196,24 @@ class SettingsAdapter(
|
||||
val binding = DialogInputStringBinding.inflate(inflater)
|
||||
val input = binding.input
|
||||
input.setText(item.selectedValue)
|
||||
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
|
||||
.setView(binding.root)
|
||||
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setView(binding.root)
|
||||
.setMessage(item.description)
|
||||
.setPositiveButton(R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
val editTextInput = input.text.toString()
|
||||
if (item.selectedValue != editTextInput) {
|
||||
notifyItemChanged(position)
|
||||
fragmentView.onSettingChanged()
|
||||
fragmentView.onSettingChanged(item)
|
||||
}
|
||||
item.setSelectedValue(fragmentView.settings!!, editTextInput)
|
||||
}
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}.setNegativeButton(R.string.cancel, null).show()
|
||||
}
|
||||
|
||||
fun onSingleChoiceClick(item: SingleChoiceSetting, position: Int) {
|
||||
clickedItem = item
|
||||
clickedPosition = position
|
||||
val value = getSelectionForSingleChoiceValue(item)
|
||||
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
|
||||
.setTitle(item.name)
|
||||
.setSingleChoiceItems(item.choicesId, value, this)
|
||||
.show()
|
||||
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
|
||||
.setSingleChoiceItems(item.choicesId, value, this).show()
|
||||
}
|
||||
|
||||
fun onStringSingleChoiceClick(item: StringSingleChoiceSetting, position: Int) {
|
||||
@@ -193,35 +223,26 @@ class SettingsAdapter(
|
||||
val choices = item.choices
|
||||
val noChoicesAvailableString = item.noChoicesAvailableString
|
||||
dialog = if (noChoicesAvailableString != 0 && choices.isEmpty()) {
|
||||
MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
|
||||
.setTitle(item.name)
|
||||
.setMessage(noChoicesAvailableString)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show()
|
||||
MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
|
||||
.setMessage(noChoicesAvailableString).setPositiveButton(R.string.ok, null).show()
|
||||
} else {
|
||||
MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
|
||||
.setTitle(item.name)
|
||||
MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
|
||||
.setSingleChoiceItems(
|
||||
item.choices, item.selectedValueIndex,
|
||||
this
|
||||
)
|
||||
.show()
|
||||
item.choices, item.selectedValueIndex, this
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun onSingleChoiceDynamicDescriptionsClick(
|
||||
item: SingleChoiceSettingDynamicDescriptions,
|
||||
position: Int
|
||||
item: SingleChoiceSettingDynamicDescriptions, position: Int
|
||||
) {
|
||||
clickedItem = item
|
||||
clickedPosition = position
|
||||
|
||||
val value = getSelectionForSingleChoiceDynamicDescriptionsValue(item)
|
||||
|
||||
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
|
||||
.setTitle(item.name)
|
||||
.setSingleChoiceItems(item.choicesId, value, this)
|
||||
.show()
|
||||
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
|
||||
.setSingleChoiceItems(item.choicesId, value, this).show()
|
||||
}
|
||||
|
||||
fun onSliderClick(item: SliderSetting, position: Int) {
|
||||
@@ -251,6 +272,7 @@ class SettingsAdapter(
|
||||
slider.valueTo = item.max
|
||||
slider.stepSize = item.stepSize
|
||||
}
|
||||
|
||||
is IntSliderSetting -> {
|
||||
slider.valueFrom = item.min.toFloat()
|
||||
slider.valueTo = item.max.toFloat()
|
||||
@@ -260,29 +282,27 @@ class SettingsAdapter(
|
||||
slider.value = (seekbarProgress / slider.stepSize).roundToInt() * slider.stepSize
|
||||
slider.addOnChangeListener(this)
|
||||
|
||||
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
|
||||
.setTitle(item.name)
|
||||
.setView(binding.root)
|
||||
.setPositiveButton(R.string.ok, this)
|
||||
.show()
|
||||
dialog = MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setTitle(item.name)
|
||||
.setView(binding.root).setPositiveButton(R.string.ok, this).show()
|
||||
}
|
||||
|
||||
fun onSubmenuClick(item: SubmenuSetting) {
|
||||
fragmentView.loadSubMenu(item.menuKey)
|
||||
}
|
||||
|
||||
fun onSearchResultClick(item: SettingsSearchResult) {
|
||||
fragmentView.loadSearchResult(item.menuKey, item.settingPosition, item.navigationExtras)
|
||||
}
|
||||
|
||||
fun onInputMappingClick(item: InputMappingControlSetting, position: Int) {
|
||||
if (item.controller.getDefaultDevice().isEmpty() && !fragmentView.isMappingAllDevices) {
|
||||
MaterialAlertDialogBuilder(fragmentView.fragmentActivity)
|
||||
.setMessage(R.string.input_binding_no_device)
|
||||
.setPositiveButton(R.string.ok, this)
|
||||
.show()
|
||||
MaterialAlertDialogBuilder(fragmentView.fragmentActivity).setMessage(R.string.input_binding_no_device)
|
||||
.setPositiveButton(R.string.ok, this).show()
|
||||
return
|
||||
}
|
||||
|
||||
val dialog = MotionAlertDialog(
|
||||
fragmentView.fragmentActivity, item,
|
||||
fragmentView.isMappingAllDevices
|
||||
fragmentView.fragmentActivity, item, fragmentView.isMappingAllDevices
|
||||
)
|
||||
|
||||
val background = ContextCompat.getDrawable(context, R.drawable.dialog_round)
|
||||
@@ -296,18 +316,16 @@ class SettingsAdapter(
|
||||
dialog.setTitle(R.string.input_binding)
|
||||
dialog.setMessage(
|
||||
String.format(
|
||||
context.getString(R.string.input_binding_description),
|
||||
item.name
|
||||
context.getString(R.string.input_binding_description), item.name
|
||||
)
|
||||
)
|
||||
dialog.setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(R.string.cancel), this)
|
||||
dialog.setButton(
|
||||
AlertDialog.BUTTON_NEUTRAL,
|
||||
context.getString(R.string.clear)
|
||||
AlertDialog.BUTTON_NEUTRAL, context.getString(R.string.clear)
|
||||
) { _: DialogInterface?, _: Int -> item.clearValue() }
|
||||
dialog.setOnDismissListener {
|
||||
notifyItemChanged(position)
|
||||
fragmentView.onSettingChanged()
|
||||
fragmentView.onSettingChanged(item)
|
||||
}
|
||||
dialog.setCanceledOnTouchOutside(false)
|
||||
dialog.show()
|
||||
@@ -317,10 +335,7 @@ class SettingsAdapter(
|
||||
val inflater = LayoutInflater.from(context)
|
||||
val binding = DialogAdvancedMappingBinding.inflate(inflater)
|
||||
val dialog = AdvancedMappingDialog(
|
||||
context,
|
||||
binding,
|
||||
item.controlReference,
|
||||
item.controller
|
||||
context, binding, item.controlReference, item.controller
|
||||
)
|
||||
|
||||
val background = ContextCompat.getDrawable(context, R.drawable.dialog_round)
|
||||
@@ -338,12 +353,11 @@ class SettingsAdapter(
|
||||
) { _: DialogInterface?, _: Int ->
|
||||
item.value = dialog.expression
|
||||
notifyItemChanged(position)
|
||||
fragmentView.onSettingChanged()
|
||||
fragmentView.onSettingChanged(item)
|
||||
}
|
||||
dialog.setButton(AlertDialog.BUTTON_NEGATIVE, context.getString(R.string.cancel), this)
|
||||
dialog.setButton(
|
||||
AlertDialog.BUTTON_NEUTRAL,
|
||||
context.getString(R.string.clear)
|
||||
AlertDialog.BUTTON_NEUTRAL, context.getString(R.string.clear)
|
||||
) { _: DialogInterface?, _: Int -> }
|
||||
dialog.setCanceledOnTouchOutside(false)
|
||||
dialog.show()
|
||||
@@ -361,14 +375,12 @@ class SettingsAdapter(
|
||||
val directoryPicker = item as DirectoryPicker
|
||||
|
||||
if (!PermissionsHandler.isExternalStorageLegacy()) {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setMessage(R.string.path_not_changeable_scoped_storage)
|
||||
MaterialAlertDialogBuilder(context).setMessage(R.string.path_not_changeable_scoped_storage)
|
||||
.setPositiveButton(R.string.ok) { dialog: DialogInterface, _: Int -> dialog.dismiss() }
|
||||
.show()
|
||||
} else {
|
||||
val intent = FileBrowserHelper.createDirectoryPickerIntent(
|
||||
fragmentView.fragmentActivity,
|
||||
FileBrowserHelper.GAME_EXTENSIONS
|
||||
fragmentView.fragmentActivity, FileBrowserHelper.GAME_EXTENSIONS
|
||||
)
|
||||
directoryPicker.launcher.launch(intent)
|
||||
}
|
||||
@@ -400,32 +412,24 @@ class SettingsAdapter(
|
||||
calendar.timeZone = TimeZone.getTimeZone("UTC")
|
||||
|
||||
// Start and end epoch times available for the Wii's date picker
|
||||
val calendarConstraints = CalendarConstraints.Builder()
|
||||
.setStart(946684800000L)
|
||||
.setEnd(2082672000000L)
|
||||
.build()
|
||||
val calendarConstraints =
|
||||
CalendarConstraints.Builder().setStart(946684800000L).setEnd(2082672000000L).build()
|
||||
|
||||
var timeFormat = TimeFormat.CLOCK_12H
|
||||
if (DateFormat.is24HourFormat(fragmentView.fragmentActivity)) {
|
||||
timeFormat = TimeFormat.CLOCK_24H
|
||||
}
|
||||
|
||||
val datePicker = MaterialDatePicker.Builder.datePicker()
|
||||
.setSelection(storedTime)
|
||||
.setTitleText(R.string.select_rtc_date)
|
||||
.setCalendarConstraints(calendarConstraints)
|
||||
.build()
|
||||
val timePicker = MaterialTimePicker.Builder()
|
||||
.setTimeFormat(timeFormat)
|
||||
.setHour(calendar[Calendar.HOUR_OF_DAY])
|
||||
.setMinute(calendar[Calendar.MINUTE])
|
||||
.setTitleText(R.string.select_rtc_time)
|
||||
val datePicker = MaterialDatePicker.Builder.datePicker().setSelection(storedTime)
|
||||
.setTitleText(R.string.select_rtc_date).setCalendarConstraints(calendarConstraints)
|
||||
.build()
|
||||
val timePicker = MaterialTimePicker.Builder().setTimeFormat(timeFormat)
|
||||
.setHour(calendar[Calendar.HOUR_OF_DAY]).setMinute(calendar[Calendar.MINUTE])
|
||||
.setTitleText(R.string.select_rtc_time).build()
|
||||
|
||||
datePicker.addOnPositiveButtonClickListener {
|
||||
timePicker.show(
|
||||
fragmentView.fragmentActivity.supportFragmentManager,
|
||||
"TimePicker"
|
||||
fragmentView.fragmentActivity.supportFragmentManager, "TimePicker"
|
||||
)
|
||||
}
|
||||
timePicker.addOnPositiveButtonClickListener {
|
||||
@@ -435,7 +439,7 @@ class SettingsAdapter(
|
||||
val rtcString = "0x" + java.lang.Long.toHexString(epochTime)
|
||||
if (item.getSelectedValue() != rtcString) {
|
||||
notifyItemChanged(clickedPosition)
|
||||
fragmentView.onSettingChanged()
|
||||
fragmentView.onSettingChanged(item)
|
||||
}
|
||||
item.setSelectedValue(fragmentView.settings!!, rtcString)
|
||||
clickedItem = null
|
||||
@@ -448,7 +452,7 @@ class SettingsAdapter(
|
||||
|
||||
if (filePicker.getSelectedValue() != selectedFile) {
|
||||
notifyItemChanged(clickedPosition)
|
||||
fragmentView.onSettingChanged()
|
||||
fragmentView.onSettingChanged(filePicker)
|
||||
}
|
||||
|
||||
filePicker.setSelectedValue(fragmentView.settings!!, selectedFile)
|
||||
@@ -470,44 +474,50 @@ class SettingsAdapter(
|
||||
val scSetting = clickedItem as SingleChoiceSetting
|
||||
|
||||
val value = getValueForSingleChoiceSelection(scSetting, which)
|
||||
if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
|
||||
if (scSetting.selectedValue != value) fragmentView.onSettingChanged(scSetting)
|
||||
|
||||
scSetting.setSelectedValue(settings!!, value)
|
||||
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
is SingleChoiceSettingDynamicDescriptions -> {
|
||||
val scSetting = clickedItem as SingleChoiceSettingDynamicDescriptions
|
||||
|
||||
val value = getValueForSingleChoiceDynamicDescriptionsSelection(scSetting, which)
|
||||
if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
|
||||
if (scSetting.selectedValue != value) fragmentView.onSettingChanged(scSetting)
|
||||
|
||||
scSetting.setSelectedValue(settings!!, value)
|
||||
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
is StringSingleChoiceSetting -> {
|
||||
val scSetting = clickedItem as StringSingleChoiceSetting
|
||||
|
||||
val value = scSetting.getValueAt(which)
|
||||
if (scSetting.selectedValue != value) fragmentView.onSettingChanged()
|
||||
if (scSetting.selectedValue != value) fragmentView.onSettingChanged(scSetting)
|
||||
|
||||
scSetting.setSelectedValue(settings!!, value)
|
||||
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
is IntSliderSetting -> {
|
||||
val sliderSetting = clickedItem as IntSliderSetting
|
||||
if (sliderSetting.selectedValue != seekbarProgress.toInt()) {
|
||||
fragmentView.onSettingChanged()
|
||||
fragmentView.onSettingChanged(sliderSetting)
|
||||
}
|
||||
sliderSetting.setSelectedValue(settings!!, seekbarProgress.toInt())
|
||||
closeDialog()
|
||||
}
|
||||
|
||||
is FloatSliderSetting -> {
|
||||
val sliderSetting = clickedItem as FloatSliderSetting
|
||||
|
||||
if (sliderSetting.selectedValue != seekbarProgress) fragmentView.onSettingChanged()
|
||||
if (sliderSetting.selectedValue != seekbarProgress) {
|
||||
fragmentView.onSettingChanged(sliderSetting)
|
||||
}
|
||||
|
||||
sliderSetting.setSelectedValue(settings!!, seekbarProgress)
|
||||
|
||||
@@ -540,6 +550,7 @@ class SettingsAdapter(
|
||||
|
||||
override fun onViewRecycled(holder: SettingViewHolder) {
|
||||
super.onViewRecycled(holder)
|
||||
holder.clearSearchResultHighlight()
|
||||
holder.onViewRecycled()
|
||||
}
|
||||
|
||||
@@ -587,8 +598,7 @@ class SettingsAdapter(
|
||||
}
|
||||
|
||||
private fun getValueForSingleChoiceDynamicDescriptionsSelection(
|
||||
item: SingleChoiceSettingDynamicDescriptions,
|
||||
which: Int
|
||||
item: SingleChoiceSettingDynamicDescriptions, which: Int
|
||||
): Int {
|
||||
val valuesId = item.valuesId
|
||||
return if (valuesId > 0) {
|
||||
|
||||
+139
-26
@@ -24,17 +24,22 @@ import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.dolphinemu.dolphinemu.R
|
||||
import org.dolphinemu.dolphinemu.databinding.FragmentSettingsBinding
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.Settings
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.viewholder.SettingViewHolder
|
||||
import org.dolphinemu.dolphinemu.utils.GpuDriverInstallResult
|
||||
import org.dolphinemu.dolphinemu.utils.SerializableHelper.serializable
|
||||
import java.util.*
|
||||
import kotlin.collections.ArrayList
|
||||
import java.util.EnumMap
|
||||
|
||||
class SettingsFragment : Fragment(), SettingsFragmentView {
|
||||
private lateinit var presenter: SettingsFragmentPresenter
|
||||
@@ -51,6 +56,11 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
|
||||
SettingsActivityResultLaunchers(this) { adapter }
|
||||
|
||||
private var oldControllerSettingsWarningHeight = 0
|
||||
private var hasScrolledToSearchResult = false
|
||||
private var highlightedSearchResult: SettingsItem? = null
|
||||
private var highlightedSearchResultPosition = RecyclerView.NO_POSITION
|
||||
private var searchIndexWarmupJob: Job? = null
|
||||
private var searchJob: Job? = null
|
||||
|
||||
private var binding: FragmentSettingsBinding? = null
|
||||
|
||||
@@ -82,9 +92,7 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
|
||||
): View {
|
||||
binding = FragmentSettingsBinding.inflate(inflater, container, false)
|
||||
return binding!!.root
|
||||
@@ -92,7 +100,11 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
if (titles.containsKey(menuTag)) {
|
||||
activityView!!.setToolbarTitle(getString(titles[menuTag]!!))
|
||||
activityView!!.setToolbarState(
|
||||
getString(titles[menuTag]!!),
|
||||
menuTag != MenuTag.SETTINGS,
|
||||
menuTag == MenuTag.SETTINGS
|
||||
)
|
||||
}
|
||||
|
||||
val manager = LinearLayoutManager(activity)
|
||||
@@ -107,10 +119,13 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
|
||||
setInsets()
|
||||
|
||||
val activity = requireActivity() as SettingsActivityView
|
||||
presenter.invalidateSearchIndex()
|
||||
presenter.onViewCreated(menuTag, activity.settings)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
clearSearchResultHighlight()
|
||||
searchJob?.cancel()
|
||||
super.onDestroyView()
|
||||
binding = null
|
||||
}
|
||||
@@ -129,7 +144,81 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
|
||||
}
|
||||
|
||||
override fun showSettingsList(settingsList: ArrayList<SettingsItem>) {
|
||||
adapter!!.setSettings(settingsList)
|
||||
val query = activityView?.settingsSearchQuery.orEmpty()
|
||||
val isShowingSearch =
|
||||
menuTag == MenuTag.SETTINGS && activityView?.isSettingsSearchActive == true
|
||||
if (!isShowingSearch) {
|
||||
adapter!!.setSettings(settingsList)
|
||||
}
|
||||
if (menuTag == MenuTag.SETTINGS) {
|
||||
warmUpSearchIndex()
|
||||
if (isShowingSearch) {
|
||||
applySettingsFilter(query)
|
||||
}
|
||||
}
|
||||
|
||||
val position = arguments?.getInt(
|
||||
ARGUMENT_SCROLL_TO_SETTING_POSITION, RecyclerView.NO_POSITION
|
||||
) ?: RecyclerView.NO_POSITION
|
||||
if (!hasScrolledToSearchResult && position in settingsList.indices) {
|
||||
hasScrolledToSearchResult = true
|
||||
binding?.listSettings?.post {
|
||||
val recyclerView = binding?.listSettings ?: return@post
|
||||
(recyclerView.layoutManager as? LinearLayoutManager)?.scrollToPositionWithOffset(
|
||||
position, 0
|
||||
)
|
||||
highlightSearchResult(position, settingsList[position])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun filterSettings(query: String) {
|
||||
if (!this::presenter.isInitialized || presenter.settings == null) {
|
||||
return
|
||||
}
|
||||
|
||||
applySettingsFilter(query)
|
||||
}
|
||||
|
||||
private fun applySettingsFilter(query: String) {
|
||||
searchJob?.cancel()
|
||||
if (query.isBlank()) {
|
||||
val results = if (activityView?.isSettingsSearchActive == true) {
|
||||
arrayListOf()
|
||||
} else {
|
||||
presenter.getSettingsList()
|
||||
}
|
||||
showSearchResults(query, results)
|
||||
return
|
||||
}
|
||||
|
||||
searchJob = viewLifecycleOwner.lifecycleScope.launch {
|
||||
delay(SEARCH_QUERY_DEBOUNCE_MS)
|
||||
val results = presenter.searchSettings(query)
|
||||
if (activityView?.settingsSearchQuery == query) {
|
||||
showSearchResults(query, results)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun warmUpSearchIndex() {
|
||||
if (searchIndexWarmupJob?.isActive == true) {
|
||||
return
|
||||
}
|
||||
|
||||
searchIndexWarmupJob = viewLifecycleOwner.lifecycleScope.launch {
|
||||
presenter.prepareSearchIndex()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSearchResults(query: String, results: ArrayList<SettingsItem>) {
|
||||
adapter!!.setSettings(results)
|
||||
binding?.textNoSearchResults?.text =
|
||||
getString(R.string.search_settings_no_results, query.trim())
|
||||
binding?.textNoSearchResults?.visibility =
|
||||
if (query.isNotBlank() && results.isEmpty()) View.VISIBLE else View.GONE
|
||||
binding?.listSettings?.visibility =
|
||||
if (query.isNotBlank() && results.isEmpty()) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
override fun loadSubMenu(menuKey: MenuTag) {
|
||||
@@ -139,13 +228,35 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
|
||||
}
|
||||
|
||||
activityView!!.showSettingsFragment(
|
||||
menuKey,
|
||||
null,
|
||||
true,
|
||||
requireArguments().getString(ARGUMENT_GAME_ID)!!
|
||||
menuKey, null, true, requireArguments().getString(ARGUMENT_GAME_ID)!!
|
||||
)
|
||||
}
|
||||
|
||||
override fun loadSearchResult(menuKey: MenuTag, settingPosition: Int, extras: Bundle?) {
|
||||
activityView!!.showSearchResult(
|
||||
menuKey, settingPosition, requireArguments().getString(ARGUMENT_GAME_ID)!!, extras
|
||||
)
|
||||
}
|
||||
|
||||
private fun highlightSearchResult(position: Int, setting: SettingsItem) {
|
||||
val recyclerView = binding?.listSettings ?: return
|
||||
highlightedSearchResult = setting
|
||||
highlightedSearchResultPosition = position
|
||||
recyclerView.post {
|
||||
(recyclerView.findViewHolderForAdapterPosition(position) as? SettingViewHolder)
|
||||
?.highlightSearchResult()
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearSearchResultHighlight() {
|
||||
val recyclerView = binding?.listSettings
|
||||
(recyclerView?.findViewHolderForAdapterPosition(
|
||||
highlightedSearchResultPosition
|
||||
) as? SettingViewHolder)?.clearSearchResultHighlight()
|
||||
highlightedSearchResult = null
|
||||
highlightedSearchResultPosition = RecyclerView.NO_POSITION
|
||||
}
|
||||
|
||||
override fun showDialogFragment(fragment: DialogFragment) {
|
||||
activityView!!.showDialogFragment(fragment)
|
||||
}
|
||||
@@ -157,7 +268,11 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
|
||||
override val settings: Settings?
|
||||
get() = presenter.settings
|
||||
|
||||
override fun onSettingChanged() {
|
||||
override fun onSettingChanged(setting: SettingsItem?) {
|
||||
if (setting == null || setting === highlightedSearchResult) {
|
||||
clearSearchResultHighlight()
|
||||
}
|
||||
presenter.invalidateSearchIndex()
|
||||
activityView!!.onSettingChanged()
|
||||
}
|
||||
|
||||
@@ -174,6 +289,10 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
|
||||
return activityView!!.hasMenuTagActionForValue(menuTag, value)
|
||||
}
|
||||
|
||||
override fun getMenuTagActionExtras(menuTag: MenuTag, value: Int): Bundle? {
|
||||
return activityView!!.getMenuTagActionExtras(menuTag, value)
|
||||
}
|
||||
|
||||
override var isMappingAllDevices: Boolean
|
||||
get() = activityView!!.isMappingAllDevices
|
||||
set(allDevices) {
|
||||
@@ -203,17 +322,13 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
|
||||
}
|
||||
val msg = "${presenter.gpuDriver!!.name} ${presenter.gpuDriver!!.driverVersion}"
|
||||
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(getString(R.string.gpu_driver_dialog_title))
|
||||
.setMessage(msg)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
MaterialAlertDialogBuilder(requireContext()).setTitle(getString(R.string.gpu_driver_dialog_title))
|
||||
.setMessage(msg).setNegativeButton(android.R.string.cancel, null)
|
||||
.setNeutralButton(R.string.gpu_driver_dialog_system) { _: DialogInterface?, _: Int ->
|
||||
presenter.useSystemDriver()
|
||||
}
|
||||
.setPositiveButton(R.string.gpu_driver_dialog_install) { _: DialogInterface?, _: Int ->
|
||||
}.setPositiveButton(R.string.gpu_driver_dialog_install) { _: DialogInterface?, _: Int ->
|
||||
askForDriverFile()
|
||||
}
|
||||
.show()
|
||||
}.show()
|
||||
}
|
||||
|
||||
override fun getFragmentLifecycle(): Lifecycle {
|
||||
@@ -230,16 +345,12 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
|
||||
|
||||
override fun onDriverInstallDone(result: GpuDriverInstallResult) {
|
||||
val view = binding?.root ?: return
|
||||
Snackbar
|
||||
.make(view, resolveInstallResultString(result), Snackbar.LENGTH_LONG)
|
||||
.show()
|
||||
Snackbar.make(view, resolveInstallResultString(result), Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onDriverUninstallDone() {
|
||||
Toast.makeText(
|
||||
requireContext(),
|
||||
R.string.gpu_driver_dialog_uninstall_done,
|
||||
Toast.LENGTH_SHORT
|
||||
requireContext(), R.string.gpu_driver_dialog_uninstall_done, Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
@@ -256,6 +367,8 @@ class SettingsFragment : Fragment(), SettingsFragmentView {
|
||||
companion object {
|
||||
private const val ARGUMENT_MENU_TAG = "menu_tag"
|
||||
private const val ARGUMENT_GAME_ID = "game_id"
|
||||
const val ARGUMENT_SCROLL_TO_SETTING_POSITION = "scroll_to_setting_position"
|
||||
private const val SEARCH_QUERY_DEBOUNCE_MS = 120L
|
||||
private val titles: MutableMap<MenuTag, Int> = EnumMap(MenuTag::class.java)
|
||||
|
||||
init {
|
||||
|
||||
+410
-359
File diff suppressed because it is too large.
Load diff
+13
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
package org.dolphinemu.dolphinemu.features.settings.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
@@ -50,6 +51,12 @@ interface SettingsFragmentView {
|
||||
* @param menuKey Identifier for the settings group that should be shown.
|
||||
*/
|
||||
fun loadSubMenu(menuKey: MenuTag)
|
||||
|
||||
/**
|
||||
* Opens the settings screen containing a search result and scrolls to the result.
|
||||
*/
|
||||
fun loadSearchResult(menuKey: MenuTag, settingPosition: Int, extras: Bundle?)
|
||||
|
||||
fun showDialogFragment(fragment: DialogFragment)
|
||||
|
||||
/**
|
||||
@@ -67,7 +74,7 @@ interface SettingsFragmentView {
|
||||
/**
|
||||
* Have the fragment tell the containing Activity that a Setting was modified.
|
||||
*/
|
||||
fun onSettingChanged()
|
||||
fun onSettingChanged(setting: SettingsItem? = null)
|
||||
|
||||
/**
|
||||
* Refetches the values of all controller settings.
|
||||
@@ -95,6 +102,11 @@ interface SettingsFragmentView {
|
||||
*/
|
||||
fun hasMenuTagActionForValue(menuTag: MenuTag, value: Int): Boolean
|
||||
|
||||
/**
|
||||
* Returns the arguments used when opening a navigable setting's associated screen.
|
||||
*/
|
||||
fun getMenuTagActionExtras(menuTag: MenuTag, value: Int): Bundle?
|
||||
|
||||
/**
|
||||
* Controls whether the input mapping dialog should detect inputs from all devices,
|
||||
* not just the device configured for the controller.
|
||||
|
||||
+43
@@ -2,14 +2,20 @@
|
||||
|
||||
package org.dolphinemu.dolphinemu.features.settings.ui.viewholder
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.view.View
|
||||
import android.view.View.OnLongClickListener
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.dolphinemu.dolphinemu.DolphinApplication
|
||||
import org.dolphinemu.dolphinemu.R
|
||||
@@ -21,6 +27,9 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
|
||||
LifecycleViewHolder(itemView, adapter.getFragmentLifecycle()),
|
||||
LifecycleOwner, View.OnClickListener, OnLongClickListener {
|
||||
|
||||
private val defaultBackground: Drawable? = itemView.background
|
||||
private var searchResultHighlightAnimator: ValueAnimator? = null
|
||||
|
||||
init {
|
||||
itemView.setOnClickListener(this)
|
||||
itemView.setOnLongClickListener(this)
|
||||
@@ -39,6 +48,35 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
|
||||
}
|
||||
}
|
||||
|
||||
fun highlightSearchResult() {
|
||||
clearSearchResultHighlight()
|
||||
|
||||
val highlight = ColorDrawable(
|
||||
MaterialColors.getColor(
|
||||
itemView, com.google.android.material.R.attr.colorSecondaryContainer
|
||||
)
|
||||
).apply { alpha = 0 }
|
||||
itemView.background = if (defaultBackground == null) {
|
||||
highlight
|
||||
} else {
|
||||
LayerDrawable(arrayOf(highlight, defaultBackground))
|
||||
}
|
||||
searchResultHighlightAnimator = ValueAnimator.ofInt(
|
||||
0, SEARCH_RESULT_HIGHLIGHT_MAX_ALPHA
|
||||
).apply {
|
||||
duration = SEARCH_RESULT_HIGHLIGHT_FADE_IN_DURATION_MS
|
||||
interpolator = DecelerateInterpolator()
|
||||
addUpdateListener { highlight.alpha = it.animatedValue as Int }
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSearchResultHighlight() {
|
||||
searchResultHighlightAnimator?.cancel()
|
||||
searchResultHighlightAnimator = null
|
||||
itemView.background = defaultBackground
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the adapter to set this ViewHolder's child views to display the list item
|
||||
* it must now represent.
|
||||
@@ -102,4 +140,9 @@ abstract class SettingViewHolder(itemView: View, protected val adapter: Settings
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SEARCH_RESULT_HIGHLIGHT_FADE_IN_DURATION_MS = 180L
|
||||
private const val SEARCH_RESULT_HIGHLIGHT_MAX_ALPHA = 255
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
package org.dolphinemu.dolphinemu.features.settings.ui.viewholder
|
||||
|
||||
import android.view.View
|
||||
import org.dolphinemu.dolphinemu.databinding.ListItemSearchResultBinding
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsItem
|
||||
import org.dolphinemu.dolphinemu.features.settings.model.view.SettingsSearchResult
|
||||
import org.dolphinemu.dolphinemu.features.settings.ui.SettingsAdapter
|
||||
|
||||
class SettingsSearchResultViewHolder(
|
||||
private val binding: ListItemSearchResultBinding, adapter: SettingsAdapter
|
||||
) : SettingViewHolder(binding.root, adapter) {
|
||||
private lateinit var result: SettingsSearchResult
|
||||
|
||||
override val item: SettingsItem
|
||||
get() = result
|
||||
|
||||
override fun bind(item: SettingsItem) {
|
||||
result = item as SettingsSearchResult
|
||||
binding.textSettingName.text = item.name
|
||||
binding.textSettingDescription.text = item.description
|
||||
}
|
||||
|
||||
override fun onClick(clicked: View) {
|
||||
adapter.onSearchResultClick(result)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -47,7 +47,7 @@ class SkylanderSlotAdapter(
|
||||
|
||||
holder.binding.buttonLoadFigure.setOnClickListener {
|
||||
activity.setSkylanderData(0, 0, "", position)
|
||||
activity.requestSkylanderFile.launch("*/*")
|
||||
activity.requestSkylanderFile.launch(arrayOf("*/*"))
|
||||
}
|
||||
|
||||
val inflater = LayoutInflater.from(activity)
|
||||
|
||||
@@ -89,10 +89,6 @@ fun PreviewTheme(
|
||||
private fun Context.toDolphinColorScheme(isDark: Boolean): ColorScheme {
|
||||
fun attr(@AttrRes attr: Int) = Color(MaterialColors.getColor(this, attr, 0))
|
||||
|
||||
val background = obtainStyledAttributes(intArrayOf(android.R.attr.colorBackground)).use {
|
||||
Color(it.getColor(0, 0))
|
||||
}
|
||||
|
||||
return if (isDark) {
|
||||
darkColorScheme(
|
||||
primary = attr(AppCompatR.attr.colorPrimary),
|
||||
@@ -111,7 +107,7 @@ private fun Context.toDolphinColorScheme(isDark: Boolean): ColorScheme {
|
||||
onError = attr(MaterialR.attr.colorOnError),
|
||||
errorContainer = attr(MaterialR.attr.colorErrorContainer),
|
||||
onErrorContainer = attr(MaterialR.attr.colorOnErrorContainer),
|
||||
background = background,
|
||||
background = attr(android.R.attr.colorBackground),
|
||||
onBackground = attr(MaterialR.attr.colorOnBackground),
|
||||
surface = attr(MaterialR.attr.colorSurface),
|
||||
onSurface = attr(MaterialR.attr.colorOnSurface),
|
||||
@@ -140,7 +136,7 @@ private fun Context.toDolphinColorScheme(isDark: Boolean): ColorScheme {
|
||||
onError = attr(MaterialR.attr.colorOnError),
|
||||
errorContainer = attr(MaterialR.attr.colorErrorContainer),
|
||||
onErrorContainer = attr(MaterialR.attr.colorOnErrorContainer),
|
||||
background = background,
|
||||
background = attr(android.R.attr.colorBackground),
|
||||
onBackground = attr(MaterialR.attr.colorOnBackground),
|
||||
surface = attr(MaterialR.attr.colorSurface),
|
||||
onSurface = attr(MaterialR.attr.colorOnSurface),
|
||||
@@ -257,7 +253,7 @@ fun OutlinedBox(
|
||||
if (onClick != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.matchParentSize()
|
||||
.clip(MaterialTheme.shapes.extraSmall)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
package org.dolphinemu.dolphinemu.utils
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import org.dolphinemu.dolphinemu.R
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import org.dolphinemu.dolphinemu.ui.main.ThemeProvider
|
||||
import android.content.res.Configuration
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.elevation.ElevationOverlayProvider
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import android.os.Build
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.google.android.material.appbar.AppBarLayout
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.elevation.ElevationOverlayProvider
|
||||
import org.dolphinemu.dolphinemu.R
|
||||
import org.dolphinemu.dolphinemu.ui.main.ThemeProvider
|
||||
|
||||
object ThemeHelper {
|
||||
|
||||
@@ -52,8 +52,7 @@ object ThemeHelper {
|
||||
.getInt(CURRENT_THEME_MODE, AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
activity.delegate.localNightMode = themeMode
|
||||
val windowController = WindowCompat.getInsetsController(
|
||||
activity.window,
|
||||
activity.window.decorView
|
||||
activity.window, activity.window.decorView
|
||||
)
|
||||
val systemReportedThemeMode =
|
||||
activity.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
|
||||
@@ -62,6 +61,7 @@ object ThemeHelper {
|
||||
Configuration.UI_MODE_NIGHT_NO -> setLightModeSystemBars(windowController)
|
||||
Configuration.UI_MODE_NIGHT_YES -> setDarkModeSystemBars(windowController)
|
||||
}
|
||||
|
||||
AppCompatDelegate.MODE_NIGHT_NO -> setLightModeSystemBars(windowController)
|
||||
AppCompatDelegate.MODE_NIGHT_YES -> setDarkModeSystemBars(windowController)
|
||||
}
|
||||
@@ -83,66 +83,50 @@ object ThemeHelper {
|
||||
|
||||
@JvmStatic
|
||||
fun saveTheme(activity: AppCompatActivity, themeValue: Int) {
|
||||
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
|
||||
.edit()
|
||||
.putInt(CURRENT_THEME, themeValue)
|
||||
.apply()
|
||||
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
|
||||
.putInt(CURRENT_THEME, themeValue).apply()
|
||||
activity.recreate()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun deleteThemeKey(activity: AppCompatActivity) {
|
||||
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
|
||||
.edit()
|
||||
.remove(CURRENT_THEME)
|
||||
.apply()
|
||||
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
|
||||
.remove(CURRENT_THEME).apply()
|
||||
activity.recreate()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun saveThemeMode(activity: AppCompatActivity, themeModeValue: Int) {
|
||||
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
|
||||
.edit()
|
||||
.putInt(CURRENT_THEME_MODE, themeModeValue)
|
||||
.apply()
|
||||
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
|
||||
.putInt(CURRENT_THEME_MODE, themeModeValue).apply()
|
||||
setThemeMode(activity)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun deleteThemeModeKey(activity: AppCompatActivity) {
|
||||
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
|
||||
.edit()
|
||||
.remove(CURRENT_THEME_MODE)
|
||||
.apply()
|
||||
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
|
||||
.remove(CURRENT_THEME_MODE).apply()
|
||||
setThemeMode(activity)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun saveBackgroundSetting(activity: AppCompatActivity, backgroundValue: Boolean) {
|
||||
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
|
||||
.edit()
|
||||
.putBoolean(USE_BLACK_BACKGROUNDS, backgroundValue)
|
||||
.apply()
|
||||
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
|
||||
.putBoolean(USE_BLACK_BACKGROUNDS, backgroundValue).apply()
|
||||
activity.recreate()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun deleteBackgroundSetting(activity: AppCompatActivity) {
|
||||
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
|
||||
.edit()
|
||||
.remove(USE_BLACK_BACKGROUNDS)
|
||||
.apply()
|
||||
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
|
||||
.remove(USE_BLACK_BACKGROUNDS).apply()
|
||||
activity.recreate()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun resetThemePreferences(activity: AppCompatActivity, applyImmediately: Boolean = false) {
|
||||
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext)
|
||||
.edit()
|
||||
.remove(CURRENT_THEME)
|
||||
.remove(CURRENT_THEME_MODE)
|
||||
.remove(USE_BLACK_BACKGROUNDS)
|
||||
.apply()
|
||||
PreferenceManager.getDefaultSharedPreferences(activity.applicationContext).edit()
|
||||
.remove(CURRENT_THEME).remove(CURRENT_THEME_MODE).remove(USE_BLACK_BACKGROUNDS).apply()
|
||||
activity.delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
activity.delegate.applyDayNight()
|
||||
if (applyImmediately) {
|
||||
@@ -170,7 +154,7 @@ object ThemeHelper {
|
||||
activity: AppCompatActivity, toolbar: MaterialToolbar, appBarLayout: AppBarLayout
|
||||
) {
|
||||
appBarLayout.addOnOffsetChangedListener { layout: AppBarLayout, verticalOffset: Int ->
|
||||
if (-verticalOffset >= layout.totalScrollRange / 2) {
|
||||
if (layout.totalScrollRange > 0 && -verticalOffset >= layout.totalScrollRange / 2) {
|
||||
@ColorInt val color =
|
||||
ElevationOverlayProvider(appBarLayout.context).compositeOverlay(
|
||||
MaterialColors.getColor(appBarLayout, R.attr.colorSurface),
|
||||
@@ -180,8 +164,7 @@ object ThemeHelper {
|
||||
setStatusBarColor(activity, color)
|
||||
} else {
|
||||
@ColorInt val statusBarColor = ContextCompat.getColor(
|
||||
activity.applicationContext,
|
||||
android.R.color.transparent
|
||||
activity.applicationContext, android.R.color.transparent
|
||||
)
|
||||
@ColorInt val appBarColor = MaterialColors.getColor(toolbar, R.attr.colorSurface)
|
||||
toolbar.setBackgroundColor(appBarColor)
|
||||
@@ -198,8 +181,7 @@ object ThemeHelper {
|
||||
setStatusBarColor(activity, color)
|
||||
} else {
|
||||
@ColorInt val statusBarColor = ContextCompat.getColor(
|
||||
activity.applicationContext,
|
||||
android.R.color.transparent
|
||||
activity.applicationContext, android.R.color.transparent
|
||||
)
|
||||
setStatusBarColor(activity, statusBarColor)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="180"
|
||||
android:fromAlpha="0"
|
||||
android:interpolator="@android:anim/decelerate_interpolator"
|
||||
android:startOffset="60"
|
||||
android:toAlpha="1" />
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="90"
|
||||
android:fromAlpha="1"
|
||||
android:interpolator="@android:anim/accelerate_interpolator"
|
||||
android:toAlpha="0" />
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:alpha="0.45" android:color="?attr/colorOutline" />
|
||||
</selector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M9.5,3a6.5,6.5 0,1 0,0 13a6.5,6.5 0,0 0,0 -13zM9.5,5a4.5,4.5 0,1 1,0 9a4.5,4.5 0,0 1,0 -9zM14.65,13.24l5.56,5.56l-1.41,1.41l-5.56,-5.56z" />
|
||||
</vector>
|
||||
@@ -1,22 +1,50 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout android:id="@+id/coordinator_main"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/coordinator_main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/frame_content_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/old_controller_settings_warning"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?attr/colorErrorContainer"
|
||||
android:clickable="true"
|
||||
android:focusable="false"
|
||||
android:text="@string/old_controller_settings"
|
||||
android:textColor="?attr/colorOnErrorContainer"
|
||||
android:visibility="invisible" />
|
||||
|
||||
<View
|
||||
android:id="@+id/workaround_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@android:color/transparent"
|
||||
android:clickable="true"
|
||||
android:focusable="false" />
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@android:color/transparent"
|
||||
app:backgroundTint="@android:color/transparent"
|
||||
app:elevation="0dp">
|
||||
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
style="?attr/collapsingToolbarLayoutMediumStyle"
|
||||
android:id="@+id/toolbar_settings_layout"
|
||||
style="?attr/collapsingToolbarLayoutMediumStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/collapsingToolbarLayoutMediumSize"
|
||||
app:contentScrim="@android:color/transparent"
|
||||
@@ -31,33 +59,94 @@
|
||||
|
||||
</com.google.android.material.appbar.CollapsingToolbarLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/settings_search_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface"
|
||||
android:paddingBottom="@dimen/spacing_medlarge">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/settings_search_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:foreground="?android:attr/selectableItemBackground"
|
||||
android:layout_marginEnd="@dimen/spacing_large"
|
||||
android:layout_marginStart="@dimen/spacing_large"
|
||||
app:cardBackgroundColor="?attr/colorSurfaceVariant"
|
||||
app:cardCornerRadius="28dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeColor="@color/settings_search_outline"
|
||||
app:strokeWidth="1dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingEnd="@dimen/spacing_large"
|
||||
android:paddingStart="20dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:contentDescription="@null"
|
||||
app:srcCompat="@drawable/ic_search"
|
||||
app:tint="?android:attr/textColorSecondary" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/spacing_large"
|
||||
android:text="@string/search_settings"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:textSize="18sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/settings_search_mode_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/settings_search_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorSurface"
|
||||
app:navigationContentDescription="@string/search_settings_back"
|
||||
app:navigationIcon="?attr/homeAsUpIndicator">
|
||||
|
||||
<androidx.appcompat.widget.SearchView
|
||||
android:id="@+id/settings_search"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@android:color/transparent"
|
||||
android:imeOptions="actionSearch"
|
||||
android:inputType="text"
|
||||
app:iconifiedByDefault="false"
|
||||
app:queryBackground="@android:color/transparent"
|
||||
app:queryHint="@string/search_settings"
|
||||
app:searchIcon="@null" />
|
||||
|
||||
</com.google.android.material.appbar.MaterialToolbar>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/settings_search_outline" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/frame_content_settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/old_controller_settings_warning"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?attr/colorErrorContainer"
|
||||
android:text="@string/old_controller_settings"
|
||||
android:textColor="?attr/colorOnErrorContainer"
|
||||
android:visibility="invisible"
|
||||
android:clickable="true"
|
||||
android:focusable="false" />
|
||||
|
||||
<View
|
||||
android:id="@+id/workaround_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:clickable="true"
|
||||
android:focusable="false"
|
||||
android:background="@android:color/transparent" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
@@ -11,4 +10,16 @@
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_no_search_results"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:padding="@dimen/spacing_xtralarge"
|
||||
android:text="@string/search_settings_no_results"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:minHeight="64dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/spacing_large"
|
||||
android:paddingEnd="@dimen/spacing_large"
|
||||
android:paddingStart="@dimen/spacing_large"
|
||||
android:paddingTop="@dimen/spacing_large">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_setting_name"
|
||||
style="@style/TextAppearance.MaterialComponents.Headline5"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAlignment="viewStart"
|
||||
android:textSize="16sp"
|
||||
tools:text="Internal Resolution" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_setting_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_small"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
tools:text="Graphics Settings › Enhancements" />
|
||||
|
||||
</LinearLayout>
|
||||
@@ -62,6 +62,10 @@
|
||||
|
||||
<!-- Main Preference Fragment -->
|
||||
<string name="settings">Settings</string>
|
||||
<string name="search_settings">Search settings</string>
|
||||
<string name="search_settings_back">Back to settings</string>
|
||||
<string name="search_settings_no_results">No settings found for “%1$s”</string>
|
||||
<string name="search_settings_category_path">%1$s › %2$s</string>
|
||||
<string name="game_settings">Game Settings: %1$s</string>
|
||||
<string name="config">Config</string>
|
||||
<string name="graphics_settings">Graphics Settings</string>
|
||||
@@ -952,6 +956,7 @@ It can efficiently compress both junk data and encrypted Wii data.
|
||||
<string name="skylander_id">ID</string>
|
||||
<string name="skylander_variant">Variant</string>
|
||||
<string name="invalid_skylander">Invalid Skylander Selection</string>
|
||||
<string name="skylander_load_failed">Failed to load Skylander figure</string>
|
||||
|
||||
<string name="emulate_infinity_base">Infinity Base</string>
|
||||
<string name="infinity_manager">Infinity Manager</string>
|
||||
@@ -1012,6 +1017,9 @@ It can efficiently compress both junk data and encrypted Wii data.
|
||||
<string name="netplay_start">Start</string>
|
||||
<string name="netplay_start_warning_title">Warning</string>
|
||||
<string name="netplay_start_warning_not_all_players_have_game">Not all players have the game. Do you really want to start?</string>
|
||||
<string name="netplay_dual_core_warning_title">Dual Core Enabled</string>
|
||||
<string name="netplay_dual_core_warning_message">Dual core during netplay is not recommended. It can cause performance issues and prevent certain games from booting.</string>
|
||||
<string name="netplay_dont_warn_again">Don\'t warn again</string>
|
||||
<string name="netplay_chat_label">Chat</string>
|
||||
<string name="netplay_chat_send">Send</string>
|
||||
<string name="netplay_message_game_changed">Game changed to %1$s</string>
|
||||
@@ -1023,6 +1031,11 @@ It can efficiently compress both junk data and encrypted Wii data.
|
||||
<string name="netplay_players_name">Name</string>
|
||||
<string name="netplay_players_ping">Ping</string>
|
||||
<string name="netplay_players_mapping">Mapping</string>
|
||||
<string name="netplay_controllers_gamecube">GameCube</string>
|
||||
<string name="netplay_controllers_wii_remotes">Wii Remotes</string>
|
||||
<string name="netplay_controllers_port">Port %1$d</string>
|
||||
<string name="netplay_controllers_remote">Remote %1$d</string>
|
||||
<string name="netplay_controllers_none">None</string>
|
||||
<string name="netplay_network_mode_label">Input mode</string>
|
||||
<string name="netplay_network_mode_fair_input_delay">Fair Input Delay</string>
|
||||
<string name="netplay_network_mode_host_input_authority">Host Input Authority</string>
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
// Copyright 2026 Dolphin Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include <jni.h>
|
||||
@@ -38,6 +41,30 @@ static NetPlay::NetPlayServer* GetServerPointer(JNIEnv* env, jobject obj)
|
||||
env->GetLongField(obj, IDCache::GetNetPlayServerPointer()));
|
||||
}
|
||||
|
||||
static jintArray PadMappingArrayToJIntArray(JNIEnv* env, const NetPlay::PadMappingArray& mapping)
|
||||
{
|
||||
const jsize size = static_cast<jsize>(mapping.size());
|
||||
jintArray jmapping = env->NewIntArray(size);
|
||||
std::array<jint, std::tuple_size_v<std::decay_t<decltype(mapping)>>> values;
|
||||
for (size_t i = 0; i < mapping.size(); i++)
|
||||
values[i] = static_cast<jint>(mapping[i]);
|
||||
env->SetIntArrayRegion(jmapping, 0, size, values.data());
|
||||
return jmapping;
|
||||
}
|
||||
|
||||
static bool JIntArrayToPadMappingArray(JNIEnv* env, jintArray jmapping,
|
||||
NetPlay::PadMappingArray* mapping)
|
||||
{
|
||||
if (env->GetArrayLength(jmapping) != static_cast<jsize>(mapping->size()))
|
||||
return false;
|
||||
|
||||
std::array<jint, std::tuple_size_v<std::decay_t<decltype(*mapping)>>> values;
|
||||
env->GetIntArrayRegion(jmapping, 0, static_cast<jsize>(values.size()), values.data());
|
||||
for (size_t i = 0; i < mapping->size(); i++)
|
||||
(*mapping)[i] = static_cast<NetPlay::PlayerId>(values[i]);
|
||||
return true;
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
@@ -224,6 +251,54 @@ Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReconnectTr
|
||||
Common::g_TraversalClient->ReconnectToServer();
|
||||
}
|
||||
|
||||
JNIEXPORT jintArray JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeGetPadMapping(JNIEnv* env,
|
||||
jobject obj)
|
||||
{
|
||||
auto* server = GetServerPointer(env, obj);
|
||||
if (!server)
|
||||
return env->NewIntArray(0);
|
||||
|
||||
return PadMappingArrayToJIntArray(env, server->GetPadMapping());
|
||||
}
|
||||
|
||||
JNIEXPORT jintArray JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeGetWiimoteMapping(JNIEnv* env,
|
||||
jobject obj)
|
||||
{
|
||||
auto* server = GetServerPointer(env, obj);
|
||||
if (!server)
|
||||
return env->NewIntArray(0);
|
||||
|
||||
return PadMappingArrayToJIntArray(env, server->GetWiimoteMapping());
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeSetPadMapping(
|
||||
JNIEnv* env, jobject obj, jintArray jmapping)
|
||||
{
|
||||
auto* server = GetServerPointer(env, obj);
|
||||
if (!server)
|
||||
return;
|
||||
|
||||
NetPlay::PadMappingArray mapping = server->GetPadMapping();
|
||||
if (JIntArrayToPadMappingArray(env, jmapping, &mapping))
|
||||
server->SetPadMapping(mapping);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeSetWiimoteMapping(
|
||||
JNIEnv* env, jobject obj, jintArray jmapping)
|
||||
{
|
||||
auto* server = GetServerPointer(env, obj);
|
||||
if (!server)
|
||||
return;
|
||||
|
||||
NetPlay::PadMappingArray mapping = server->GetWiimoteMapping();
|
||||
if (JIntArrayToPadMappingArray(env, jmapping, &mapping))
|
||||
server->SetWiimoteMapping(mapping);
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL
|
||||
Java_org_dolphinemu_dolphinemu_features_netplay_NetplaySession_nativeReleaseUICallbacks(
|
||||
JNIEnv*, jobject, jlong pointer)
|
||||
|
||||
@@ -49,6 +49,7 @@ else()
|
||||
check_and_add_flag(INIT_SELF -Winit-self)
|
||||
check_and_add_flag(MISSING_DECLARATIONS -Wmissing-declarations)
|
||||
check_and_add_flag(MISSING_VARIABLE_DECLARATIONS -Wmissing-variable-declarations)
|
||||
check_and_add_flag(IMPLICIT_FALLTHROUGH -Wimplicit-fallthrough)
|
||||
|
||||
# Disable -Wstringop-truncation warnings as they result in many false positives.
|
||||
# In most (all?) cases where std::strncpy is used, we want to fill the entire buffer
|
||||
|
||||
@@ -2157,8 +2157,10 @@ static void InitExtensionList(GLContext* context)
|
||||
default:
|
||||
case 320:
|
||||
s_extension_list["VERSION_GLES_3_2"] = true;
|
||||
[[fallthrough]];
|
||||
case 310:
|
||||
s_extension_list["VERSION_GLES_3_1"] = true;
|
||||
[[fallthrough]];
|
||||
case 300:
|
||||
s_extension_list["VERSION_GLES_3"] = true;
|
||||
break;
|
||||
@@ -2195,6 +2197,7 @@ static void InitExtensionList(GLContext* context)
|
||||
};
|
||||
for (auto it : gl450exts)
|
||||
s_extension_list[it] = true;
|
||||
[[fallthrough]];
|
||||
}
|
||||
case 440:
|
||||
{
|
||||
@@ -2211,6 +2214,7 @@ static void InitExtensionList(GLContext* context)
|
||||
};
|
||||
for (auto it : gl440exts)
|
||||
s_extension_list[it] = true;
|
||||
[[fallthrough]];
|
||||
}
|
||||
case 430:
|
||||
{
|
||||
@@ -2239,6 +2243,7 @@ static void InitExtensionList(GLContext* context)
|
||||
};
|
||||
for (auto it : gl430exts)
|
||||
s_extension_list[it] = true;
|
||||
[[fallthrough]];
|
||||
}
|
||||
case 420:
|
||||
{
|
||||
@@ -2259,6 +2264,7 @@ static void InitExtensionList(GLContext* context)
|
||||
};
|
||||
for (auto it : gl420exts)
|
||||
s_extension_list[it] = true;
|
||||
[[fallthrough]];
|
||||
}
|
||||
case 410:
|
||||
{
|
||||
@@ -2273,6 +2279,7 @@ static void InitExtensionList(GLContext* context)
|
||||
};
|
||||
for (auto it : gl410exts)
|
||||
s_extension_list[it] = true;
|
||||
[[fallthrough]];
|
||||
}
|
||||
case 400:
|
||||
{
|
||||
@@ -2293,6 +2300,7 @@ static void InitExtensionList(GLContext* context)
|
||||
};
|
||||
for (auto it : gl400exts)
|
||||
s_extension_list[it] = true;
|
||||
[[fallthrough]];
|
||||
}
|
||||
case 330:
|
||||
{
|
||||
@@ -2311,6 +2319,7 @@ static void InitExtensionList(GLContext* context)
|
||||
};
|
||||
for (auto it : gl330exts)
|
||||
s_extension_list[it] = true;
|
||||
[[fallthrough]];
|
||||
}
|
||||
case 320:
|
||||
{
|
||||
@@ -2328,6 +2337,7 @@ static void InitExtensionList(GLContext* context)
|
||||
};
|
||||
for (auto it : gl320exts)
|
||||
s_extension_list[it] = true;
|
||||
[[fallthrough]];
|
||||
}
|
||||
case 310:
|
||||
{
|
||||
@@ -2343,6 +2353,7 @@ static void InitExtensionList(GLContext* context)
|
||||
};
|
||||
for (auto it : gl310exts)
|
||||
s_extension_list[it] = true;
|
||||
[[fallthrough]];
|
||||
}
|
||||
case 300:
|
||||
{
|
||||
@@ -2374,6 +2385,7 @@ static void InitExtensionList(GLContext* context)
|
||||
};
|
||||
for (auto it : gl300exts)
|
||||
s_extension_list[it] = true;
|
||||
[[fallthrough]];
|
||||
}
|
||||
case 210:
|
||||
case 200:
|
||||
|
||||
@@ -136,33 +136,46 @@ static u64 GetMurmurHash3(const u8* src, u32 len, u32 samples)
|
||||
{
|
||||
case 15:
|
||||
k2 ^= u64(tail[14]) << 48;
|
||||
[[fallthrough]];
|
||||
case 14:
|
||||
k2 ^= u64(tail[13]) << 40;
|
||||
[[fallthrough]];
|
||||
case 13:
|
||||
k2 ^= u64(tail[12]) << 32;
|
||||
[[fallthrough]];
|
||||
case 12:
|
||||
k2 ^= u64(tail[11]) << 24;
|
||||
[[fallthrough]];
|
||||
case 11:
|
||||
k2 ^= u64(tail[10]) << 16;
|
||||
[[fallthrough]];
|
||||
case 10:
|
||||
k2 ^= u64(tail[9]) << 8;
|
||||
[[fallthrough]];
|
||||
case 9:
|
||||
k2 ^= u64(tail[8]) << 0;
|
||||
|
||||
[[fallthrough]];
|
||||
case 8:
|
||||
k1 ^= u64(tail[7]) << 56;
|
||||
[[fallthrough]];
|
||||
case 7:
|
||||
k1 ^= u64(tail[6]) << 48;
|
||||
[[fallthrough]];
|
||||
case 6:
|
||||
k1 ^= u64(tail[5]) << 40;
|
||||
[[fallthrough]];
|
||||
case 5:
|
||||
k1 ^= u64(tail[4]) << 32;
|
||||
[[fallthrough]];
|
||||
case 4:
|
||||
k1 ^= u64(tail[3]) << 24;
|
||||
[[fallthrough]];
|
||||
case 3:
|
||||
k1 ^= u64(tail[2]) << 16;
|
||||
[[fallthrough]];
|
||||
case 2:
|
||||
k1 ^= u64(tail[1]) << 8;
|
||||
[[fallthrough]];
|
||||
case 1:
|
||||
k1 ^= u64(tail[0]) << 0;
|
||||
bmix64(h1, h2, k1, k2, c1, c2);
|
||||
|
||||
@@ -26,7 +26,11 @@ void MemArena::GrabSHMSegment(size_t size, std::string_view base_name)
|
||||
}
|
||||
|
||||
memory_object_size_t entry_size = size;
|
||||
constexpr vm_prot_t prot = VM_PROT_READ | VM_PROT_WRITE;
|
||||
// Large anonymous mappings are split into 128 MB chunks. Without MAP_MEM_VM_SHARE,
|
||||
// mach_make_memory_entry_64 will only return an entry spanning the first chunk.
|
||||
// Attempting to map through that entry will fail if it extends beyond the 128 MB
|
||||
// boundary, which can happen when the sizes of MEM1/MEM2 are overridden.
|
||||
constexpr vm_prot_t prot = VM_PROT_READ | VM_PROT_WRITE | MAP_MEM_VM_SHARE;
|
||||
|
||||
retval = mach_make_memory_entry_64(mach_task_self(), &entry_size, m_shm_address, prot,
|
||||
&m_shm_entry, MACH_PORT_NULL);
|
||||
|
||||
@@ -802,7 +802,7 @@ const char* StrNetworkError()
|
||||
|
||||
bool SetPlatformSocketOptions(int fd [[maybe_unused]])
|
||||
{
|
||||
#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
|
||||
#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__NetBSD__)
|
||||
int opt_no_sigpipe = 1;
|
||||
if (setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &opt_no_sigpipe, sizeof(opt_no_sigpipe)) < 0)
|
||||
{
|
||||
|
||||
@@ -80,11 +80,15 @@ const std::string& GetScmUpdateTrackStr()
|
||||
const std::string& GetNetplayDolphinVer()
|
||||
{
|
||||
#ifdef _WIN32
|
||||
static const std::string netplay_dolphin_ver = SCM_DESC_STR " Win";
|
||||
static const std::string netplay_dolphin_ver = SCM_DESC_STR " Windows";
|
||||
#elif __APPLE__
|
||||
static const std::string netplay_dolphin_ver = SCM_DESC_STR " Mac";
|
||||
static const std::string netplay_dolphin_ver = SCM_DESC_STR " macOS";
|
||||
#elif ANDROID
|
||||
static const std::string netplay_dolphin_ver = SCM_DESC_STR " Android";
|
||||
#elif __linux__
|
||||
static const std::string netplay_dolphin_ver = SCM_DESC_STR " Linux";
|
||||
#else
|
||||
static const std::string netplay_dolphin_ver = SCM_DESC_STR " Lin";
|
||||
static const std::string netplay_dolphin_ver = SCM_DESC_STR;
|
||||
#endif
|
||||
return netplay_dolphin_ver;
|
||||
}
|
||||
|
||||
@@ -6,18 +6,18 @@ OSMinimumVersionWin11=10.0.22000.0
|
||||
// This is the runtime which was compiled against - providing a way for Updater to detect if update
|
||||
// is needed before executing this binary. Note that, annoyingly, the version in environment
|
||||
// variables does not match the "real" version. Consider:
|
||||
// VersionInfo : File: C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Redist\MSVC\14.32.31326\vc_redist.x64.exe
|
||||
// VersionInfo : File: C:\Program Files\Microsoft Visual Studio\18\Community\VC\Redist\MSVC\14.51.36231\vc_redist.x64.exe
|
||||
// InternalName: setup
|
||||
// OriginalFilename: VC_redist.x64.exe
|
||||
// FileVersion: 14.32.31332.0
|
||||
// FileDescription: Microsoft Visual C++ 2015-2022 Redistributable (x64) - 14.32.31332
|
||||
// Product: Microsoft Visual C++ 2015-2022 Redistributable (x64) - 14.32.31332
|
||||
// ProductVersion: 14.32.31332.0
|
||||
// FileVersion: 14.51.36247.0
|
||||
// FileDescription: Microsoft Visual C++ v14 Redistributable (x64) - 14.51.36247
|
||||
// Product: Microsoft Visual C++ v14 Redistributable (x64) - 14.51.36247
|
||||
// ProductVersion: 14.51.36247.0
|
||||
// Whereas the environment variables look like:
|
||||
// VCToolsInstallDir=C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Tools\MSVC\14.33.31629\
|
||||
// VCToolsRedistDir=C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Redist\MSVC\14.32.31326\
|
||||
// VCToolsVersion=14.33.31629
|
||||
// We're really looking for "14.32.31332.0" (because that's what will appear in the registry once
|
||||
// VCToolsInstallDir=C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.51.36231\
|
||||
// VCToolsRedistDir=C:\Program Files\Microsoft Visual Studio\18\Community\VC\Redist\MSVC\14.51.36231\
|
||||
// VCToolsVersion=14.51.36231
|
||||
// We're really looking for "14.51.36247.00" (because that's what will appear in the registry once
|
||||
// installed), NOT the other values!
|
||||
VCToolsVersion=${VC_TOOLS_VERSION}
|
||||
VCToolsUpdateURL=https://aka.ms/vs/17/release/vc_redist.x64.exe
|
||||
VCToolsUpdateURL=https://aka.ms/vc14/vc_redist.x64.exe
|
||||
@@ -1261,6 +1261,9 @@ void AchievementManager::HandleAchievementProgressIndicatorShowEvent(
|
||||
const rc_client_event_t* client_event)
|
||||
{
|
||||
auto& instance = AchievementManager::GetInstance();
|
||||
instance.update_event.Trigger(UpdatedItems{.achievements = {client_event->achievement->id}});
|
||||
if (!Config::Get(Config::RA_PROGRESS_ENABLED))
|
||||
return;
|
||||
auto current_time = std::chrono::steady_clock::now();
|
||||
const auto message_wait_time = std::chrono::milliseconds{OSD::Duration::SHORT};
|
||||
if (current_time - instance.m_last_progress_message < message_wait_time)
|
||||
@@ -1270,7 +1273,6 @@ void AchievementManager::HandleAchievementProgressIndicatorShowEvent(
|
||||
OSD::Duration::SHORT, OSD::Color::GREEN,
|
||||
&instance.GetAchievementBadge(client_event->achievement->id, false));
|
||||
instance.m_last_progress_message = current_time;
|
||||
instance.update_event.Trigger(UpdatedItems{.achievements = {client_event->achievement->id}});
|
||||
}
|
||||
|
||||
void AchievementManager::HandleGameCompletedEvent(const rc_client_event_t* client_event,
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "Common/Align.h"
|
||||
#include "Common/BitUtils.h"
|
||||
#include "Common/IOFile.h"
|
||||
#include "Common/Swap.h"
|
||||
#include "Core/Boot/AncastTypes.h"
|
||||
@@ -33,98 +35,91 @@ DolReader::~DolReader() = default;
|
||||
|
||||
bool DolReader::Initialize(std::span<const u8> buffer)
|
||||
{
|
||||
if (buffer.size() < sizeof(SDolHeader) || buffer.size() > UINT32_MAX)
|
||||
if (buffer.size() < sizeof(DolHeader) || buffer.size() > UINT32_MAX)
|
||||
return false;
|
||||
|
||||
memcpy(&m_dolheader, buffer.data(), sizeof(SDolHeader));
|
||||
std::memcpy(&m_dolheader, buffer.data(), sizeof(DolHeader));
|
||||
|
||||
// swap memory
|
||||
u32* p = (u32*)&m_dolheader;
|
||||
for (size_t i = 0; i < (sizeof(SDolHeader) / sizeof(u32)); i++)
|
||||
for (std::size_t i = 0; i < (sizeof(DolHeader) / sizeof(u32)); i++)
|
||||
p[i] = Common::swap32(p[i]);
|
||||
|
||||
const u32 HID4_pattern = Common::swap32(0x7c13fba6);
|
||||
const u32 HID4_mask = Common::swap32(0xfc1fffff);
|
||||
|
||||
m_is_wii = false;
|
||||
|
||||
m_text_sections.reserve(DOL_NUM_TEXT);
|
||||
for (int i = 0; i < DOL_NUM_TEXT; ++i)
|
||||
{
|
||||
if (m_dolheader.textSize[i] != 0)
|
||||
if (m_dolheader.m_text_size[i] == 0)
|
||||
continue;
|
||||
|
||||
// apploaders/IOS align the size and work from there.
|
||||
// we will do the same, and prepare data to be read later with aligned sizes
|
||||
// yes it can read too much, but thats how apploaders/IOS work
|
||||
// it will also cause the effect of the dol not loading once it is to be read out of bounds
|
||||
const u64 section_size = Common::AlignUp(static_cast<u64>(m_dolheader.m_text_size[i]), 32);
|
||||
if (section_size != m_dolheader.m_text_size[i])
|
||||
{
|
||||
if ((m_dolheader.textAddress[i] & 31) != 0 || (m_dolheader.textSize[i] & 31) != 0)
|
||||
{
|
||||
ERROR_LOG_FMT(BOOT,
|
||||
"Text section {} is not 32-byte aligned: address = 0x{:08x}, size = 0x{:x}",
|
||||
i, m_dolheader.textAddress[i], m_dolheader.textSize[i]);
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t section_offset = m_dolheader.textOffset[i];
|
||||
const std::size_t section_size = m_dolheader.textSize[i];
|
||||
|
||||
if (buffer.size() < section_offset || (buffer.size() - section_offset) < section_size)
|
||||
return false;
|
||||
|
||||
const u8* text_start = &buffer[section_offset];
|
||||
m_text_sections.emplace_back(text_start, &text_start[section_size]);
|
||||
|
||||
for (unsigned int j = 0; !m_is_wii && j < (m_dolheader.textSize[i] / sizeof(u32)); ++j)
|
||||
{
|
||||
u32 word = ((u32*)text_start)[j];
|
||||
if ((word & HID4_mask) == HID4_pattern)
|
||||
m_is_wii = true;
|
||||
}
|
||||
WARN_LOG_FMT(BOOT,
|
||||
"DolReader: Text section {} size is not aligned to 32 bytes and can cause "
|
||||
"issues when loaded",
|
||||
i);
|
||||
}
|
||||
else
|
||||
|
||||
if (buffer.size() < m_dolheader.m_text_offset[i] + section_size)
|
||||
return false;
|
||||
|
||||
auto& section = m_sections.emplace_back();
|
||||
section.m_address = m_dolheader.m_text_address[i];
|
||||
section.m_header_section_size = m_dolheader.m_text_size[i];
|
||||
section.m_data = buffer.subspan(m_dolheader.m_text_offset[i], section_size);
|
||||
|
||||
const u8* const begin = section.m_data.data();
|
||||
const u8* const end = begin + section_size;
|
||||
for (const u8* ptr = begin; ptr < end; ptr += 4)
|
||||
{
|
||||
// Make sure that m_text_sections indexes match header indexes
|
||||
m_text_sections.emplace_back();
|
||||
if ((Common::BitCastPtr<u32>(ptr) & HID4_mask) != HID4_pattern)
|
||||
continue;
|
||||
|
||||
m_is_wii = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_data_sections.reserve(DOL_NUM_DATA);
|
||||
for (int i = 0; i < DOL_NUM_DATA; ++i)
|
||||
{
|
||||
if (m_dolheader.dataSize[i] != 0)
|
||||
if (m_dolheader.m_data_size[i] == 0)
|
||||
continue;
|
||||
|
||||
const u64 section_size = Common::AlignUp(static_cast<u64>(m_dolheader.m_data_size[i]), 32);
|
||||
const u32 section_offset = m_dolheader.m_data_offset[i];
|
||||
if (section_size != m_dolheader.m_data_size[i])
|
||||
{
|
||||
const std::size_t section_size = m_dolheader.dataSize[i];
|
||||
const std::size_t section_offset = m_dolheader.dataOffset[i];
|
||||
if ((m_dolheader.dataAddress[i] & 31) != 0 || (section_size & 31) != 0)
|
||||
{
|
||||
ERROR_LOG_FMT(BOOT,
|
||||
"Data section {} is not 32-byte aligned: address = 0x{:08x}, size = 0x{:x}",
|
||||
i, m_dolheader.dataAddress[i], section_size);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (buffer.size() < section_offset)
|
||||
return false;
|
||||
|
||||
std::vector<u8> data(section_size);
|
||||
const u8* data_start = &buffer[section_offset];
|
||||
std::memcpy(&data[0], data_start, std::min(section_size, buffer.size() - section_offset));
|
||||
m_data_sections.emplace_back(data);
|
||||
WARN_LOG_FMT(BOOT,
|
||||
"DolReader: Data section {} size is not aligned to 32 bytes and can cause "
|
||||
"issues when loaded",
|
||||
i);
|
||||
}
|
||||
else
|
||||
|
||||
if (buffer.size() < section_offset + section_size)
|
||||
return false;
|
||||
|
||||
auto& section = m_sections.emplace_back();
|
||||
section.m_address = m_dolheader.m_data_address[i];
|
||||
section.m_header_section_size = m_dolheader.m_data_size[i];
|
||||
section.m_data = buffer.subspan(section_offset, section_size);
|
||||
|
||||
// Check if this dol contains an ancast image
|
||||
// The ancast image will always be in the first data section
|
||||
if (i == 0 && section.m_header_section_size > sizeof(EspressoAncastHeader) &&
|
||||
section.m_address == ESPRESSO_ANCAST_LOCATION_VIRT &&
|
||||
Common::swap32(section.m_data.data()) == ANCAST_MAGIC)
|
||||
{
|
||||
// Make sure that m_data_sections indexes match header indexes
|
||||
m_data_sections.emplace_back();
|
||||
m_ancast_index = m_sections.size() - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if this dol contains an ancast image
|
||||
// The ancast image will always be in the first data section
|
||||
m_is_ancast = false;
|
||||
if (m_data_sections[0].size() > sizeof(EspressoAncastHeader) &&
|
||||
m_dolheader.dataAddress[0] == ESPRESSO_ANCAST_LOCATION_VIRT)
|
||||
{
|
||||
// Check for the ancast magic
|
||||
if (Common::swap32(m_data_sections[0].data()) == ANCAST_MAGIC)
|
||||
m_is_ancast = true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -133,33 +128,18 @@ bool DolReader::LoadIntoMemory(Core::System& system, bool only_in_mem1) const
|
||||
if (!m_is_valid)
|
||||
return false;
|
||||
|
||||
if (m_is_ancast)
|
||||
if (IsAncast())
|
||||
return LoadAncastIntoMemory(system);
|
||||
|
||||
auto& memory = system.GetMemory();
|
||||
|
||||
// load all text (code) sections
|
||||
for (size_t i = 0; i < m_text_sections.size(); ++i)
|
||||
// load all loadable sections
|
||||
for (auto& section : m_sections)
|
||||
{
|
||||
if (!m_text_sections[i].empty() &&
|
||||
!(only_in_mem1 &&
|
||||
m_dolheader.textAddress[i] + m_text_sections[i].size() >= memory.GetRamSizeReal()))
|
||||
{
|
||||
memory.CopyToEmu(m_dolheader.textAddress[i], m_text_sections[i].data(),
|
||||
m_text_sections[i].size());
|
||||
}
|
||||
}
|
||||
if (only_in_mem1 && section.m_address + section.m_data.size() >= memory.GetRamSizeReal())
|
||||
continue;
|
||||
|
||||
// load all data sections
|
||||
for (size_t i = 0; i < m_data_sections.size(); ++i)
|
||||
{
|
||||
if (!m_data_sections[i].empty() &&
|
||||
!(only_in_mem1 &&
|
||||
m_dolheader.dataAddress[i] + m_data_sections[i].size() >= memory.GetRamSizeReal()))
|
||||
{
|
||||
memory.CopyToEmu(m_dolheader.dataAddress[i], m_data_sections[i].data(),
|
||||
m_data_sections[i].size());
|
||||
}
|
||||
memory.CopyToEmu(section.m_address, section.m_data.data(), section.m_data.size());
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -168,48 +148,47 @@ bool DolReader::LoadIntoMemory(Core::System& system, bool only_in_mem1) const
|
||||
// On a real console this would be done in the Espresso bootrom
|
||||
bool DolReader::LoadAncastIntoMemory(Core::System& system) const
|
||||
{
|
||||
// The ancast image will always be in data section 0
|
||||
const auto& section = m_data_sections[0];
|
||||
const u32 section_address = m_dolheader.dataAddress[0];
|
||||
|
||||
const auto* header = reinterpret_cast<const EspressoAncastHeader*>(section.data());
|
||||
const LoadableSection& section = m_sections[m_ancast_index.value()];
|
||||
const u32 section_address = m_dolheader.m_data_address[0];
|
||||
const EspressoAncastHeader header =
|
||||
Common::BitCastPtr<EspressoAncastHeader>(section.m_data.data());
|
||||
|
||||
// Verify header block size
|
||||
if (Common::swap32(header->header_block.header_block_size) != sizeof(AncastHeaderBlock))
|
||||
if (Common::swap32(header.header_block.header_block_size) != sizeof(AncastHeaderBlock))
|
||||
{
|
||||
ERROR_LOG_FMT(BOOT, "Ancast: Invalid header block size: 0x{:x}",
|
||||
Common::swap32(header->header_block.header_block_size));
|
||||
Common::swap32(header.header_block.header_block_size));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure this is a PPC ancast image
|
||||
if (Common::swap32(header->signature_block.signature_type) != 0x01)
|
||||
if (Common::swap32(header.signature_block.signature_type) != 0x01)
|
||||
{
|
||||
ERROR_LOG_FMT(BOOT, "Ancast: Invalid signature type: 0x{:x}",
|
||||
Common::swap32(header->signature_block.signature_type));
|
||||
Common::swap32(header.signature_block.signature_type));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Make sure this is a Wii-Mode ancast image
|
||||
if (Common::swap32(header->info_block.image_type) != ANCAST_IMAGE_TYPE_ESPRESSO_WII)
|
||||
if (Common::swap32(header.info_block.image_type) != ANCAST_IMAGE_TYPE_ESPRESSO_WII)
|
||||
{
|
||||
ERROR_LOG_FMT(BOOT, "Ancast: Invalid image type: 0x{:x}",
|
||||
Common::swap32(header->info_block.image_type));
|
||||
Common::swap32(header.info_block.image_type));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify the body size
|
||||
const u32 body_size = Common::swap32(header->info_block.body_size);
|
||||
if (body_size + sizeof(EspressoAncastHeader) > section.size())
|
||||
const u32 body_size = Common::swap32(header.info_block.body_size);
|
||||
if (body_size + sizeof(EspressoAncastHeader) > section.m_header_section_size)
|
||||
{
|
||||
ERROR_LOG_FMT(BOOT, "Ancast: Invalid body size: 0x{:x}", body_size);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify the body hash
|
||||
const auto digest =
|
||||
Common::SHA1::CalculateDigest(section.data() + sizeof(EspressoAncastHeader), body_size);
|
||||
if (digest != header->info_block.body_hash)
|
||||
const auto digest = Common::SHA1::CalculateDigest(
|
||||
section.m_data.data() + sizeof(EspressoAncastHeader), body_size);
|
||||
if (digest != header.info_block.body_hash)
|
||||
{
|
||||
ERROR_LOG_FMT(BOOT, "Ancast: Body hash mismatch");
|
||||
return false;
|
||||
@@ -217,14 +196,14 @@ bool DolReader::LoadAncastIntoMemory(Core::System& system) const
|
||||
|
||||
// Check if this is a retail or dev image
|
||||
bool is_dev = false;
|
||||
if (Common::swap32(header->info_block.console_type) == ANCAST_CONSOLE_TYPE_DEV)
|
||||
if (Common::swap32(header.info_block.console_type) == ANCAST_CONSOLE_TYPE_DEV)
|
||||
{
|
||||
is_dev = true;
|
||||
}
|
||||
else if (Common::swap32(header->info_block.console_type) != ANCAST_CONSOLE_TYPE_RETAIL)
|
||||
else if (Common::swap32(header.info_block.console_type) != ANCAST_CONSOLE_TYPE_RETAIL)
|
||||
{
|
||||
ERROR_LOG_FMT(BOOT, "Ancast: Invalid console type: 0x{:x}",
|
||||
Common::swap32(header->info_block.console_type));
|
||||
Common::swap32(header.info_block.console_type));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -240,14 +219,14 @@ bool DolReader::LoadAncastIntoMemory(Core::System& system) const
|
||||
static constexpr u8 vwii_ancast_iv[0x10] = {0x59, 0x6d, 0x5a, 0x9a, 0xd7, 0x05, 0xf9, 0x4f,
|
||||
0xe1, 0x58, 0x02, 0x6f, 0xea, 0xa7, 0xb8, 0x87};
|
||||
std::vector<u8> decrypted(body_size);
|
||||
if (!ctx->Crypt(vwii_ancast_iv, section.data() + sizeof(EspressoAncastHeader), decrypted.data(),
|
||||
body_size))
|
||||
if (!ctx->Crypt(vwii_ancast_iv, section.m_data.data() + sizeof(EspressoAncastHeader),
|
||||
decrypted.data(), body_size))
|
||||
return false;
|
||||
|
||||
auto& memory = system.GetMemory();
|
||||
|
||||
// Copy the Ancast header to the emu
|
||||
memory.CopyToEmu(section_address, header, sizeof(EspressoAncastHeader));
|
||||
memory.CopyToEmu(section_address, &header, sizeof(EspressoAncastHeader));
|
||||
|
||||
// Copy the decrypted body to the emu
|
||||
memory.CopyToEmu(section_address + sizeof(EspressoAncastHeader), decrypted.data(), body_size);
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -24,8 +26,8 @@ public:
|
||||
|
||||
bool IsValid() const override { return m_is_valid; }
|
||||
bool IsWii() const override { return m_is_wii; }
|
||||
bool IsAncast() const { return m_is_ancast; }
|
||||
u32 GetEntryPoint() const override { return m_dolheader.entryPoint; }
|
||||
bool IsAncast() const { return m_ancast_index.has_value(); }
|
||||
u32 GetEntryPoint() const override { return m_dolheader.m_entrypoint; }
|
||||
bool LoadIntoMemory(Core::System& system, bool only_in_mem1 = false) const override;
|
||||
bool LoadSymbols(const Core::CPUThreadGuard& guard, PPCSymbolDB& ppc_symbol_db,
|
||||
const std::string& filename) const override
|
||||
@@ -40,29 +42,35 @@ private:
|
||||
DOL_NUM_DATA = 11
|
||||
};
|
||||
|
||||
struct SDolHeader
|
||||
struct DolHeader
|
||||
{
|
||||
u32 textOffset[DOL_NUM_TEXT];
|
||||
u32 dataOffset[DOL_NUM_DATA];
|
||||
u32 m_text_offset[DOL_NUM_TEXT];
|
||||
u32 m_data_offset[DOL_NUM_DATA];
|
||||
|
||||
u32 textAddress[DOL_NUM_TEXT];
|
||||
u32 dataAddress[DOL_NUM_DATA];
|
||||
u32 m_text_address[DOL_NUM_TEXT];
|
||||
u32 m_data_address[DOL_NUM_DATA];
|
||||
|
||||
u32 textSize[DOL_NUM_TEXT];
|
||||
u32 dataSize[DOL_NUM_DATA];
|
||||
u32 m_text_size[DOL_NUM_TEXT];
|
||||
u32 m_data_size[DOL_NUM_DATA];
|
||||
|
||||
u32 bssAddress;
|
||||
u32 bssSize;
|
||||
u32 entryPoint;
|
||||
u32 m_bss_address;
|
||||
u32 m_bss_size;
|
||||
u32 m_entrypoint;
|
||||
};
|
||||
SDolHeader m_dolheader;
|
||||
DolHeader m_dolheader;
|
||||
|
||||
std::vector<std::vector<u8>> m_data_sections;
|
||||
std::vector<std::vector<u8>> m_text_sections;
|
||||
struct LoadableSection
|
||||
{
|
||||
u32 m_address;
|
||||
u32 m_header_section_size;
|
||||
std::span<const u8> m_data;
|
||||
};
|
||||
|
||||
std::vector<LoadableSection> m_sections;
|
||||
|
||||
bool m_is_valid;
|
||||
bool m_is_wii;
|
||||
bool m_is_ancast;
|
||||
std::optional<std::size_t> m_ancast_index;
|
||||
|
||||
// Copy sections to internal buffers
|
||||
bool Initialize(std::span<const u8> buffer);
|
||||
|
||||
@@ -116,6 +116,29 @@ void SaveToDTM(Movie::DTMHeader* dtm)
|
||||
|
||||
dtm->bFollowBranch = Config::Get(Config::MAIN_JIT_FOLLOW_BRANCH);
|
||||
|
||||
dtm->controllers = 0;
|
||||
dtm->bongos = 0;
|
||||
dtm->GBAControllers = 0;
|
||||
for (int i = 0; i < SerialInterface::MAX_SI_CHANNELS; ++i)
|
||||
{
|
||||
const SerialInterface::SIDevices si_device = Config::Get(Config::GetInfoForSIDevice(i));
|
||||
if (si_device != SerialInterface::SIDEVICE_NONE)
|
||||
dtm->controllers |= 1 << i;
|
||||
|
||||
if (si_device == SerialInterface::SIDEVICE_GC_GBA_EMULATED)
|
||||
dtm->GBAControllers |= 1 << i;
|
||||
else if (si_device == SerialInterface::SIDEVICE_GC_TARUKONGA)
|
||||
dtm->bongos |= 1 << i;
|
||||
}
|
||||
if (dtm->bWii)
|
||||
{
|
||||
for (int i = 0; i < MAX_WIIMOTES; ++i)
|
||||
{
|
||||
if (Config::Get(Config::GetInfoForWiimoteSource(i)) != WiimoteSource::None)
|
||||
dtm->controllers |= 1 << (i + 4);
|
||||
}
|
||||
}
|
||||
|
||||
// Settings which only existed in old Dolphin versions
|
||||
dtm->bSkipIdle = true;
|
||||
dtm->bEFBCopyEnable = true;
|
||||
|
||||
@@ -277,9 +277,7 @@ bool Init(Core::System& system, std::unique_ptr<BootParameters> boot, const Wind
|
||||
|
||||
static void ResetRumble()
|
||||
{
|
||||
#if defined(__LIBUSB__)
|
||||
GCAdapter::ResetRumble();
|
||||
#endif
|
||||
if (!Pad::IsInitialized())
|
||||
return;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
@@ -465,8 +463,7 @@ static void FifoPlayerThread(Core::System& system, const std::optional<std::stri
|
||||
}
|
||||
else
|
||||
{
|
||||
// FIFO log does not contain any frames, cannot continue.
|
||||
PanicAlertFmt("FIFO file is invalid, cannot playback.");
|
||||
PanicAlertFmt("FIFO file doesn't contain any frame, cannot playback.");
|
||||
system.GetFifoPlayer().Close();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ u16 Accelerator::ReadSample(const s16* coefs)
|
||||
raw_sample -= 16;
|
||||
|
||||
s32 val32 = (scale * raw_sample) + ((0x400 + coef1 * m_yn1 + coef2 * m_yn2) >> 11);
|
||||
val = static_cast<s16>(std::clamp<s32>(val32, -0x7FFF, 0x7FFF));
|
||||
val = MathUtil::SaturatingCast<s16>(val32);
|
||||
step_size = 2;
|
||||
|
||||
m_yn2 = m_yn1;
|
||||
|
||||
@@ -628,7 +628,11 @@ void ZeldaUCode::RenderAudio()
|
||||
if (m_rendering_curr_voice == 0)
|
||||
m_renderer.PrepareFrame();
|
||||
|
||||
while (m_rendering_curr_voice < m_rendering_voices_per_frame)
|
||||
// TODO: Find out the actual hardware behavior for the case where
|
||||
// (m_rendering_voices_per_frame >> 4) >= m_sync_voice_skip_flags.size()
|
||||
const u16 number_of_voices = std::min(m_rendering_voices_per_frame,
|
||||
static_cast<u16>(m_sync_voice_skip_flags.size() << 4));
|
||||
while (m_rendering_curr_voice < number_of_voices)
|
||||
{
|
||||
// If we are not meant to render this voice yet, go back to message
|
||||
// processing.
|
||||
@@ -1468,9 +1472,16 @@ void ZeldaAudioRenderer::LoadInputSamples(MixingBuffer* buffer, VPB* vpb)
|
||||
// the end of processing, if needed.
|
||||
//
|
||||
// Maximum of 0x500 samples here - see NeededRawSamplesCount to understand
|
||||
// this practical limit (resampling_ratio = 0xFFFF -> 0x500 samples). Add a
|
||||
// margin of 4 that is needed for samples source that do resampling.
|
||||
std::array<s16, 0x500 + 4> raw_input_samples;
|
||||
// this practical limit (resampling_ratio = 0xFFFF -> 0x500 samples).
|
||||
//
|
||||
// If current_pos_frac contains an (invalid) non-fractional part, it can push
|
||||
// this up by another 15 samples. Which DownloadAFCSamplesFromARAM then rounds
|
||||
// up to the next multiple of 16. So add an extra 0x10 samples to be safe.
|
||||
//
|
||||
// Plus we need an extra four samples at the start to hold the last four
|
||||
// samples from the previous frame.
|
||||
|
||||
std::array<s16, 4 + 0x500 + 0x10> raw_input_samples;
|
||||
for (size_t i = 0; i < 4; ++i)
|
||||
raw_input_samples[i] = vpb->resample_buffer[i];
|
||||
|
||||
@@ -1715,6 +1726,12 @@ void ZeldaAudioRenderer::DownloadAFCSamplesFromARAM(s16* dst, VPB* vpb, u16 requ
|
||||
return;
|
||||
}
|
||||
|
||||
if (vpb->afc_remaining_decoded_samples > 0x10) [[unlikely]]
|
||||
{
|
||||
ERROR_LOG_FMT(DSPHLE, "afc_remaining_decoded_samples > 0x10");
|
||||
vpb->afc_remaining_decoded_samples = 0x10;
|
||||
}
|
||||
|
||||
// Try several things until we have output enough samples.
|
||||
while (true)
|
||||
{
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
#include "VideoCommon/OnScreenDisplay.h"
|
||||
|
||||
#if defined(__linux__) or defined(__APPLE__) or defined(__FreeBSD__) or defined(__NetBSD__) or \
|
||||
defined(__HAIKU__)
|
||||
defined(__OpenBSD__) or defined(__HAIKU__)
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ void GeckoSockServer::ClientThread()
|
||||
{
|
||||
did_nothing = false;
|
||||
|
||||
recv_fifo.insert(recv_fifo.end(), buffer.data(), &buffer[got]);
|
||||
recv_fifo.insert(recv_fifo.end(), buffer.data(), buffer.data() + got);
|
||||
}
|
||||
|
||||
if (!send_fifo.empty())
|
||||
|
||||
@@ -544,7 +544,7 @@ inline s32 GCMemcardDirectory::SaveAreaRW(u32 block, bool writing)
|
||||
}
|
||||
|
||||
const int idx = m_saves[i].UsesBlock(block);
|
||||
if (idx != -1)
|
||||
if (idx >= 0)
|
||||
{
|
||||
if (!m_saves[i].LoadSaveBlocks())
|
||||
{
|
||||
@@ -561,6 +561,12 @@ inline s32 GCMemcardDirectory::SaveAreaRW(u32 block, bool writing)
|
||||
m_saves[i].m_dirty = true;
|
||||
}
|
||||
|
||||
if (static_cast<size_t>(idx) >= m_saves[i].m_save_data.size())
|
||||
{
|
||||
PanicAlertFmt("Block index ({0}) is larger than the number of available blocks ({1})",
|
||||
idx, m_saves[i].m_save_data.size());
|
||||
return -1;
|
||||
}
|
||||
m_last_block = block;
|
||||
m_last_block_address = m_saves[i].m_save_data[idx].m_block.data();
|
||||
return m_last_block;
|
||||
|
||||
@@ -193,6 +193,7 @@ void MagneticCardReader::Command_33_ReadData()
|
||||
|
||||
AppendRange(&m_command_payload, *track_data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -139,12 +139,45 @@ ResultCode FileSystem::CreateFullPath(Uid uid, Gid gid, const std::string& path,
|
||||
|
||||
void FileSystem::DoStateRead(PointerWrap& p, const std::string& directory_path)
|
||||
{
|
||||
const ResultCode delete_result = Delete(0, 0, directory_path);
|
||||
if (delete_result != ResultCode::Success && delete_result != ResultCode::NotFound)
|
||||
const auto delete_ = [this](const std::string& directory_path_) {
|
||||
const ResultCode delete_result = Delete(0, 0, directory_path_);
|
||||
if (delete_result != ResultCode::Success && delete_result != ResultCode::NotFound)
|
||||
{
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call Delete for {}: {}", directory_path_,
|
||||
delete_result);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (directory_path != "/")
|
||||
{
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call Delete: {}", delete_result);
|
||||
p.SetVerifyMode();
|
||||
return;
|
||||
if (!delete_(directory_path))
|
||||
{
|
||||
p.SetVerifyMode();
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Calling Delete for the root would return Invalid. Let's delete all children instead.
|
||||
auto children = ReadDirectory(0, 0, directory_path);
|
||||
if (!children)
|
||||
{
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call ReadDirectory for {}: {}", directory_path,
|
||||
children.error());
|
||||
p.SetVerifyMode();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const std::string& child_name : children.value())
|
||||
{
|
||||
if (!delete_(directory_path + child_name))
|
||||
{
|
||||
p.SetVerifyMode();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Metadata metadata{};
|
||||
@@ -156,12 +189,29 @@ void FileSystem::DoStateRead(PointerWrap& p, const std::string& directory_path)
|
||||
p.Do(metadata.size);
|
||||
p.Do(metadata.fst_index);
|
||||
|
||||
const ResultCode create_directory_result = CreateDirectory(
|
||||
metadata.uid, metadata.gid, directory_path, metadata.attribute, metadata.modes);
|
||||
if (create_directory_result != ResultCode::Success)
|
||||
// Again here, calling CreateDirectory for the root would return Invalid.
|
||||
// The root always exists, so we can skip creating it.
|
||||
if (directory_path != "/")
|
||||
{
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call CreateDirectory: {}",
|
||||
create_directory_result);
|
||||
// UID 0 is used to avoid AccessDenied.
|
||||
const ResultCode create_directory_result =
|
||||
CreateDirectory(0, 0, directory_path, metadata.attribute, metadata.modes);
|
||||
if (create_directory_result != ResultCode::Success)
|
||||
{
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call CreateDirectory for {}: {}", directory_path,
|
||||
create_directory_result);
|
||||
p.SetVerifyMode();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Change the UID and GID from 0 to the intended values.
|
||||
const ResultCode set_metadata_result = SetMetadata(0, directory_path, metadata.uid, metadata.gid,
|
||||
metadata.attribute, metadata.modes);
|
||||
if (set_metadata_result != ResultCode::Success)
|
||||
{
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call SetMetadata for {}: {}", directory_path,
|
||||
set_metadata_result);
|
||||
p.SetVerifyMode();
|
||||
return;
|
||||
}
|
||||
@@ -182,22 +232,34 @@ void FileSystem::DoStateRead(PointerWrap& p, const std::string& directory_path)
|
||||
|
||||
if (child_metadata.is_file)
|
||||
{
|
||||
// UID 0 is used to avoid AccessDenied.
|
||||
const ResultCode create_file_result =
|
||||
CreateFile(child_metadata.uid, child_metadata.gid, child_path, child_metadata.attribute,
|
||||
child_metadata.modes);
|
||||
CreateFile(0, 0, child_path, child_metadata.attribute, child_metadata.modes);
|
||||
if (create_file_result != ResultCode::Success)
|
||||
{
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call CreateFile for {}: {}", child_name,
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call CreateFile for {}: {}", child_path,
|
||||
create_file_result);
|
||||
p_.SetVerifyMode();
|
||||
return;
|
||||
}
|
||||
|
||||
// Change the UID and GID from 0 to the intended values.
|
||||
const ResultCode set_metadata_result =
|
||||
SetMetadata(0, child_path, child_metadata.uid, child_metadata.gid,
|
||||
child_metadata.attribute, child_metadata.modes);
|
||||
if (set_metadata_result != ResultCode::Success)
|
||||
{
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call SetMetadata for {}: {}", child_path,
|
||||
set_metadata_result);
|
||||
p_.SetVerifyMode();
|
||||
return;
|
||||
}
|
||||
|
||||
std::array<u8, BUFFER_CHUNK_SIZE> buffer;
|
||||
Result<FileHandle> handle = OpenFile(0, 0, child_path, Mode::Write);
|
||||
if (!handle)
|
||||
{
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call OpenFile for {}: {}", child_name,
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call OpenFile for {}: {}", child_path,
|
||||
handle.error());
|
||||
p_.SetVerifyMode();
|
||||
return;
|
||||
@@ -213,7 +275,7 @@ void FileSystem::DoStateRead(PointerWrap& p, const std::string& directory_path)
|
||||
Result<size_t> write_result = handle->Write(buffer.data(), bytes_to_write);
|
||||
if (!write_result)
|
||||
{
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call Write for {}: {}", child_name,
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateRead failed to call Write for {}: {}", child_path,
|
||||
write_result.error());
|
||||
p_.SetVerifyMode();
|
||||
return;
|
||||
@@ -221,7 +283,7 @@ void FileSystem::DoStateRead(PointerWrap& p, const std::string& directory_path)
|
||||
if (*write_result != bytes_to_write)
|
||||
{
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateRead tried to write {} bytes to {} but wrote {} bytes",
|
||||
child_name, bytes_to_write, *write_result);
|
||||
child_path, bytes_to_write, *write_result);
|
||||
p_.SetVerifyMode();
|
||||
return;
|
||||
}
|
||||
@@ -241,7 +303,8 @@ void FileSystem::DoStateWriteOrMeasure(PointerWrap& p, const std::string& direct
|
||||
const Result<Metadata> metadata = GetMetadata(0, 0, directory_path);
|
||||
if (!metadata)
|
||||
{
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateWriteOrMeasure failed to call GetMetadata: {}", metadata.error());
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateWriteOrMeasure failed to call GetMetadata for {}: {}",
|
||||
directory_path, metadata.error());
|
||||
p.SetVerifyMode();
|
||||
return;
|
||||
}
|
||||
@@ -256,8 +319,8 @@ void FileSystem::DoStateWriteOrMeasure(PointerWrap& p, const std::string& direct
|
||||
auto children = ReadDirectory(0, 0, directory_path);
|
||||
if (!children)
|
||||
{
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateWriteOrMeasure failed to call ReadDirectory: {}",
|
||||
children.error());
|
||||
ERROR_LOG_FMT(IOS_FS, "DoStateWriteOrMeasure failed to call ReadDirectory for {}: {}",
|
||||
directory_path, children.error());
|
||||
p.SetVerifyMode();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -279,8 +279,18 @@ HostFileSystem::FstEntry* HostFileSystem::GetFstEntryForPath(const std::string&
|
||||
|
||||
void HostFileSystem::DoState(PointerWrap& p)
|
||||
{
|
||||
// Temporarily close the file, to prevent any issues with the savestating of files/folders.
|
||||
for (Handle& handle : m_handles)
|
||||
// This piece of code is handling four separate problems:
|
||||
// 1. Close host handles by calling reset on them, in case DoStateRead needs to modify a file that
|
||||
// was open.
|
||||
// 2. Close guest handles by setting opened to false on each element in m_handles, in case
|
||||
// DoStateRead needs to modify a file that was open.
|
||||
// 3. Close guest handles by setting opened to false on each element in m_handles, because if all
|
||||
// of them were open, it would make DoStateRead/DoStateWriteOrMeasure's calls to OpenFile fail.
|
||||
// 4. Create a copy of m_handles that we can restore later in case we're writing/measuring,
|
||||
// because OpenFile happily stomps over elements in m_handles that have opened set to false.
|
||||
auto handles_copy = std::move(m_handles);
|
||||
m_handles = {};
|
||||
for (Handle& handle : handles_copy)
|
||||
handle.host_file.reset();
|
||||
|
||||
// The format for the next part of the save state is follows:
|
||||
@@ -316,6 +326,8 @@ void HostFileSystem::DoState(PointerWrap& p)
|
||||
memcpy(nand_size_ptr, &size_of_nand, sizeof(size_of_nand));
|
||||
}
|
||||
}
|
||||
|
||||
m_handles = std::move(handles_copy);
|
||||
}
|
||||
else // case where we're in read mode.
|
||||
{
|
||||
|
||||
@@ -221,7 +221,7 @@ static void ReleasePPCAncast(Core::System& system)
|
||||
void RAMOverrideForIOSMemoryValues(Memory::MemoryManager& memory, MemorySetupType setup_type)
|
||||
{
|
||||
// Don't touch anything if the feature isn't enabled.
|
||||
if (!Config::Get(Config::MAIN_RAM_OVERRIDE_ENABLE) ||
|
||||
if (!Config::Get(Config::MAIN_RAM_OVERRIDE_ENABLE) &&
|
||||
SConfig::GetInstance().GetSimulatedMemorySize() <= Memory::MEM1_SIZE_RETAIL)
|
||||
{
|
||||
return;
|
||||
|
||||
@@ -146,6 +146,7 @@ std::optional<IPCReply> BluetoothRealDevice::IOCtlV(const IOCtlVRequest& request
|
||||
}
|
||||
|
||||
ERROR_LOG_FMT(IOS_WIIMOTE, "IOCTLV_USBV0_INTRMSG: Unknown endpoint: 0x{:02x}", cmd->endpoint);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ERROR_LOG_FMT(IOS_WIIMOTE, "IOCtlV: Unknown request: 0x{:08x}", request.request);
|
||||
|
||||
@@ -1309,17 +1309,6 @@ void MovieManager::SaveRecording(const std::string& filename)
|
||||
header.filetype[3] = 0x1A;
|
||||
strncpy(header.gameID.data(), SConfig::GetInstance().GetGameID().c_str(), 6);
|
||||
header.bWii = m_system.IsWii();
|
||||
header.controllers = 0;
|
||||
header.GBAControllers = 0;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
{
|
||||
if (IsUsingGBA(i))
|
||||
header.GBAControllers |= 1 << i;
|
||||
if (IsUsingPad(i))
|
||||
header.controllers |= 1 << i;
|
||||
if (IsUsingWiimote(i) && m_system.IsWii())
|
||||
header.controllers |= 1 << (i + 4);
|
||||
}
|
||||
|
||||
header.bFromSaveState = m_recording_from_save_state;
|
||||
header.frameCount = m_total_frames;
|
||||
@@ -1336,7 +1325,6 @@ void MovieManager::SaveRecording(const std::string& filename)
|
||||
strncpy(header.discChange.data(), m_disc_change_filename.c_str(), header.discChange.size());
|
||||
strncpy(header.author.data(), m_author.c_str(), header.author.size());
|
||||
header.md5 = m_md5;
|
||||
header.bongos = m_bongos;
|
||||
header.revision = m_revision;
|
||||
header.DSPiromHash = m_dsp_irom_hash;
|
||||
header.DSPcoefHash = m_dsp_coef_hash;
|
||||
|
||||
@@ -2273,6 +2273,15 @@ void NetPlayServer::AssignNewUserAPad(const Client& player)
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (PlayerId& mapping : m_wiimote_map)
|
||||
{
|
||||
// 0 means unmapped
|
||||
if (mapping == 0)
|
||||
{
|
||||
mapping = player.pid;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PlayerId NetPlayServer::GiveFirstAvailableIDTo(ENetPeer* player)
|
||||
|
||||
@@ -137,7 +137,7 @@ public:
|
||||
// space), a PanicAlert will be shown to the user and zero (or an empty string for the string
|
||||
// case) will be returned.
|
||||
template <std::unsigned_integral T>
|
||||
static T HostRead(const Core::CPUThreadGuard& guard, const u32 address);
|
||||
static T HostRead(const Core::CPUThreadGuard& guard, u32 address);
|
||||
template <typename T>
|
||||
requires(!std::unsigned_integral<T>)
|
||||
static T HostRead(const Core::CPUThreadGuard& guard, const u32 address)
|
||||
@@ -167,7 +167,7 @@ public:
|
||||
requires(!std::unsigned_integral<T>)
|
||||
{
|
||||
using U = Common::MakeUnsignedSameSize<T>;
|
||||
std::optional<ReadResult<U>> result = HostTryRead<U>(guard, address);
|
||||
std::optional<ReadResult<U>> result = HostTryRead<U>(guard, address, space);
|
||||
return std::bit_cast<std::optional<ReadResult<T>>>(result);
|
||||
}
|
||||
static std::optional<ReadResult<u32>>
|
||||
@@ -198,8 +198,8 @@ public:
|
||||
// user-visible alert on failure.
|
||||
template <std::unsigned_integral T>
|
||||
static std::optional<WriteResult>
|
||||
HostTryWrite(const Core::CPUThreadGuard& guard, const Common::MakeAtLeastU32<T> var,
|
||||
const u32 address, RequestedAddressSpace space = RequestedAddressSpace::Effective);
|
||||
HostTryWrite(const Core::CPUThreadGuard& guard, Common::MakeAtLeastU32<T> var, u32 address,
|
||||
RequestedAddressSpace space = RequestedAddressSpace::Effective);
|
||||
template <typename T>
|
||||
static std::optional<WriteResult>
|
||||
HostTryWrite(const Core::CPUThreadGuard& guard, const T var, const u32 address,
|
||||
@@ -228,10 +228,10 @@ public:
|
||||
TryReadInstResult TryReadInstruction(u32 address);
|
||||
|
||||
template <std::unsigned_integral T>
|
||||
T Read(const u32 address);
|
||||
T Read(u32 address);
|
||||
|
||||
template <std::unsigned_integral T>
|
||||
void Write(const Common::MakeAtLeastU32<T> var, const u32 address);
|
||||
void Write(Common::MakeAtLeastU32<T> var, u32 address);
|
||||
|
||||
void Write_U16_Swap(u32 var, u32 address);
|
||||
void Write_U32_Swap(u32 var, u32 address);
|
||||
@@ -328,7 +328,7 @@ private:
|
||||
TranslateAddressResult TranslateAddress(u32 address);
|
||||
|
||||
template <const XCheckTLBFlag flag>
|
||||
TranslateAddressResult TranslatePageAddress(const EffectiveAddress address, bool* wi);
|
||||
TranslateAddressResult TranslatePageAddress(EffectiveAddress address, bool* wi);
|
||||
|
||||
void GenerateDSIException(u32 effective_address, bool write);
|
||||
void GenerateISIException(u32 effective_address);
|
||||
@@ -345,7 +345,7 @@ private:
|
||||
template <XCheckTLBFlag flag, std::unsigned_integral T, bool never_translate = false>
|
||||
T ReadFromHardware(u32 em_address);
|
||||
template <XCheckTLBFlag flag, bool never_translate = false>
|
||||
void WriteToHardware(u32 em_address, const u32 data, const u32 size);
|
||||
void WriteToHardware(u32 em_address, u32 data, u32 size);
|
||||
template <XCheckTLBFlag flag>
|
||||
bool IsEffectiveRAMAddress(u32 address);
|
||||
bool IsPhysicalRAMAddress(u32 address) const;
|
||||
|
||||
Loaded 100 of 123 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user