Merge tag '2609' into 2606a-update

Release for September 2026
This commit is contained in:
Craig Carnell committed 2026-09-24 10:15:25 +01:00
commit eb97db0652
123 files changed
+26388 -22047

No files matched your search

+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+4
View File
@@ -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
+10
View File
@@ -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
+41
View File
@@ -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
+6
View File
@@ -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.
+6
View File
@@ -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.
+6
View File
@@ -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.
+6
View File
@@ -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.
+6
View File
@@ -0,0 +1,6 @@
# RDCE78, RDCP78 - Deadly Creatures
[Core]
# Overclock to work around long black screen on boot.
Overclock = 1.10
OverclockEnable = True
+5
View File
@@ -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 "$@"
+3 -1
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
+756 -672
View File
File diff suppressed because it is too large. Load diff
+752 -668
View File
File diff suppressed because it is too large. Load diff
+758 -674
View File
File diff suppressed because it is too large. Load diff
+770 -692
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+763 -679
View File
File diff suppressed because it is too large. Load diff
+751 -667
View File
File diff suppressed because it is too large. Load diff
+804 -714
View File
File diff suppressed because it is too large. Load diff
+751 -667
View File
File diff suppressed because it is too large. Load diff
+795 -706
View File
File diff suppressed because it is too large. Load diff
+887 -717
View File
File diff suppressed because it is too large. Load diff
+751 -667
View File
File diff suppressed because it is too large. Load diff
+772 -694
View File
File diff suppressed because it is too large. Load diff
+795 -706
View File
File diff suppressed because it is too large. Load diff
+772 -699
View File
File diff suppressed because it is too large. Load diff
+772 -697
View File
File diff suppressed because it is too large. Load diff
+764 -680
View File
File diff suppressed because it is too large. Load diff
+767 -683
View File
File diff suppressed because it is too large. Load diff
+772 -693
View File
File diff suppressed because it is too large. Load diff
+759 -675
View File
File diff suppressed because it is too large. Load diff
+752 -668
View File
File diff suppressed because it is too large. Load diff
+790 -702
View File
File diff suppressed because it is too large. Load diff
+752 -668
View File
File diff suppressed because it is too large. Load diff
+772 -699
View File
File diff suppressed because it is too large. Load diff
+751 -667
View File
File diff suppressed because it is too large. Load diff
+951 -758
View File
File diff suppressed because it is too large. Load diff
+765 -685
View File
File diff suppressed because it is too large. Load diff
+933 -847
View File
File diff suppressed because it is too large. Load diff
+754 -670
View File
File diff suppressed because it is too large. Load diff
@@ -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())
@@ -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)
@@ -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)
@@ -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
@@ -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 }
@@ -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),
@@ -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,
)
}
}
@@ -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,
@@ -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)
@@ -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
}
}
@@ -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
}
@@ -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)
}
@@ -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"
}
}
@@ -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.
@@ -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) {
@@ -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 {
@@ -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.
@@ -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
}
}
@@ -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)
}
}
@@ -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>
+75
View File
@@ -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)
+1
View File
@@ -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:
+14 -1
View File
@@ -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);
+5 -1
View File
@@ -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);
+1 -1
View File
@@ -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)
{
+7 -3
View File
@@ -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;
}
+10 -10
View File
@@ -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
+3 -1
View File
@@ -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,
+87 -108
View File
@@ -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);
+24 -16
View File
@@ -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;
+1 -4
View File
@@ -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;
}
+1 -1
View File
@@ -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;
+21 -4
View File
@@ -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)
{
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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;
+82 -19
View File
@@ -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;
}
+14 -2
View File
@@ -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.
{
+1 -1
View File
@@ -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);
-12
View File
@@ -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;
+9
View File
@@ -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)
+8 -8
View File
@@ -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